"use strict";

const express = require("express"),
  router = express.Router(),
  crypto = require("crypto"),
  { readFile, writeFile } = require("fs").promises,
  fs = require("fs"),
  accessSync = fs.accessSync,
  randomWords = require("random-words"),
  equal = require("fast-deep-equal");

const debug = {
  audio: false,
  get: true,
  set: true,
  update: false
};

let chatDB;

require("../db/chats").then(function(db) {
  chatDB = db;
});

const sessionFromColor = (chat, color) => {
  for (let key in chat.sessions) {
    if (chat.sessions[key].color === color) {
      return chat.sessions[key];
    }
  }
}

const newPerson = (color) => {
  return {
    status: "Not active", 
    lastActive: 0,
    color: color,
    name: "",
  };
}

const getSession = (chat, id) => {
  if (!chat.sessions) {
    chat.sessions = {};
  }

  /* If this session is not yet in the chat, add it and set the person's name */
  if (!(id in chat.sessions)) {
    chat.sessions[id] = {
      id: `[${id.substring(0, 8)}]`,
      name: '',
      color: '',
      person: undefined,
      lastActive: Date.now(),
      live: true
    };
  }

  const session = chat.sessions[id];
  session.lastActive = Date.now();
  session.live = true;
  if (session.person) {
    session.person.live = true;
    session.person.lastActive = session.lastActive;
  }

  /* Expire old unused sessions */
  for (let _id in chat.sessions) {
    const _session = chat.sessions[_id];
    if (_session.color || _session.name || _session.person) {
      continue;
    }
    if (_id === id) {
      continue;
    }
    /* 60 minutes */
    const age = Date.now() - _session.lastActive;
    if (age > 60 * 60 * 1000) {
      console.log(`${_session.id}: Expiring old session ${_id}: ${age/(60 * 1000)} minutes`);
      delete chat.sessions[_id];
      if (_id in chat.sessions) {
        console.log('delete DID NOT WORK!');
      }
    }
  }

  return chat.sessions[id];
};

const loadChat = async (id) => {
  if (/^\.|\//.exec(id)) {
    return undefined;
  }

  if (id in chats) {
    return chats[id];
  }

  let chat = await readFile(`chats/${id}`)
  .catch(() => {
    return;
  });

  if (chat) {
    try {
      chat = JSON.parse(chat);
      console.log(`${info}: Creating backup of chats/${id}`);
      await writeFile(`chats/${id}.bk`, JSON.stringify(chat));
    } catch (error) {
      console.log(`Load or parse error from chats/${id}:`, error);
      console.log(`Attempting to load backup from chats/${id}.bk`);
      chat = await readFile(`chats/${id}.bk`)
      .catch(() => {
        console.error(error, chat);
      });
      if (chat) {
        try {
          chat = JSON.parse(chat);
          console.log(`Saving backup to chats/${id}`);
          await writeFile(`chats/${id}`, JSON.stringify(chat, null, 2));
        } catch (error) {
          console.error(error);
          chat = null;
        }
      }
    }
  }
  
  if (!chat) {
    chat = createChat(id);
  }

  /* Clear out cached names from person colors and rebuild them
   * from the information in the saved chat sessions */
  for (let color in chat.persons) {
    delete chat.persons[color].name;
    chat.persons[color].status = 'Not active';
  }

  /* Reconnect session person colors to the person objects */
  chat.unselected = [];
  for (let id in chat.sessions) {
    const session = chat.sessions[id];
    if (session.name && session.color && session.color in chat.persons) {
      session.person = chat.persons[session.color];
      session.person.name = session.name;
      session.person.status = 'Active';
      session.person.live = false;
    } else {
      session.color = '';
      session.person = undefined;
    }
   
    session.live = false;
   
    /* Populate the 'unselected' list from the session table */
    if (!chat.sessions[id].color && chat.sessions[id].name) {
      chat.unselected.push(chat.sessions[id]);
    }
  }

  chats[id] = chat;
  return chat;
};

const clearPerson = (person) => {
  const color = person.color;
  for (let key in person) {
    delete person[key];
  }
  Object.assign(person, newPerson(color));
}

const setPersonName = (chat, session, name) => {
  if (session.name === name) {
    return; /* no-op */
  }
  if (session.color) {
    return `You cannot change your name while you have a color selected.`;
  }
  
  if (!name) {
    return `You can not set your name to nothing!`;
  }

  /* Check to ensure name is not already in use */
  let rejoin = false;
  for (let id in chat.sessions) {
    const tmp = chat.sessions[id];
    if (tmp === session || !tmp.name) {
      continue;
    }
    if (tmp.name.toLowerCase() === name.toLowerCase()) {
      if (!tmp.person || (Date.now() - tmp.person.lastActive) > 60000) {
        rejoin = true;
        /* Update the session object from tmp, but retain websocket
         * from active session */
        Object.assign(session, tmp, { ws: session.ws, id: session.id });
        console.log(`${info}: ${name} has been reallocated to a new session.`);
        delete chat.sessions[id];
      } else {
        return `${name} is already taken and has been active in the last minute.`;
      }
    }
  }
  
  let message;

  if (!session.name) {
    message = `A new person has entered the lobby as ${name}.`;
  } else {
    if (rejoin) {
      if (session.color) {
        message = `${name} has reconnected to the chat.`;
      } else {
        message = `${name} has rejoined the lobby.`;
      }
      session.name = name;
      if (session.ws && (chat.id in audio) 
      && session.name in audio[chat.id]) {
        part(audio[chat.id], session);
      }
    } else {
      message = `${session.name} has changed their name to ${name}.`;
      if (session.ws && chat.id in audio) {
        part(audio[chat.id], session);
      }
    }
  }

  session.name = name;
  session.live = true;
  if (session.person) {
    session.color = session.person.color;
    session.person.name = session.name;
    session.person.status = `Active`;
    session.person.lastActive = Date.now();
    session.person.name = name;
    session.person.live = true;
  }
  
  if (session.ws && session.hasAudio) {
    join(audio[chat.id], session, {
      hasVideo: session.video ? true : false,
      hasAudio: session.audio ? true : false
    });
  }
  console.log(`${info}: ${message}`);
  addChatMessage(chat, null, message);

   /* Rebuild the unselected list */
  if (!session.color) {
    console.log(`${info}: Adding ${session.name} to the unselected`);
  }
  chat.unselected = [];
  for (let id in chat.sessions) {
    if (!chat.sessions[id].color && chat.sessions[id].name) {
      chat.unselected.push(chat.sessions[id]);
    }
  }

  sendUpdateToPerson(chat, session, {
    name: session.name,
    color: session.color,
    live: session.live,
    private: session.person
  });
  sendUpdateToPersons(chat, {
    persons: getFilteredPersons(chat),
    unselected: getFilteredUnselected(chat),
    chat: chat.chat
  });
  /* Now that a name is set, send the full chat to the person */
  sendChatToPerson(chat, session);
}

const colorToWord = (color) => {
  switch (color) {
    case 'O': return 'orange';
    case 'W': return 'white';
    case 'B': return 'blue';
    case 'R': return 'red';
    default:
      return '';
  }
}

const getActiveCount = (chat) => {
  let active = 0;
  for (let color in chat.persons) {
    if (!chat.persons[color].name) {
      continue;
    }
    active++;
  }
  return active;
}

const setPersonColor = (chat, session, color) => {
  /* Selecting the same color is a NO-OP */
  if (session.color === color) {
    return;
  }

  /* Verify the person has a name set */
  if (!session.name) {
    return `You may only select a person when you have set your name.`;
  }

  if (chat.state !== 'lobby') {
    return `You may only select a person when the chat is in the lobby.`;
  }

  /* Verify selection is valid */
  if (color && !(color in chat.persons)) {
    return `An invalid person selection was attempted.`;
  }

  /* Verify selection is not already taken */
  if (color && chat.persons[color].status !== 'Not active') {
    return `${chat.persons[color].name} already has ${colorToWord(color)}`;
  }

  let active = getActiveCount(chat);
  
  if (session.person) {
    /* Deselect currently active person for this session */
    clearPerson(session.person);
    session.person = undefined;
    session.color = '';
    active--;
   
    /* If the person is not selecting a color, then return */
    if (!color) {
      addChatMessage(chat, null, 
        `${session.name} is no longer ${colorToWord(session.color)}.`);
      chat.unselected.push(session);
      chat.active = active;
      if (active === 1) {
        addChatMessage(chat, null,
          `There are no longer enough persons to start a chat.`);
      }
      sendUpdateToPerson(chat, session, {
        name: session.name,
        color: '',
        live: session.live,
        private: session.person
      });
      sendUpdateToPersons(chat, {
        active: chat.active,
        unselected: getFilteredUnselected(chat),
        persons: getFilteredPersons(chat),
        chat: chat.chat
      });
      return;
    }
  }

  /* All good -- set this person to requested selection */
  active++;
  session.color = color;
  session.live = true;
  session.person = chat.persons[color];
  session.person.name = session.name;
  session.person.status = `Active`;
  session.person.lastActive = Date.now();
  session.person.live = true;
  addChatMessage(chat, session, `${session.name} has chosen to play as ${colorToWord(color)}.`);

  const update = {
    persons: getFilteredPersons(chat),
    chat: chat.chat
  };

  /* Rebuild the unselected list */
  const unselected = [];
  for (let id in chat.sessions) {
    if (!chat.sessions[id].color && chat.sessions[id].name) {
      unselected.push(chat.sessions[id]);
    }
  }
  if (unselected.length !== chat.unselected.length) {
    chat.unselected = unselected;
    update.unselected = getFilteredUnselected(chat);
  }

  if (chat.active !== active) {
    if (chat.active < 2 && active >= 2) {
      addChatMessage(chat, null,
         `There are now enough persons to start the chat.`);
    }
    chat.active = active;
    update.active = chat.active;
  }

  sendUpdateToPerson(chat, session, {
    name: session.name,
    color: session.color,
    live: session.live,
    private: session.person,
  });
  sendUpdateToPersons(chat, update);
};

const addActivity = (chat, session, message) => {
  let date = Date.now();
  if (chat.activities.length && chat.activities[chat.activities.length - 1].date === date) {
    date++;
  }
  chat.activities.push({ color: session ? session.color : '', message, date });
  if (chat.activities.length > 30) {
    chat.activities.splice(0, chat.activities.length - 30);
  }
}

const addChatMessage = (chat, session, message, isNormalChat) => {
  let now = Date.now();
  let lastTime = 0;
  if (chat.chat.length) {
    lastTime = chat.chat[chat.chat.length - 1].date;
  }
  if (now <= lastTime) {
    now = lastTime + 1;
  }

  const entry = {
    date: now,
    message: message
  };
  if (isNormalChat) {
    entry.normalChat = true;
  }
  if (session && session.name) {
    entry.from = session.name;
  }
  if (session && session.color) {
    entry.color = session.color;
  }
  chat.chat.push(entry);
  if (chat.chat.length > 50) {
    chat.chat.splice(0, chat.chat.length - 50);
  }
};

const getColorFromName = (chat, name) => {
  for (let id in chat.sessions) {
    if (chat.sessions[id].name === name) {
      return chat.sessions[id].color;
    }
  }
  return '';
};
      
const getLastPersonName = (chat) => {
  let index = chat.personOrder.length - 1;
  for (let id in chat.sessions) {
    if (chat.sessions[id].color === chat.personOrder[index]) {
      return chat.sessions[id].name;
    }
  }
  return '';
}

const getFirstPersonName = (chat) => {
  let index = 0;
  for (let id in chat.sessions) {
    if (chat.sessions[id].color === chat.personOrder[index]) {
      return chat.sessions[id].name;
    }
  }
  return '';
}

router.put("/:id/:action/:value?", async (req, res) => {
  const { action, id } = req.params,
    value = req.params.value ? req.params.value : "";
  console.log(`PUT chats/${id}/${action}/${value}`);

  const chat = await loadChat(id);
  if (!chat) {
    const error = `Chat not found and cannot be created: ${id}`;
    return res.status(404).send(error);
  }

  let error = 'Invalid request';

  if ('private-token' in req.headers) {
    if (req.headers['private-token'] !== req.app.get('admin')) {
      error = `Invalid admin credentials.`;
    } else {
      error = adminCommands(chat, action, value, req.query);
    }
    if (!error) {
      sendChatToPersons(chat);
    } else {
      console.log(`admin-action error: ${error}`);
    }
  }

  return res.status(400).send(error);
});

const clearTimeNotice= (chat, session) => {
  if (!session.person.turnNotice) {
    /* benign state; don't alert the user */
    //return `You have not been idle.`;
  }
  session.person.turnNotice = "";
  sendUpdateToPerson(chat, session, {
    private: session.person
  });
};

const startTurnTimer = (chat, session) => {
  const timeout = 90;
  if (!session.ws) {
    console.log(`${session.id}: Aborting turn timer as ${session.name} is disconnected.`);
  } else {
    console.log(`${session.id}: (Re)setting turn timer for ${session.name} to ${timeout} seconds.`);
  }
  if (chat.turnTimer) {
    clearTimeout(chat.turnTimer);
  }
  if (!session.connected) {
    chat.turnTimer = 0;
    return;
  }
  chat.turnTimer = setTimeout(() => {
    console.log(`${session.id}: Turn timer expired for ${session.name}`);
    session.person.turnNotice = 'It is still your turn.';
    sendUpdateToPerson(chat, session, {
      private: session.person
    });
    resetTurnTimer(chat, session);
  }, timeout * 1000);
}

const resetTurnTimer = (chat, session) => {
  startTurnTimer(chat, session);
}

const stopTurnTimer = (chat) => {
  if (chat.turnTimer) {
    console.log(`${info}: Stopping turn timer.`);
    clearTimeout(chat.turnTimer);
    chat.turnTimer = 0;
  }
}

const ping = (session) => {
  if (!session.ws) {
    console.log(`[no socket] Not sending ping to ${session.name} -- connection does not exist.`);
    return;
  }

  session.ping = Date.now();
  // console.log(`Sending ping to ${session.name}`);
  session.ws.send(JSON.stringify({ type: 'ping', ping: session.ping }));
  if (session.keepAlive) {
    clearTimeout(session.keepAlive);
  }
  session.keepAlive = setTimeout(() => { ping(session); }, 2500);
}

const wsInactive = (chat, req) => {
  const session = getSession(chat, req.cookies.person);

  if (session && session.ws) {
    console.log(`Closing WebSocket to ${session.name} due to inactivity.`);
    session.ws.close();
    session.ws = undefined;
  }

  /* Prevent future pings */
  if (req.keepAlive) {
    clearTimeout(req.keepAlive);
  }
}

const setChatState = (chat, session, state) => {
  if (!state) {
    return `Invalid state.`;
  }

  if (!session.color) {
    return `You must have an active person to start the chat.`;
  }

  if (state === chat.state) {
    return;
  }

  switch (state) {
  case "chat-order":
    if (chat.state !== 'lobby') {
      return `You can only start the chat from the lobby.`;
    }
    const active = getActiveCount(chat);
    if (active < 2) {
      return `You need at least two persons to start the chat.`;
    }
    /* Delete any non-played colors from the person map; reduces all
     * code that would otherwise have to filter out persons by checking
     * the 'Not active' state of person.status */
    for (let key in chat.persons) {
      if (chat.persons[key].status !== 'Active') {
        delete chat.persons[key];
      }
    }
    addChatMessage(chat, null, `${session.name} requested to start the chat.`);
    chat.state = state;

    sendUpdateToPersons(chat, {
      state: chat.state, 
      chat: chat.chat 
    });
    break;
  }

}

const resetDisconnectCheck = (chat, req) => {  
  if (req.disconnectCheck) {
    clearTimeout(req.disconnectCheck);
  }
  //req.disconnectCheck = setTimeout(() => { wsInactive(chat, req) }, 20000);
}

const join = (peers, session, { hasVideo, hasAudio }) => {
  const ws = session.ws;

  if (!session.name) {
    console.error(`${session.id}: <- join - No name set yet. Audio not available.`);
    return;
  }

  console.log(`${session.id}: <- join - ${session.name}`);
  console.log(`${all}: -> addPeer - ${session.name}`);
  
  if (session.name in peers) {
    console.log(`${session.id}:${session.name} - Already joined to Audio.`);
    return;
  }

  for (let peer in peers) {
    /* Add this caller to all peers */
    peers[peer].ws.send(JSON.stringify({
      type: 'addPeer', 
      data: {
        peer_id: session.name, 
        should_create_offer: false,
        hasAudio, hasVideo
      }
    }));

    /* Add each other peer to the caller */
    ws.send(JSON.stringify({
      type: 'addPeer', 
      data: {
        peer_id: peer, 
        should_create_offer: true,
        hasAudio: peers[peer].hasAudio,
        hasVideo: peers[peer].hasVideo
      }
    }));
  }

  /* Add this user as a peer connected to this WebSocket */
  peers[session.name] = {
    ws,
    hasAudio,
    hasVideo
  };
};

const part = (peers, session) => {
  const ws = session.ws;

  if (!session.name) {
    console.error(`${session.id}: <- part - No name set yet. Audio not available.`);
    return;
  }

  if (!(session.name in peers)) {
    console.log(`${session.id}: <- ${session.name} - Does not exist in chat audio.`);
    return;
  }

  console.log(`${session.id}: <- ${session.name} - Audio part.`);
  console.log(`${all}: -> removePeer - ${session.name}`);

  delete peers[session.name];

  /* Remove this peer from all other peers, and remove each
   * peer from this peer */
  for (let peer in peers) {
    peers[peer].ws.send(JSON.stringify({
      type: 'removePeer',
      data: {'peer_id': session.name}
    }));
    ws.send(JSON.stringify({
      type: 'removePeer', 
      data: {'peer_id': session.name}
    }));
  }
};


const getName = (session) => {
  return session ? (session.name ? session.name : session.id) : 'Admin';
}

const saveChat = async (chat) => {
  /* Shallow copy chat, filling its sessions with a shallow copy of sessions so we can then
   * delete the person field from them */
  const reducedChat = Object.assign({}, chat, { sessions: {} }),
    reducedSessions = [];
  
  for (let id in chat.sessions) {
    const reduced = Object.assign({}, chat.sessions[id]);
    if (reduced.person) {
      delete reduced.person;
    }
    if (reduced.ws) {
      delete reduced.ws;
    }
    if (reduced.keepAlive) {
      delete reduced.keepAlive;
    }
  
    reducedchat.sessions[id] = reduced;

    /* Do not send session-id as those are secrets */
    reducedSessions.push(reduced);
  }

  delete reducedchat.turnTimer;
  delete reducedchat.unselected;

  /* Save per turn while debugging... */
  chat.step = chat.step ? chat.step : 0;
  /*
  await writeFile(`chats/${chat.id}.${chat.step++}`, JSON.stringify(reducedChat, null, 2))
  .catch((error) => {
    console.error(`${session.id} Unable to write to chats/${chat.id}`);
    console.error(error);
  });
  */
  await writeFile(`chats/${chat.id}`, JSON.stringify(reducedChat, null, 2))
  .catch((error) => {
    console.error(`${session.id} Unable to write to chats/${chat.id}`);
    console.error(error);
  });
}

const departLobby = (chat, session, color) => {
  const update = {};
  update.unselected = getFilteredUnselected(chat);

  if (session.person) {
    session.person.live = false;
    update.persons = chat.persons;
  }  

  if (session.name) {
    if (session.color) {
      addChatMessage(chat, null, `${session.name} has disconnected ` +
        `from the  chat.`);      
    } else {
      addChatMessage(chat, null, `${session.name} has left the lobby.`);
    }
    update.chat = chat.chat;
  } else {
    console.log(`${session.id}: departLobby - ${getName(session)} is ` +
      `being removed from ${chat.id}'s sessions.`);
    for (let id in chat.sessions) {
      if (chat.sessions[id] === session) {
        delete chat.sessions[id];
        break;
      }
    }
  }

  sendUpdateToPersons(chat, update);
}

const all  = `[   all  ]`;
const info = `[  info  ]`;
const todo = `[  todo  ]`;

const sendChatToPerson = (chat, session) => {
  console.log(`${session.id}: -> sendChatPerson:${getName(session)} - full chat`);
  if (!session.ws) {
    console.log(`${session.id}: -> sendChatPerson:: Currently no connection`);
    return;
  }

  let update;

  /* Only send empty name data to unnamed persons */
  if (!session.name) {
    console.log(`${session.id}: -> sendChatPerson:${getName(session)} - only sending empty name`);
    update = { name: "" };
  } else {
    update = getFilteredChatForPerson(chat, session);
  }
    
  session.ws.send(JSON.stringify({
    type: 'chat-update', 
    update: update
  }));
};

const sendChatToPersons = (chat) => {
  console.log(`${all}: -> sendChatPersons - full chat`);

  for (let key in chat.sessions) {
    sendChatToPerson(chat, chat.sessions[key]);
  }  
};

const sendUpdateToPersons = async (chat, update) => {
  /* Ensure clearing of a field actually gets sent by setting
    * undefined to 'false' 
    */
  for (let key in update) {
    if (update[key] === undefined) {
      update[key] = false;
    }
  }

  calculatePoints(chat, update);

  if (debug.update) {
    console.log(`[   all  ]: -> sendUpdateToPersons - `, update);
  } else {
    const keys = Object.getOwnPropertyNames(update);
    console.log(`[   all  ]: -> sendUpdateToPersons - ${keys.join(',')}`);
  }
  
  const message = JSON.stringify({
    type: 'chat-update', 
    update
  });
  for (let key in chat.sessions) {
    const session = chat.sessions[key];
    /* Only send person and chat data to named persons */
    if (!session.name) {
      console.log(`${session.id}: -> sendUpdateToPersons:` +
        `${getName(session)} - only sending empty name`);
      if (session.ws) {
        session.ws.send(JSON.stringify({
          type: 'chat-update',
          update: { name: "" }
        }));
      }
      continue;
    }
    if (!session.ws) {
      console.log(`${session.id}: -> sendUpdateToPersons: ` +
        `Currently no connection.`);
    } else {
      session.ws.send(message);
    }
  }
}

const sendUpdateToPerson = async (chat, session, update) => {
  /* If this person does not have a name, *ONLY* send the name, regardless
   * of what is requested */
  if (!session.name) {
    console.log(`${session.id}: -> sendUpdateToPerson:${getName(session)} - only sending empty name`);
    update = { name: "" };
  }

  /* Ensure clearing of a field actually gets sent by setting
    * undefined to 'false' 
    */
  for (let key in update) {
    if (update[key] === undefined) {
      update[key] = false;
    }
  }
  
  calculatePoints(chat, update);

  if (debug.update) {
    console.log(`${session.id}: -> sendUpdateToPerson:${getName(session)} - `, update);
  } else {
    const keys = Object.getOwnPropertyNames(update);
    console.log(`${session.id}: -> sendUpdateToPerson:${getName(session)} - ${keys.join(',')}`);
  }

  const message = JSON.stringify({
    type: 'chat-update', 
    update
  });

  if (!session.ws) {
    console.log(`${session.id}: -> sendUpdateToPerson: ` +
      `Currently no connection.`);
  } else {
    session.ws.send(message);
  }
}

const getFilteredUnselected = (chat) => {
  if (!chat.unselected) {
    return [];
  }
  return chat.unselected
    .filter(session => session.live)
    .map(session => session.name);
}

const parseChatCommands = (chat, message) => {
  /* Chat messages can set chat flags and fields */
  const parts = message.match(/^set +([^ ]*) +(.*)$/i);
  if (!parts || parts.length !== 3) {
    return;
  }
  switch (parts[1].toLowerCase()) {
  case 'chat':
    if (parts[2].trim().match(/^beginner('?s)?( +layout)?/i)) {
      setBeginnerChat(chat);
      addChatMessage(chat, session, `${session.name} set chat board to the Beginner's Layout.`);
      break;
    }
    const signature = parts[2].match(/^([0-9a-f]{12})-([0-9a-f]{38})-([0-9a-f]{38})/i);
    if (signature) {
      if (setChatFromSignature(chat, signature[1], signature[2], signature[3])) {
        chat.signature = parts[2];
        addChatMessage(chat, session, `${session.name} set chat board to ${parts[2]}.`);
      } else {
        addChatMessage(chat, session, `${session.name} requested an invalid chat board.`);   
      }
    }
    break;
  }
};

const sendError = (session, error) => {
  session.ws.send(JSON.stringify({ type: 'error', error }));
}

const sendWarning = (session, warning) => {
  session.ws.send(JSON.stringify({ type: 'warning', warning }));
}

const getFilteredPersons = (chat) => {
  const filtered = {};
  for (let color in chat.persons) {
    const person = Object.assign({}, chat.persons[color]);
    filtered[color] = person;
    if (person.status === 'Not active') {
      if (chat.state !== 'lobby') {
        delete filtered[color];
      }
      continue;
    }
    person.resources = 0;
    [ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(resource => {
      person.resources += person[resource];
      delete person[resource];
    });
    delete person.development;
  }
  return filtered;
};

const calculatePoints = (chat, update) => {
  if (chat.state === 'winner') {
    return;
  }
  /* Calculate points and determine if there is a winner */
  for (let key in chat.persons) {
    const person = chat.persons[key];
    if (person.status === 'Not active') {
      continue;
    }
    const currentPoints = person.points;

    person.points = 0;
    if (key === chat.longestRoad) {
      person.points += 2;
    }
    if (key === chat.largestArmy) {
      person.points += 2;
    }
    if (key === chat.mostPorts) {
      person.points += 2;
    }
    if (key === chat.mostDeveloped) {
      person.points += 2;
    }
    person.points += MAX_SETTLEMENTS - person.settlements;
    person.points += 2 * (MAX_CITIES - person.cities);
  
    person.unplayed = 0;
    person.potential = 0;
    person.development.forEach(card => {
      if (card.type === 'vp') {
        if (card.played) {
          person.points++;
        } else {
          person.potential++;
        }
      }
      if (!card.played) {
        person.unplayed++;
      }
    });

    if (person.points === currentPoints) {
      continue;
    }

    if (person.points < getVictoryPointRule(chat)) {
      update.persons = getFilteredPersons(chat);
      continue;
    }

    /* This person has enough points! Check if they are the current
     * person and if so, declare victory! */
    console.log(`${info}: Whoa! ${person.name} has ${person.points}!`);
    for (let key in chat.sessions) {
      if (chat.sessions[key].color !== person.color
      || chat.sessions[key].status === 'Not active') {
        continue;
      }
      const message =  `Wahoo! ${person.name} has ${person.points} ` +
        `points on their turn and has won!`;
      addChatMessage(chat, null, message)
      console.log(`${info}: ${message}`);
      update.winner = Object.assign({}, person, { 
        state: 'winner',
        stolen: chat.stolen,
        chat: chat.chat,
        turns: chat.turns,
        persons: chat.persons,
        elapsedTime: Date.now() - chat.startTime
      });
      chat.winner = update.winner;
      chat.state = 'winner';
      chat.waiting = [];
      stopTurnTimer(chat);
      sendUpdateToPersons(chat, {
        state: chat.state,
        winner: chat.winner,
        persons: chat.persons /* unfiltered */
      });
    }
  }

  /* If the chat isn't in a win state, do not share development card information 
    * with other persons */
  if (chat.state !== 'winner') {
    for (let key in chat.persons) {
      const person = chat.persons[key];
      if (person.status === 'Not active') {
        continue;
      }
      delete person.potential;      
    }
  }
}

const clearChat = (chat, session) => {
  resetChat(chat);
  addChatMessage(chat, null,
    `The chat has been reset. You can play again with this board, or ` +
    `click 'New board' to mix things up a bit.`);
  sendChatToPersons(chat);
};

const gotoLobby = (chat, session) => {
  if (!chat.waiting) {
    chat.waiting = [];
  }
  const already = chat.waiting.indexOf(session.name) !== -1;

  const waitingFor = [];
  for (let key in chat.sessions) {
    if (chat.sessions[key] === session) {
      continue;
    }

    if (chat.sessions[key].person && chat.waiting.indexOf(chat.sessions[key].name) == -1) {
      waitingFor.push(chat.sessions[key].name);
    }
  }

  if (!already) {
    chat.waiting.push(session.name);
    addChatMessage(chat, null, `${session.name} has gone to the lobby.`);
  } else if (waitingFor.length !== 0) {
    return `You are already waiting in the lobby. ` +
      `${waitingFor.join(',')} still needs to go to the lobby.`;
  }

  if (waitingFor.length === 0) {
    resetChat(chat);
    addChatMessage(chat, null, `All persons are back to the lobby.`);
    addChatMessage(chat, null,
      `The chat has been reset. You can play again with this board, or `+
      `click 'New board' to mix things up a bit.`);
    sendChatToPersons(chat);
    return;
  }

  addChatMessage(chat, null, `Waiting for ${waitingFor.join(',')} to go to lobby.`);
  sendUpdateToPersons(chat, {
    chat: chat.chat
  });
}

router.ws("/ws/:id", async (ws, req) => {
  if (!req.cookies || !req.cookies.person) {
    ws.send(JSON.stringify({ type: 'error', error: `Unable to find session cookie` }));
    return;
  }

  const { id } = req.params;
  const chatId = id;

  const short = `[${req.cookies.person.substring(0, 8)}]`;
  ws.id = short;

  console.log(`${short}: Chat ${chatId} - New connection from client.`);
  if (!(id in audio)) {
    audio[id] = {}; /* List of peer sockets using session.name as index. */
    console.log(`${short}: Chat ${id} - New Chat Audio`);
  } else {
    console.log(`${short}: Chat ${id} - Already has Audio`);
  }

  /* Setup WebSocket event handlers prior to performing any async calls or
   * we may miss the first messages from clients */
  ws.on('error', async (event) => {
    console.error(`WebSocket error: `, event.message);
    const chat = await loadChat(chatId);
    if (!chat) {
      return;
    }
    const session = getSession(chat, req.cookies.person);
    session.live = false;
    if (session.ws) {
      session.ws.close();
      session.ws = undefined;
    }

    departLobby(chat, session);
  });

  ws.on('close', async (event) => {
    console.log(`${short} - closed connection`);

    const chat = await loadChat(chatId);
    if (!chat) {
      return;
    }
    const session = getSession(chat, req.cookies.person);
    if (session.person) {
      session.person.live = false;
    }  
    session.live = false;
    if (session.ws) {
        /* Cleanup any voice channels */
      if (id in audio) {
        part(audio[id], session);
      }
      session.ws.close();
      session.ws = undefined;
      console.log(`${short}:WebSocket closed for ${getName(session)}`);
    }

    departLobby(chat, session);

    /* Check for a chat in the Winner state with no more connections
     * and remove it */
    if (chat.state === 'lobby') {
      let dead = true;
      for (let id in chat.sessions) {
        if (chat.sessions[id].live && chat.sessions[id].name) {
          dead = false;
        }
      }
      if (dead) {
        console.log(`${session.id}: No more persons in ${chat.id}. ` +
          `Removing.`);
        addChatMessage(chat, null, `No more active persons in chat. ` +
          `It is being removed from the server.`);
        sendUpdateToPersons(chat, {
          chat: chat.chat
        });
        for (let id in chat.sessions) {
          if (chat.sessions[id].ws) {
            chat.sessions[id].ws.close();
            delete chat.sessions[id];
          }
        }
        delete audio[id];
        delete chats[id];
        try {
          fs.unlinkSync(`chats/${id}`);
        } catch (error) {
          console.error(`${session.id}: Unable to remove chats/${id}`);
        }
      }
    } 
  });

  ws.on('message', async (message) => {
    let data;
    try {
      data = JSON.parse(message);
    } catch (error) {
      console.error(`${all}: parse error`, message);
      return;
    }
    const chat = await loadChat(chatId);
    const session = getSession(chat, req.cookies.person);
    if (!session.ws) {
      session.ws = ws;
    }
    if (session.person) {
      session.person.live = true;
    }
    session.live = true;
    session.lastActive = Date.now();
   
    let error, warning, update, processed = true;
 
    switch (data.type) {
    case 'join':
      join(audio[id], session, data.config);
      break;

    case 'part':
      part(audio[id], session);
      break;

    case 'relayICECandidate': {
      if (!(id in audio)) {
        console.error(`${session.id}:${id} <- relayICECandidate - Does not have Audio`);
        return;
      }

      const { peer_id, candidate } = data.config;
      if (debug.audio) console.log(`${short}:${id} <- relayICECandidate ${getName(session)} to ${peer_id}`,
        candidate);
  
      message = JSON.stringify({
        type: 'iceCandidate', 
        data: {'peer_id': getName(session), 'candidate': candidate }
      });

      if (peer_id in audio[id]) {
        audio[id][peer_id].ws.send(message);
      }
    } break;

    case 'relaySessionDescription': {
      if (!(id in audio)) {
        console.error(`${id} - relaySessionDescription - Does not have Audio`);
        return;
      }
      const { peer_id, session_description } = data.config;
      if (debug.audio) console.log(`${short}:${id} - relaySessionDescription ${getName(session)} to ${peer_id}`,
        session_description);
      message = JSON.stringify({
        type: 'sessionDescription', 
        data: {'peer_id': getName(session), 'session_description': session_description }
      });
      if (peer_id in audio[id]) {
        audio[id][peer_id].ws.send(message);
      }
    } break;

    case 'pong':
      resetDisconnectCheck(chat, req);
      break;

    case 'chat-update':
      console.log(`${short}: <- chat-update ${getName(session)} - full chat update.`);
      sendChatToPerson(chat, session);
      break;

    case 'person-name':
      console.log(`${short}: <- person-name:${getName(session)} - setPersonName - ${data.name}`)
      error = setPersonName(chat, session, data.name);
      if (error) {
        sendError(session, error);
      }else {
        saveChat(chat);
      }
      break;

    case 'set':
      console.log(`${short}: <- set:${getName(session)} ${data.field} = ${data.value}`);
      switch (data.field) {
      case 'state':
        warning = setChatState(chat, session, data.value);
        if (warning) {
          sendWarning(session, warning);
        } else {
          saveChat(chat);
        }
        break;

      case 'color':
        warning = setPersonColor(chat, session, data.value);
        if (warning) {
          sendWarning(session, warning);
        } else {
          saveChat(chat);
        }
        break;
      default:
        console.warn(`WARNING: Requested SET unsupported field: ${data.field}`);
        break;
      }
      break;

    case 'get':
      console.log(`${short}: <- get:${getName(session)} ${data.fields.join(',')}`);
      update = {};
      data.fields.forEach((field) => {
        switch (field) {
        case 'person': 
          sendWarning(session, `'person' is not a valid item. use 'private' instead`); 
          update.person = undefined;
          break;
        case 'id':
        case 'chat':
        case 'activities':
          update[field] = chat[field];
          break;
        case 'name':
          update.name = session.name;
          break;
        case 'unselected':
          update.unselected = getFilteredUnselected(chat);
          break;
        case 'private':
          update.private = session.person;
          break;
        case 'persons':
          update.persons = getFilteredPersons(chat);
          break;
        case 'color':
          console.log(`${session.id}: -> Returning color as ${session.color} for ${getName(session)}`);
          update.color = session.color;
          break;
        case 'timestamp':
          update.timestamp = Date.now();
          break;
        default:
          if (field in chat) {
            console.warn(`${short}: WARNING: Requested GET not-privatized/sanitized field: ${field}`);
            update[field] = chat[field];
          } else {
            if (field in session) {
              console.warn(`${short}: WARNING: Requested GET not-sanitized session field: ${field}`);
              update[field] = session[field];
            } else {
              console.warn(`${short}: WARNING: Requested GET unsupported field: ${field}`);
            }
          }
          break;
        }
      });
      sendUpdateToPerson(chat, session, update);
      break;
      
    case 'chat':
      console.log(`${short}:${id} - ${data.type} - ${data.message}`)
      addChatMessage(chat, session, `${session.name}: ${data.message}`, true);
      parseChatCommands(chat, data.message);
      sendUpdateToPersons(chat, { chat: chat.chat });
      saveChat(chat);
      break;

    case 'media-status':
      console.log(`${short}: <- media-status - `, data.audio, data.video);
      session.video = data.video;
      session.audio = data.audio;
      break;

    default:
      processed = false;
      break;
    }

    if (processed) {
      /* saveChat(chat); -- do not save here; only save on changes */
      return;
    }
 
    /* The rest of the actions and commands require an active chat
     * participant */

    if (!session.person) {
      error = `Person must have an active color.`;
      sendError(session, error);
      return;
    }
  
    processed = true;
    const priorSession = session;
    
    switch (data.type) {
    case 'clear-chat':
      console.log(`${short}: <- clear-chat:${getName(session)}`);
      warning = clearChat(chat, session);
      if (warning) {
        sendWarning(session, warning);
      }
      break;
    case 'goto-lobby':
      console.log(`${short}: <- goto-lobby:${getName(session)}`);
      warning = gotoLobby(chat, session);
      if (warning) {
        sendWarning(session, warning);
      }
      break;
    default:
      console.warn(`Unsupported request: ${data.type}`);
      processed = false;
      break;
    }

    /* If action was taken, persist the chat */
    if (processed) {
      saveChat(chat);
    }

    /* If the current person took an action, reset the session timer */
    if (processed && session.color === chat.turn.color && chat.state !== 'winner') {
      resetTurnTimer(chat, session);
    }  
  });

 
  /* This will result in the node tick moving forward; if we haven't already
   * setup the event handlers, a 'message' could come through prior to this
   * completing */
  const chat = await loadChat(chatId);
  if (!chat) {
    console.error(`Unable to load/create new chat for WS request.`);
    return;
  }

  const session = getSession(chat, req.cookies.person);
  session.ws = ws;
  if (session.person) {
    session.person.live = true;
  }
  session.live = true;
  session.lastActive = Date.now();
  if (session.name) {
    sendUpdateToPersons(chat, {
      persons: getFilteredPersons(chat),
      unselected: getFilteredUnselected(chat)
    });
  }
  
  /* If the current turn person just rejoined, set their turn timer */
  if (chat.turn && chat.turn.color === session.color && chat.state !== 'winner') {
    resetTurnTimer(chat, session);
  }

  if (session.name) {
    if (session.color) {
      addChatMessage(chat, null, `${session.name} has reconnected to the chat.`);
    } else {
      addChatMessage(chat, null, `${session.name} has rejoined the lobby.`);
    }
    sendUpdateToPersons(chat, { chat: chat.chat });
  }

  resetDisconnectCheck(chat, req);
  console.log(`${short}: Chat ${id} - WebSocket connect from ${getName(session)}`);

  /* Send initial ping to initiate communication with client */
  if (!session.keepAlive) {
    console.log(`${short}: Sending initial ping`);
    ping(session);
  } else {
    clearTimeout(session.keepAlive);
    session.keepAlive = setTimeout(() => { ping(session); }, 2500);
  }
});

const getFilteredChatForPerson = (chat, session) => {
  /* Shallow copy chat, filling its sessions with a shallow copy of 
   * sessions so we can then delete the person field from them */
  const reducedChat = Object.assign({}, chat, { sessions: {} }),
    reducedSessions = [];
  
  for (let id in chat.sessions) {
    const reduced = Object.assign({}, chat.sessions[id]);
    if (reduced.person) {
      delete reduced.person;
    }
    if (reduced.ws) {
      delete reduced.ws;
    }
    if (reduced.keepAlive) {
      delete reduced.keepAlive;
    }
    reducedchat.sessions[id] = reduced;

    /* Do not send session-id as those are secrets */
    reducedSessions.push(reduced);
  }

  const person = session.person ? session.person : undefined;

  /* Strip out data that should not be shared with persons */
  delete reducedchat.developmentCards;

  /* Delete the chat timer */
  delete reducedchat.turnTimer;

  reducedchat.unselected = getFilteredUnselected(chat);

  return Object.assign(reducedChat, {
    live: true,
    timestamp: Date.now(),
    status: session.error ? session.error : "success",
    name: session.name,
    color: session.color,
    order: (session.color in chat.persons) ? chat.persons[session.color].order : 0,
    private: person,
    sessions: reducedSessions,
    layout: layout,
    persons: getFilteredPersons(chat),
  });
}

const resetChat = (chat) => {
  Object.assign(chat, {
    startTime: Date.now(),
    state: 'lobby',
    chat: [],
    activities: [],
    persons: chat.persons,
    active: 0
  });

  stopTurnTimer(chat);

  /* Populate the chat corner and road placement data as cleared */  
  for (let i = 0; i < layout.corners.length; i++) {
    chat.placements.corners[i] = {
      color: undefined,
      type: undefined
    };
  }

  /* Reset all person data, and add in any missing colors */
  [ 'R', 'B', 'W', 'O' ].forEach(color => {
    if (color in chat.persons) {
      clearPerson(chat.persons[color]);
    } else {
      chat.persons[color] = newPerson(color);
    }
  });

  /* Ensure sessions are connected to person objects */
  for (let key in chat.sessions) {
    const session = chat.sessions[key];
    if (session.color) {
      chat.active++;
      session.person = chat.persons[session.color];
      session.person.status = 'Active';
      session.person.lastActive = Date.now();
      session.person.live = session.live;
      session.person.name = session.name;
      session.person.color = session.color;
    }
  }
}

const createChat = (id) => {
  /* Look for a new chat with random words that does not already exist */
  while (!id) {
    id = randomWords(4).join('-');
    try {
      /* If file can be read, it already exists so look for a new name */
      accessSync(`chats/${id}`, fs.F_OK);
      id = '';
    } catch (error) {
      break;
    }
  }
  console.log(`${info}: creating ${id}`);

  const chat = {
    id: id,
    persons: {
      O: newPerson('O'),
      R: newPerson('R'),
      B: newPerson('B'),
      W: newPerson('W')
    },
    sessions: {},
    unselected: [],
    active: 0,
  };

  addChatMessage(chat, null, `New chat created: ${chat.id}`);

  chats[chat.id] = chat;
  audio[chat.id] = {};
  return chat;
};

/* Simple NO-OP to set session cookie so person-id can use it as the 
 * index */
router.get("/", (req, res/*, next*/) => {
  let personId;
  if (!req.cookies.person) {
    personId = crypto.randomBytes(16).toString('hex');
    res.cookie('person', personId);
  } else {
    personId = req.cookies.person;
  }

  console.log(`[${personId.substring(0, 8)}]: Browser hand-shake achieved.`);

  return res.status(200).send({ person: personId });
});

router.post("/:id?", async (req, res/*, next*/) => {
  const { id } = req.params;

  let personId;
  if (!req.cookies.person) {
    personId = crypto.randomBytes(16).toString('hex');
    res.cookie('person', personId);
  } else {
    personId = req.cookies.person;
  }

  if (id) {
    console.log(`[${personId.substring(0,8)}]: Attempting load of ${id}`);
  } else {
    console.log(`[${personId.substring(0,8)}]: Creating new chat.`);
  }
  const chat = await loadChat(id); /* will create chat if it doesn't exist */
  console.log(`[${personId.substring(0,8)}]: ${chat.id} loaded.`);

  return res.status(200).send({ id: chat.id });
});


module.exports = router;
