import { type Game, type Session, type Player, type PlayerColor, RESOURCE_TYPES } from "./types";
import { newPlayer } from "./playerFactory";
import { debug, info, MAX_CITIES, MAX_SETTLEMENTS, SEND_THROTTLE_MS } from "./constants";
import { shuffleBoard } from "./gameFactory";
import { getVictoryPointRule } from "./rules";

export const addActivity = (game: Game, session: Session | null, message: string): void => {
  let date = Date.now();
  if (!game.activities) game.activities = [];
  if (game.activities.length && game.activities[game.activities.length - 1].date === date) {
    date++;
  }
  const actColor = session && session.color && session.color !== "unassigned" ? session.color : "";
  game.activities.push({ color: actColor, message, date });
  if (game.activities.length > 30) {
    game.activities.splice(0, game.activities.length - 30);
  }
};

export const addChatMessage = (game: Game, session: Session | null, message: string, isNormalChat?: boolean) => {
  let now = Date.now();
  let lastTime = 0;
  if (!game.chat) game.chat = [];
  if (game.chat.length) {
    lastTime = game.chat[game.chat.length - 1].date;
  }
  if (now <= lastTime) {
    now = lastTime + 1;
  }

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

export const getColorFromName = (game: Game, name: string): string => {
  for (let id in game.sessions) {
    const s = game.sessions[id];
    if (s && s.name === name) {
      return s.color && s.color !== "unassigned" ? s.color : "";
    }
  }
  return "";
};

export const getLastPlayerName = (game: Game): string => {
  const index = (game.playerOrder || []).length - 1;
  const color = (game.playerOrder || [])[index];
  if (!color) return "";
  for (let id in game.sessions) {
    const s = game.sessions[id];
    if (s && s.color === color) {
      return s.name || "";
    }
  }
  return "";
};

export const getFirstPlayerName = (game: Game): string => {
  const color = (game.playerOrder || [])[0];
  if (!color) return "";
  for (let id in game.sessions) {
    const s = game.sessions[id];
    if (s && s.color === color) {
      return s.name || "";
    }
  }
  return "";
};

export const getNextPlayerSession = (game: Game, name: string): Session | undefined => {
  let color: PlayerColor | undefined;
  for (let id in game.sessions) {
    const s = game.sessions[id];
    if (s && s.name === name) {
      color = s.color;
      break;
    }
  }
  if (!color) return undefined;

  const order = game.playerOrder || [];
  let index = order.indexOf(color);
  if (index === -1) return undefined;
  index = (index + 1) % order.length;
  const nextColor = order[index];
  for (let id in game.sessions) {
    const s = game.sessions[id];
    if (s && s.color === nextColor) {
      return s;
    }
  }
  console.error(`getNextPlayerSession -- no player found!`);
  console.log(game.players);
  return undefined;
};

export const getPrevPlayerSession = (game: Game, name: string): Session | undefined => {
  let color: PlayerColor | undefined;
  for (let id in game.sessions) {
    const s = game.sessions[id];
    if (s && s.name === name) {
      color = s.color;
      break;
    }
  }
  if (!color) return undefined;
  const order = game.playerOrder || [];
  let index = order.indexOf(color);
  if (index === -1) return undefined;
  index = (index - 1 + order.length) % order.length;
  const prevColor = order[index];
  for (let id in game.sessions) {
    const s = game.sessions[id];
    if (s && s.color === prevColor) {
      return s;
    }
  }
  console.error(`getPrevPlayerSession -- no player found!`);
  console.log(game.players);
  return undefined;
};

export const clearPlayer = (player: Player) => {
  // Use shared factory to ensure a single source of defaults
  Object.assign(player, newPlayer(player.color));
};

export const canGiveBuilding = (game: Game): string | undefined => {
  if (!game.turn.roll) {
    return `Admin cannot give a building until the dice have been rolled.`;
  }
  if (game.turn.actions && game.turn.actions.length !== 0) {
    return `Admin cannot give a building while other actions in play: ${game.turn.actions.join(", ")}.`;
  }
  return undefined;
};

export const setForRoadPlacement = (game: Game, limits: any): void => {
  game.turn.actions = ["place-road"];
  game.turn.limits = { roads: limits };
};

export const setForCityPlacement = (game: Game, limits: any): void => {
  game.turn.actions = ["place-city"];
  game.turn.limits = { corners: limits };
};

export const setForSettlementPlacement = (game: Game, limits: number[]): void => {
  game.turn.actions = ["place-settlement"];
  game.turn.limits = { corners: limits };
};

// Adjust a player's resource counts by a deltas map. Deltas may be negative.
export const adjustResources = (player: Player, deltas: Partial<Record<string, number>>): void => {
  if (!player) return;
  let total = player.resources || 0;
  const keys = Object.keys(deltas);
  keys.forEach((type) => {
    const v = deltas[type] || 0;
    // update named resource slot if present
    try {
      switch (type) {
        case "wood":
        case "brick":
        case "sheep":
        case "wheat":
        case "stone":
          const current = player[type] || 0;
          player[type] = current + v;
          total += v;
          break;
      }
    } catch (e) {
      // ignore unexpected keys
    }
  });
  player.resources = total;
};

export const startTurnTimer = (game: Game, session: Session) => {
  const timeout = 90;
  if (!session.ws) {
    console.log(`${session.short}: Aborting turn timer as ${session.name} is disconnected.`);
  } else {
    console.log(`${session.short}: (Re)setting turn timer for ${session.name} to ${timeout} seconds.`);
  }
  if (game.turnTimer) {
    clearTimeout(game.turnTimer);
  }
  if (!session.connected) {
    game.turnTimer = 0;
    return;
  }
  game.turnTimer = setTimeout(() => {
    console.log(`${session.short}: Turn timer expired for ${session.name}`);
    if (session.player) {
      session.player.turnNotice = "It is still your turn.";
    }
    sendUpdateToPlayer(game, session, {
      private: session.player,
    });
    resetTurnTimer(game, session);
  }, timeout * 1000);
};

const calculatePoints = (game: any, update: any): void => {
  if (game.state === "winner") {
    return;
  }
  /* Calculate points and determine if there is a winner */
  for (let key in game.players) {
    const player = game.players[key];
    if (player.status === "Not active") {
      continue;
    }
    const currentPoints = player.points;

    player.points = 0;
    if (key === game.longestRoad) {
      player.points += 2;
    }
    if (key === game.largestArmy) {
      player.points += 2;
    }
    if (key === game.mostPorts) {
      player.points += 2;
    }
    if (key === game.mostDeveloped) {
      player.points += 2;
    }
    player.points += MAX_SETTLEMENTS - player.settlements;
    player.points += 2 * (MAX_CITIES - player.cities);

    player.unplayed = 0;
    player.potential = 0;
    player.development.forEach((card: any) => {
      if (card.type === "vp") {
        if (card.played) {
          player.points++;
        } else {
          player.potential++;
        }
      }
      if (!card.played) {
        player.unplayed++;
      }
    });

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

    if (player.points < getVictoryPointRule(game)) {
      update.players = getFilteredPlayers(game);
      continue;
    }

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

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

export const resetTurnTimer = (game: Game, session: Session): void => {
  startTurnTimer(game, session);
};

export const stopTurnTimer = (game: Game): void => {
  if (game.turnTimer) {
    console.log(`${info}: Stopping turn timer.`);
    try {
      clearTimeout(game.turnTimer);
    } catch (e) {
      /* ignore if not a real timeout */
    }
    game.turnTimer = 0;
  }
  return undefined;
};

export const sendUpdateToPlayers = async (game: any, update: any): Promise<void> => {
  /* 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(game, update);

  if (debug.update) {
    console.log(`[   all  ]: -> sendUpdateToPlayers - `, update);
  } else {
    const keys = Object.getOwnPropertyNames(update);
    console.log(`[   all  ]: -> sendUpdateToPlayers - ${keys.join(",")}`);
  }

  const message = JSON.stringify({
    type: "game-update",
    update,
  });
  for (let key in game.sessions) {
    const session = game.sessions[key];
    /* Only send player and game data to named players */
    if (!session.name) {
      console.log(`${session.short}: -> sendUpdateToPlayers:` + `${getName(session)} - only sending empty name`);
      if (session.ws) {
        session.ws.send(
          JSON.stringify({
            type: "game-update",
            update: { name: "" },
          })
        );
      }
      continue;
    }
    if (!session.ws) {
      console.log(`${session.short}: -> sendUpdateToPlayers: ` + `Currently no connection.`);
    } else {
      queueSend(session, message);
    }
  }
};

export const sendUpdateToPlayer = async (game: any, session: any, update: any): Promise<void> => {
  /* If this player does not have a name, *ONLY* send the name, regardless
   * of what is requested */
  if (!session.name) {
    console.log(`${session.short}: -> sendUpdateToPlayer:${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(game, update);

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

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

  if (!session.ws) {
    console.log(`${session.short}: -> sendUpdateToPlayer: ` + `Currently no connection.`);
  } else {
    queueSend(session, message);
  }
};

export const queueSend = (session: any, message: any): void => {
  if (!session || !session.ws) return;
  try {
    // Ensure we compare a stable serialization: if message is JSON text,
    // parse it and re-serialize with sorted keys so semantically-equal
    // objects compare equal even when property order differs.
    const stableStringify = (msg: any): string => {
      try {
        const obj = typeof msg === "string" ? JSON.parse(msg) : msg;
        const ordered = (v: any): any => {
          if (v === null || typeof v !== "object") return v;
          if (Array.isArray(v)) return v.map(ordered);
          const keys = Object.keys(v).sort();
          const out: any = {};
          for (const k of keys) out[k] = ordered(v[k]);
          return out;
        };
        return JSON.stringify(ordered(obj));
      } catch (e) {
        // If parsing fails, fall back to original string representation
        return typeof msg === "string" ? msg : JSON.stringify(msg);
      }
    };
    const stableMessage = stableStringify(message);
    const now = Date.now();
    if (!session._lastSent) session._lastSent = 0;
    const elapsed = now - session._lastSent;
    // If the exact same message (in stable form) was sent last time and
    // nothing is pending, skip sending to avoid pointless duplicate
    // traffic.
    if (!session._pendingTimeout && session._lastMessage === stableMessage) {
      return;
    }

    // If we haven't sent recently and there's no pending timer, send now
    if (elapsed >= SEND_THROTTLE_MS && !session._pendingTimeout) {
      try {
        session.ws.send(typeof message === "string" ? message : JSON.stringify(message));
        session._lastSent = Date.now();
        session._lastMessage = stableMessage;
      } catch (e) {
        console.warn(`${session.id}: queueSend immediate send failed:`, e);
      }
      return;
    }

    // Otherwise, store latest message and schedule a send
    // If the pending message would equal the last-sent message, don't bother
    // storing/scheduling it.
    if (session._lastMessage === stableMessage) {
      return;
    }
    session._pendingMessage = typeof message === "string" ? message : JSON.stringify(message);
    if (session._pendingTimeout) {
      // already scheduled; newest message will be sent when timer fires
      return;
    }
    const delay = Math.max(1, SEND_THROTTLE_MS - elapsed);
    session._pendingTimeout = setTimeout(() => {
      try {
        if (session.ws && session._pendingMessage) {
          session.ws.send(session._pendingMessage);
          session._lastSent = Date.now();
          // compute stable form of what we actually sent
          try {
            session._lastMessage = stableStringify(session._pendingMessage);
          } catch (e) {
            session._lastMessage = session._pendingMessage;
          }
        }
      } catch (e) {
        console.warn(`${session.id}: queueSend delayed send failed:`, e);
      }
      // clear pending fields
      session._pendingMessage = undefined;
      clearTimeout(session._pendingTimeout);
      session._pendingTimeout = undefined;
    }, delay);
  } catch (e) {
    console.warn(`${session.id}: queueSend exception:`, e);
  }
};

export const shuffle = (game: Game, session: Session): string | undefined => {
  if (game.state !== "lobby") {
    return `Game no longer in lobby (${game.state}). Can not shuffle board.`;
  }
  if (game.turns > 0) {
    return `Game already in progress (${game.turns} so far!) and cannot be shuffled.`;
  }
  shuffleBoard(game);
  console.log(`${session.short}: Shuffled to new signature: ${game.signature}`);

  sendUpdateToPlayers(game, {
    pipOrder: game.pipOrder,
    tileOrder: game.tileOrder,
    borderOrder: game.borderOrder,
    robber: game.robber,
    robberName: game.robberName,
    signature: game.signature,
    animationSeeds: game.animationSeeds,
  });
  return undefined;
};

export const getName = (session: Session): string => {
  return session ? (session.name ? session.name : session.id) : "Admin";
};

export const getFilteredPlayers = (game: Game): Record<string, Player> => {
  const filtered: Record<string, Player> = {};
  for (let color in game.players) {
    const player = Object.assign({}, game.players[color]);
    filtered[color] = player;
    if (player.status === "Not active") {
      if (game.state !== "lobby") {
        delete filtered[color];
      }
      continue;
    }
    player.resources = 0;
    RESOURCE_TYPES.forEach((resource) => {
      switch (resource) {
        case "wood":
        case "brick":
        case "sheep":
        case "wheat":
        case "stone":
          player.resources += player[resource];
          player[resource] = 0;
          break;
      }
    });
    player.development = [];
  }
  return filtered;
};
