import { randomUUID } from "node:crypto";
import type { SaveData } from "@psx/shared";
import { MAX_PLAYERS } from "@psx/shared";

/**
 * Persistence port.
 *
 * Milestone 2 still has no Neon credentials, so the in-memory implementation
 * below is what runs. The interface mirrors `schema.sql` closely enough that a
 * Neon-backed implementation is a matter of writing queries rather than
 * reshaping call sites.
 */

export interface UserRecord {
  id: string;
  username: string;
  email: string;
  displayName: string | null;
  passwordHash: string;
  createdAt: string;
  lastLogin: string | null;
}

export interface SaveRecord {
  userId: string;
  slot: number;
  campaign: string;
  title: string | null;
  data: SaveData;
  playtimeSeconds: number;
  updatedAt: string;
}

export type LobbyStatus = "waiting" | "starting" | "in_game" | "closed";

export interface LobbyPlayerRecord {
  userId: string;
  peerId: number;
  displayName: string;
  isReady: boolean;
  joinedAt: string;
}

export interface LobbyRecord {
  id: string;
  code: string;
  hostId: string;
  status: LobbyStatus;
  maxPlayers: number;
  settings: Record<string, unknown>;
  createdAt: string;
  expiresAt: string;
  players: LobbyPlayerRecord[];
}

export interface Store {
  createUser(input: {
    username: string;
    email: string;
    displayName?: string;
    passwordHash: string;
  }): Promise<UserRecord>;
  findUserByUsername(username: string): Promise<UserRecord | null>;
  findUserById(id: string): Promise<UserRecord | null>;
  touchLogin(id: string): Promise<void>;

  putSave(record: Omit<SaveRecord, "updatedAt">): Promise<SaveRecord>;
  getSave(userId: string, campaign: string, slot: number): Promise<SaveRecord | null>;
  listSaves(userId: string): Promise<SaveRecord[]>;
  deleteSave(userId: string, campaign: string, slot: number): Promise<boolean>;

  createLobby(hostId: string, hostName: string, settings?: Record<string, unknown>): Promise<LobbyRecord>;
  findLobbyByCode(code: string): Promise<LobbyRecord | null>;
  joinLobby(code: string, userId: string, displayName: string): Promise<LobbyRecord | { error: string }>;
  leaveLobby(code: string, userId: string): Promise<LobbyRecord | null>;
  setReady(code: string, userId: string, isReady: boolean): Promise<LobbyRecord | null>;
  setLobbyStatus(code: string, status: LobbyStatus): Promise<LobbyRecord | null>;
}

const saveKey = (userId: string, campaign: string, slot: number) =>
  `${userId}:${campaign}:${slot}`;

/**
 * Lobby codes are read aloud over voice chat, so the alphabet omits characters
 * that sound or look alike: no O/0, no I/1, no S/5.
 */
const CODE_ALPHABET = "ABCDEFGHJKLMNPQRTUVWXY2346789";
const CODE_LENGTH = 5;

export class InMemoryStore implements Store {
  private readonly users = new Map<string, UserRecord>();
  private readonly usersByUsername = new Map<string, string>();
  private readonly saves = new Map<string, SaveRecord>();
  private readonly lobbies = new Map<string, LobbyRecord>();

  // --- users ---------------------------------------------------------------

  async createUser(input: {
    username: string;
    email: string;
    displayName?: string;
    passwordHash: string;
  }): Promise<UserRecord> {
    const key = input.username.toLowerCase();
    if (this.usersByUsername.has(key)) throw new Error("username already taken");

    const user: UserRecord = {
      id: randomUUID(),
      username: input.username,
      email: input.email,
      displayName: input.displayName ?? input.username,
      passwordHash: input.passwordHash,
      createdAt: new Date().toISOString(),
      lastLogin: null,
    };
    this.users.set(user.id, user);
    this.usersByUsername.set(key, user.id);
    return user;
  }

  async findUserByUsername(username: string): Promise<UserRecord | null> {
    const id = this.usersByUsername.get(username.toLowerCase());
    return id ? (this.users.get(id) ?? null) : null;
  }

  async findUserById(id: string): Promise<UserRecord | null> {
    return this.users.get(id) ?? null;
  }

  async touchLogin(id: string): Promise<void> {
    const user = this.users.get(id);
    if (user) user.lastLogin = new Date().toISOString();
  }

  // --- saves ---------------------------------------------------------------

  async putSave(record: Omit<SaveRecord, "updatedAt">): Promise<SaveRecord> {
    const stored: SaveRecord = { ...record, updatedAt: new Date().toISOString() };
    this.saves.set(saveKey(record.userId, record.campaign, record.slot), stored);
    return stored;
  }

  async getSave(userId: string, campaign: string, slot: number): Promise<SaveRecord | null> {
    return this.saves.get(saveKey(userId, campaign, slot)) ?? null;
  }

  async listSaves(userId: string): Promise<SaveRecord[]> {
    return [...this.saves.values()]
      .filter((record) => record.userId === userId)
      .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
  }

  async deleteSave(userId: string, campaign: string, slot: number): Promise<boolean> {
    return this.saves.delete(saveKey(userId, campaign, slot));
  }

  // --- lobbies -------------------------------------------------------------

  private generateCode(): string {
    for (let attempt = 0; attempt < 50; attempt++) {
      let code = "";
      for (let i = 0; i < CODE_LENGTH; i++) {
        code += CODE_ALPHABET[Math.floor(Math.random() * CODE_ALPHABET.length)];
      }
      if (!this.lobbies.has(code)) return code;
    }
    throw new Error("could not allocate a free lobby code");
  }

  async createLobby(
    hostId: string,
    hostName: string,
    settings: Record<string, unknown> = {},
  ): Promise<LobbyRecord> {
    const now = Date.now();
    const lobby: LobbyRecord = {
      id: randomUUID(),
      code: this.generateCode(),
      hostId,
      status: "waiting",
      maxPlayers: MAX_PLAYERS,
      settings,
      createdAt: new Date(now).toISOString(),
      expiresAt: new Date(now + 4 * 60 * 60 * 1000).toISOString(),
      // The host is peer 0 by construction, which is what makes "peer 0 is
      // authoritative" a safe assumption everywhere else in the stack.
      players: [
        {
          userId: hostId,
          peerId: 0,
          displayName: hostName,
          isReady: true,
          joinedAt: new Date(now).toISOString(),
        },
      ],
    };
    this.lobbies.set(lobby.code, lobby);
    return lobby;
  }

  async findLobbyByCode(code: string): Promise<LobbyRecord | null> {
    const lobby = this.lobbies.get(code.toUpperCase());
    if (!lobby) return null;
    if (Date.parse(lobby.expiresAt) < Date.now()) {
      lobby.status = "closed";
    }
    return lobby;
  }

  async joinLobby(
    code: string,
    userId: string,
    displayName: string,
  ): Promise<LobbyRecord | { error: string }> {
    const lobby = await this.findLobbyByCode(code);
    if (!lobby) return { error: "lobby not found" };
    if (lobby.status === "closed") return { error: "lobby is closed" };

    const existing = lobby.players.find((player) => player.userId === userId);
    if (existing) return lobby; // rejoin is idempotent

    if (lobby.players.length >= lobby.maxPlayers) return { error: "lobby is full" };

    // Reuse the lowest free peer id so a 4-player lobby always spans 0..3 even
    // after churn — the client indexes spawn points by peer id.
    const taken = new Set(lobby.players.map((player) => player.peerId));
    let peerId = 0;
    while (taken.has(peerId)) peerId++;

    lobby.players.push({
      userId,
      peerId,
      displayName,
      isReady: false,
      joinedAt: new Date().toISOString(),
    });
    return lobby;
  }

  async leaveLobby(code: string, userId: string): Promise<LobbyRecord | null> {
    const lobby = await this.findLobbyByCode(code);
    if (!lobby) return null;

    lobby.players = lobby.players.filter((player) => player.userId !== userId);

    // Losing the host ends the session: the authoritative simulation lived in
    // that browser, so there is nothing for the remaining peers to continue.
    if (lobby.players.length === 0 || userId === lobby.hostId) {
      lobby.status = "closed";
    }
    return lobby;
  }

  async setReady(code: string, userId: string, isReady: boolean): Promise<LobbyRecord | null> {
    const lobby = await this.findLobbyByCode(code);
    if (!lobby) return null;
    const player = lobby.players.find((entry) => entry.userId === userId);
    if (player) player.isReady = isReady;
    return lobby;
  }

  async setLobbyStatus(code: string, status: LobbyStatus): Promise<LobbyRecord | null> {
    const lobby = await this.findLobbyByCode(code);
    if (!lobby) return null;
    lobby.status = status;
    return lobby;
  }
}
