import { ElysiaWS } from 'elysia/dist/ws';
import { Html } from '@elysiajs/html';
import { GomokuGame as GomokuGame, PlayerColor } from './game/game-instance';
import { renderGameBoardHtml } from './view/board-renderer';
import {
  Message,
  MakeMoveMessage,
  ResignationMessage,
  TakebackMessage,
  DrawMessage,
  RematchMessage,
  RedirectToGameMessage,
  UpdateDisplayNameMessage,
} from './messages';
import { v4 as uuidv4 } from 'uuid';

type WS = ElysiaWS<{ query: { playerId: string; gameId: string } }>;

class PlayerConnection {
  id: string;
  name: string;
  ws: WS;

  constructor(id: string, name: string, ws: WS) {
    this.id = id;
    this.name = name;
    this.ws = ws;
  }

  public sendMessage(severity: 'info' | 'error', message: string) {
    console.log(`Sending message ${message} to player ${this.id}`);
    // TODO
  }
}

class GameServer {
  id: string;
  gomoku: GomokuGame;
  connections: Map<string, PlayerConnection>;
  blackPlayerId?: string;
  whitePlayerId?: string;
  takebackRequesterId: string | null = null;
  drawRequesterId: string | null = null;
  rematchRequesterId: string | null = null;

  constructor(
    id: string,
    private webSocketHandler: WebSocketHandler,
  ) {
    this.id = id;
    this.gomoku = new GomokuGame();
    this.connections = new Map();
  }

  public handleConnection(ws: WS) {
    const { playerId } = ws.data.query;
    if (this.connections.has(playerId)) {
      const existingConn = this.connections.get(playerId)!;
      existingConn.ws = ws; // Update with new WebSocket
      console.log(
        `Updated connection for player ${playerId} in game ${this.id}, replacing old WS.`,
      );
    } else {
      const playerName = this.webSocketHandler.getPlayerName(playerId); // Retrieve name or use ID
      const conn = new PlayerConnection(playerId, playerName, ws);
      this.connections.set(playerId, conn);
      console.log(
        `Created new connection with player ${conn.id} in game ${this.id}`,
      );

      if (!this.blackPlayerId) {
        this.blackPlayerId = conn.id;
      } else if (!this.whitePlayerId) {
        this.whitePlayerId = conn.id;
      }
    }
    if (this.whitePlayerId && this.blackPlayerId) {
      this.gomoku.status = 'playing';
    }

    this.broadcastBoard();
    this.broadcastButtons();
    this.broadcastTitle();
  }

  public handleDisconnect(ws: WS) {
    const { playerId } = ws.data.query;
    this.connections.delete(playerId);
    this.broadcastTitle();
  }

  public broadcastBoard() {
    this.connections.forEach((conn: PlayerConnection) =>
      this.broadcastBoardToPlayer(conn),
    );
  }

  public broadcastTitle() {
    this.connections.forEach((conn: PlayerConnection) =>
      this.broadcastTitleToPlayer(conn),
    );
  }

  public broadcastButtons() {
    this.connections.forEach((conn: PlayerConnection) =>
      this.broadcastButtonsToPlayer(conn),
    );
  }

  public broadcastBoardToPlayer(conn: PlayerConnection) {
    const isToPlay =
      this.gomoku.currentPlayerColor == this.getPlayerColor(conn);
    const updatedBoardHtml = renderGameBoardHtml(this.gomoku, isToPlay);
    conn.ws.send(updatedBoardHtml);
    console.log(`Sent board for game ${this.id} to player ${conn.id}`);
  }

  public broadcastTitleToPlayer(conn: PlayerConnection) {
    let message = '';
    switch (this.gomoku.status) {
      case 'waiting': {
        message = 'Waiting for players...';
        break;
      }
      case 'playing': {
        const blackTag = this.playerTag(this.blackPlayerId!, 'black');
        const whiteTag = this.playerTag(this.whitePlayerId!, 'white');
        message = `${blackTag} vs ${whiteTag}`;
        break;
      }
      case 'finished': {
        switch (this.gomoku.winnerColor) {
          case 'draw': {
            message = 'Game ended in draw.';
            break;
          }
          case 'black': {
            const name = this.connections.get(this.blackPlayerId!)?.name;
            message = `${name} wins!`;
            break;
          }
          case 'white': {
            const name = this.connections.get(this.whitePlayerId!)?.name;
            message = `${name} wins!`;
            break;
          }
        }
        break;
      }
    }
    conn.ws.send(<div id="title-box">{message}</div>);
    console.log(`Sent title for game ${this.id} to player ${conn.id}`);
  }

  public broadcastButtonsToPlayer(conn: PlayerConnection) {
    const buttons: JSX.Element[] = [];

    if (this.gomoku.status == 'playing' && this.getPlayerColor(conn)) {
      if (this.takebackRequesterId) {
        if (this.takebackRequesterId === conn.id) {
          buttons.push(
            <button
              id="cancel-takeback-request-button"
              class="decline-button"
              ws-send="click"
            >
              <svg class="icon" alt="Cancel">
                <use href="/icons/decline.svg"></use>
              </svg>
              Cancel Takeback Request
            </button>,
          );
        } else {
          buttons.push(
            <button
              id="accept-takeback-button"
              class="accept-button"
              ws-send="click"
            >
              <svg class="icon" alt="Accept">
                <use href="/icons/accept.svg"></use>
              </svg>
              Accept Takeback
            </button>,
          );
          buttons.push(
            <button
              id="decline-takeback-button"
              class="decline-button"
              ws-send="click"
            >
              <svg class="icon" alt="Decline">
                <use href="/icons/decline.svg"></use>
              </svg>
              Decline Takeback
            </button>,
          );
        }
      } else if (this.drawRequesterId) {
        if (this.drawRequesterId === conn.id) {
          buttons.push(
            <button
              id="cancel-draw-request-button"
              class="decline-button"
              ws-send="click"
            >
              <svg class="icon" alt="Cancel">
                <use href="/icons/decline.svg"></use>
              </svg>
              Cancel Draw Request
            </button>,
          );
        } else {
          buttons.push(
            <button
              id="accept-draw-button"
              class="accept-button"
              ws-send="click"
            >
              <svg class="icon" alt="Accept">
                <use href="/icons/accept.svg"></use>
              </svg>
              Accept Draw
            </button>,
          );
          buttons.push(
            <button
              id="decline-draw-button"
              class="decline-button"
              ws-send="click"
            >
              <svg class="icon" alt="Decline">
                <use href="/icons/decline.svg"></use>
              </svg>
              Decline Draw
            </button>,
          );
        }
      } else {
        buttons.push(
          <button id="resign-button" ws-send="click">
            <svg class="icon" alt="Resign">
              <use href="/icons/resign.svg"></use>
            </svg>
            Resign
          </button>,
        );
        buttons.push(
          <button id="takeback-button" ws-send="click">
            <svg class="icon" alt="Takeback">
              <use href="/icons/undo.svg"></use>
            </svg>
            Takeback
          </button>,
        );
        buttons.push(
          <button id="draw-button" ws-send="click">
            <svg class="icon" alt="Draw">
              <use href="/icons/draw.svg"></use>
            </svg>
            Draw
          </button>,
        );
      }
    } else if (this.gomoku.status === 'finished') {
      if (this.rematchRequesterId) {
        if (this.rematchRequesterId === conn.id) {
          buttons.push(
            <button
              id="cancel-rematch-request-button"
              class="decline-button"
              ws-send="click"
            >
              <svg class="icon" alt="Cancel">
                <use href="/icons/decline.svg"></use>
              </svg>
              Cancel Rematch Request
            </button>,
          );
        } else {
          buttons.push(
            <button
              id="accept-rematch-button"
              class="accept-button"
              ws-send="click"
            >
              <svg class="icon" alt="Accept">
                <use href="/icons/accept.svg"></use>
              </svg>
              Accept Rematch
            </button>,
          );
          buttons.push(
            <button
              id="decline-rematch-button"
              class="decline-button"
              ws-send="click"
            >
              <svg class="icon" alt="Decline">
                <use href="/icons/decline.svg"></use>
              </svg>
              Decline Rematch
            </button>,
          );
        }
      } else {
        buttons.push(
          <button id="rematch-button" ws-send="click">
            <svg class="icon" alt="Rematch">
              <use href="/icons/rotate-right.svg"></use>
            </svg>
            Rematch
          </button>,
        );
      }
    } else if (this.gomoku.status === 'waiting') {
      buttons.push(
        <button id="copy-link-button" onclick="copyGameLink()">
          <svg class="icon" alt="Copy">
            <use href="/icons/clipboard-copy.svg"></use>
          </svg>
          <span id="copy-link-text">Click to copy game link!</span>
        </button>,
      );
    }

    conn.ws.send(<div id="button-box">{buttons}</div>);
    console.log(`Sent buttons for game ${this.id} to player ${conn.id}`);
  }

  private playerTag(playerId: string, color: PlayerColor) {
    const connectionIcon = this.connections.has(playerId)
      ? ''
      : `<img src="/icons/disconnected.svg" alt="Disconnected" class="icon" />`;
    var turnClass =
      this.gomoku.currentPlayerColor === color ? 'player-to-play' : '';
    const classes = `player-name ${'player-' + color} ${turnClass}`.trim();
    return (
      <span class={classes}>
        {this.connections.get(playerId)?.name}
        {connectionIcon}
      </span>
    );
  }

  public handleMessage(ws: WS, message: Message): void {
    const conn = this.connections.get(ws.data.query.playerId);
    if (!conn) {
      console.error(
        `Failed to handle message from player ${ws.data.query.playerId}, because they are not in the game ${this.id}, which it was routed to`,
      );
      return;
    }

    console.log(
      `Handling ${message.type} message in game ${this.id} from player ${conn.id}: ${JSON.stringify(message)}`,
    );
    switch (message.type) {
      case 'make_move': {
        this.handleMakeMove(conn, message as MakeMoveMessage);
        break;
      }
      case 'resign': {
        this.handleResignation(conn, message as ResignationMessage);
        break;
      }
      case 'takeback': {
        this.handleTakebackMessage(conn, message as TakebackMessage);
        break;
      }
      case 'draw': {
        this.handleDrawMessage(conn, message as DrawMessage);
        break;
      }
      case 'rematch': {
        this.handleRematchMessage(conn, message as RematchMessage);
        break;
      }
      case 'update_display_name': {
        this.handleUpdateDisplayName(conn, message as UpdateDisplayNameMessage);
        break;
      }
    }
  }

  private getPlayerColor(conn: PlayerConnection): PlayerColor | undefined {
    if (this.blackPlayerId === conn.id) {
      return 'black';
    } else if (this.whitePlayerId === conn.id) {
      return 'white';
    } else {
      return undefined;
    }
  }

  private handleMakeMove(
    conn: PlayerConnection,
    message: MakeMoveMessage,
  ): void {
    const { row, col } = message;

    var playerColor;
    if (this.blackPlayerId === conn.id) {
      playerColor = 'black';
    } else if (this.whitePlayerId == conn.id) {
      playerColor = 'white';
    } else {
      conn.sendMessage(
        'error',
        'You are not a player in this game, you cannot make a move!',
      );
      return;
    }
    if (this.gomoku.currentPlayerColor !== playerColor) {
      conn.sendMessage('error', "It's not your turn");
      return;
    }

    if (this.takebackRequesterId || this.drawRequesterId) {
      this.takebackRequesterId = null;
      this.drawRequesterId = null;
      this.broadcastButtons();
    }

    const stateBeforeMove = this.gomoku.status;
    const result = this.gomoku.makeMove(playerColor, row, col);
    if (result.success) {
      this.broadcastBoard();
      this.broadcastTitle();
      // We only need to re-send buttons when the game state changes
      if (stateBeforeMove != this.gomoku.status) {
        this.broadcastButtons();
      }
      console.log(
        `Move made in game ${this.id} by ${conn.id}: (${row}, ${col})`,
      );
    } else {
      conn.sendMessage('error', result.error!);
    }
  }

  private handleResignation(
    conn: PlayerConnection,
    message: ResignationMessage,
  ): void {
    console.log(
      `Handling resign message in game ${this.id} from player ${conn.id}: ${{ message }}`,
    );

    if (this.gomoku.status !== 'playing') {
      conn.sendMessage('error', 'You can only resign from an active game.');
      return;
    }

    const resigningPlayerColor = this.getPlayerColor(conn);
    if (!resigningPlayerColor) {
      conn.sendMessage('error', 'You are not a player in this game.');
      return;
    }

    this.gomoku.resign(resigningPlayerColor);
    this.broadcastBoard();
    this.broadcastTitle();
    this.broadcastButtons();

    console.log(`Player ${conn.id} resigned from game ${this.id}`);
  }

  private handleTakebackMessage(
    conn: PlayerConnection,
    message: TakebackMessage,
  ): void {
    if (this.gomoku.status !== 'playing') {
      conn.sendMessage(
        'error',
        'You can only perform this action in an active game.',
      );
      return;
    }
    switch (message.action) {
      case 'request':
        this.handleRequestTakeback(conn);
        break;
      case 'accept':
        if (!this.takebackRequesterId) {
          conn.sendMessage('error', 'No takeback has been requested.');
          return;
        }
        this.handleAcceptTakeback();
        break;
      case 'decline':
        if (!this.takebackRequesterId) {
          conn.sendMessage('error', 'No takeback has been requested.');
          return;
        }
        this.handleDeclineTakeback();
        break;
      case 'cancel':
        if (this.takebackRequesterId !== conn.id) {
          conn.sendMessage(
            'error',
            'You are not the one who requested a takeback.',
          );
          return;
        }
        this.handleCancelTakebackRequest();
        break;
    }
  }

  private handleDrawMessage(
    conn: PlayerConnection,
    message: DrawMessage,
  ): void {
    if (this.gomoku.status !== 'playing') {
      conn.sendMessage(
        'error',
        'You can only perform this action in an active game.',
      );
      return;
    }
    switch (message.action) {
      case 'request':
        this.handleRequestDraw(conn);
        break;
      case 'accept':
        if (!this.drawRequesterId) {
          conn.sendMessage('error', 'No draw has been requested.');
          return;
        }
        this.handleAcceptDraw();
        break;
      case 'decline':
        if (!this.drawRequesterId) {
          conn.sendMessage('error', 'No draw has been requested.');
          return;
        }
        this.handleDeclineDraw();
        break;
      case 'cancel':
        if (this.drawRequesterId !== conn.id) {
          conn.sendMessage(
            'error',
            'You are not the one who requested a draw.',
          );
          return;
        }
        this.handleCancelDrawRequest();
        break;
    }
  }

  private handleRematchMessage(
    conn: PlayerConnection,
    message: RematchMessage,
  ): void {
    if (this.gomoku.status !== 'finished') {
      conn.sendMessage(
        'error',
        'You can only perform this action in a finished game.',
      );
      return;
    }
    switch (message.action) {
      case 'request':
        this.handleRequestRematch(conn);
        break;
      case 'accept':
        if (!this.rematchRequesterId) {
          conn.sendMessage('error', 'No rematch has been requested.');
          return;
        }
        this.handleAcceptRematch();
        break;
      case 'decline':
        if (!this.rematchRequesterId) {
          conn.sendMessage('error', 'No rematch has been requested.');
          return;
        }
        this.handleDeclineRematch();
        break;
      case 'cancel':
        if (this.rematchRequesterId !== conn.id) {
          conn.sendMessage(
            'error',
            'You are not the one who requested a rematch.',
          );
          return;
        }
        this.handleCancelRematchRequest();
        break;
    }
  }

  private handleRequestTakeback(conn: PlayerConnection): void {
    if (this.gomoku.history.length === 0) {
      conn.sendMessage('error', 'There are no moves to take back.');
      return;
    }

    if (this.drawRequesterId) {
      conn.sendMessage('error', 'A draw has already been requested.');
      return;
    }
    this.takebackRequesterId = conn.id;
    this.broadcastButtons();
  }

  private handleAcceptTakeback(): void {
    this.gomoku.undoMove();
    this.takebackRequesterId = null;
    this.broadcastBoard();
    this.broadcastButtons();
    this.broadcastTitle();
  }

  private handleDeclineTakeback(): void {
    this.takebackRequesterId = null;
    this.broadcastButtons();
  }

  private handleRequestDraw(conn: PlayerConnection): void {
    if (this.takebackRequesterId) {
      conn.sendMessage('error', 'A takeback has already been requested.');
      return;
    }

    this.drawRequesterId = conn.id;
    this.broadcastButtons();
  }

  private handleAcceptDraw(): void {
    this.gomoku.declareDraw();
    this.drawRequesterId = null;
    this.broadcastBoard();
    this.broadcastButtons();
    this.broadcastTitle();
  }

  private handleDeclineDraw(): void {
    this.drawRequesterId = null;
    this.broadcastButtons();
  }

  private handleRequestRematch(conn: PlayerConnection): void {
    this.rematchRequesterId = conn.id;
    this.broadcastButtons();
  }

  private handleAcceptRematch(): void {
    const newGameId = this.webSocketHandler.createGame(
      undefined,
      this.whitePlayerId,
      this.blackPlayerId,
    );
    const redirectMessage: RedirectToGameMessage = {
      type: 'redirect_to_game',
      gameId: newGameId,
    };
    this.connections.forEach((c) => {
      c.ws.send(redirectMessage);
    });
  }

  private handleDeclineRematch(): void {
    this.rematchRequesterId = null;
    this.broadcastButtons();
  }

  private handleCancelTakebackRequest(): void {
    this.takebackRequesterId = null;
    this.broadcastButtons();
  }

  private handleCancelDrawRequest(): void {
    this.drawRequesterId = null;
    this.broadcastButtons();
  }

  private handleCancelRematchRequest(): void {
    this.rematchRequesterId = null;
    this.broadcastButtons();
  }

  private handleUpdateDisplayName(
    conn: PlayerConnection,
    message: UpdateDisplayNameMessage,
  ): void {
    const newDisplayName = message.displayName.trim();

    if (!newDisplayName) {
      conn.sendMessage('error', 'Display name cannot be empty.');
      return;
    }

    if (newDisplayName.length > 20) {
      conn.sendMessage(
        'error',
        'Display name cannot be longer than 20 characters.',
      );
      return;
    }

    if (newDisplayName === conn.name) {
      return; // No change, do nothing
    }

    conn.name = newDisplayName;
    this.webSocketHandler.setPlayerName(conn.id, newDisplayName);
    this.broadcastTitle();
  }
}

export class WebSocketHandler {
  private games: Map<String, GameServer>;
  private playerNames: Map<string, string>;

  constructor() {
    this.games = new Map();
    this.playerNames = new Map();
  }

  public handleConnection(ws: WS): void {
    const { gameId, playerId } = ws.data.query;
    if (this.games.has(gameId)) {
      const game = this.games.get(gameId)!;
      game.handleConnection(ws);
    } else {
      ws.send('Error: game not found');
      ws.close();
    }
  }

  public handleDisconnect(ws: WS): void {
    const { gameId, playerId } = ws.data.query;

    const game = this.games.get(gameId);
    if (!game) {
      console.error(
        `Attempted to disconnect player ${playerId} from game ${gameId}, which does not exist!`,
      );
      return;
    }

    game.handleDisconnect(ws);
    console.log(`${playerId} disconnected from game ${gameId}`);

    if (game.connections.size == 0) {
      this.games.delete(gameId);
      console.log(`Game ${gameId} has been deleted (empty).`);
    }
  }

  public getPlayerName(playerId: string): string {
    const name = this.playerNames.get(playerId);
    return name ? name : 'New Player';
  }

  public setPlayerName(playerId: string, displayName: string) {
    this.playerNames.set(playerId, displayName);
  }

  public handleMessage(ws: WS, messageUnparsed: any) {
    let message = messageUnparsed as Message;
    if (!message) {
      console.log(
        `Received malformed message ${messageUnparsed} from player ${ws.data.query.playerId}.`,
      );
      ws.send('Error: malformed message!');
      return;
    }

    const { gameId } = ws.data.query;
    const gameServer = this.games.get(gameId);
    if (!gameServer) {
      console.error(
        `A WebSocket connection was left open for the non-existent game ${gameId}`,
      );
      return;
    }

    gameServer.handleMessage(ws, message);
  }

  public createGame(
    id?: string,
    blackPlayerId?: string,
    whitePlayerId?: string,
  ): string {
    const realId = id ? id : uuidv4();
    this.games.set(realId, new GameServer(realId, this));
    const game = this.games.get(realId)!;
    game.blackPlayerId = blackPlayerId;
    game.whitePlayerId = whitePlayerId;
    return realId;
  }

  public hasGame(id: string): boolean {
    return this.games.has(id);
  }
}
