import type {
  ChatGroup,
  ChatMessage,
  Player,
  SessionSnapshot,
  TakeoverRequest,
  Transaction,
} from "../shared/types";
import type { Session } from "./types";
import { DomainError } from "./errors";
import {
  clampBalance,
  createId,
  now,
  parseAmount,
  DEFAULT_START_BALANCE,
} from "./util";

const MAX_CHAT_LENGTH = 280;

export function snapshotSession(session: Session): SessionSnapshot {
  return {
    id: session.id,
    code: session.code,
    status: session.status,
    createdAt: session.createdAt,
    bankerId: session.bankerId,
    blackoutActive: session.blackoutActive,
    blackoutReason: session.blackoutReason,
    players: Array.from(session.players.values()),
    transactions: session.transactions,
    chats: session.chats,
    groups: session.groups,
    takeoverRequests: session.takeoverRequests,
  };
}

function remapPlayerId(value: string, fromId: string, toId: string): string {
  return value === fromId ? toId : value;
}

function remapSnapshotBanker(snapshot: SessionSnapshot, banker: Player): SessionSnapshot {
  const fromId = snapshot.bankerId;
  const toId = banker.id;
  if (fromId === toId) {
    return snapshot;
  }

  const players = snapshot.players
    .filter((player) => player.id !== toId)
    .map((player) => {
      if (player.id !== fromId) return player;
      return {
        ...player,
        id: toId,
        name: banker.name,
        role: "banker" as const,
        connected: true,
        isDummy: false,
        lastActiveAt: now(),
      };
    });

  if (!players.some((player) => player.id === toId)) {
    players.push({
      ...banker,
      role: "banker",
      connected: true,
      isDummy: false,
      lastActiveAt: now(),
    });
  }

  const transactions = snapshot.transactions.map((tx) => ({
    ...tx,
    fromId: tx.fromId ? remapPlayerId(tx.fromId, fromId, toId) : null,
    toId: tx.toId ? remapPlayerId(tx.toId, fromId, toId) : null,
  }));

  const chats = snapshot.chats.map((message) => ({
    ...message,
    fromId: remapPlayerId(message.fromId, fromId, toId),
  }));

  const groups = snapshot.groups.map((group) => ({
    ...group,
    createdBy: remapPlayerId(group.createdBy, fromId, toId),
    memberIds: group.memberIds.map((id) => remapPlayerId(id, fromId, toId)),
  }));

  const takeoverRequests = snapshot.takeoverRequests.map((request) => ({
    ...request,
    dummyId: remapPlayerId(request.dummyId, fromId, toId),
    requesterId: remapPlayerId(request.requesterId, fromId, toId),
  }));

  return {
    ...snapshot,
    bankerId: toId,
    players,
    transactions,
    chats,
    groups,
    takeoverRequests,
  };
}

export function applySnapshot(session: Session, snapshot: SessionSnapshot): void {
  const currentBanker = session.players.get(session.bankerId) ?? null;
  const normalized = currentBanker ? remapSnapshotBanker(snapshot, currentBanker) : snapshot;

  session.status = normalized.status;
  session.createdAt = normalized.createdAt;
  session.bankerId = currentBanker ? currentBanker.id : normalized.bankerId;
  session.blackoutActive = normalized.blackoutActive;
  session.blackoutReason = normalized.blackoutReason;

  const players = new Map<string, Player>();
  normalized.players.forEach((player) => {
    players.set(player.id, { ...player });
  });

  const banker = players.get(session.bankerId);
  if (banker) {
    banker.role = "banker";
    banker.connected = true;
    banker.isDummy = false;
    banker.lastActiveAt = now();
  }

  players.forEach((player) => {
    if (player.id !== session.bankerId && player.role === "banker") {
      player.role = "player";
    }
  });

  session.players = players;
  session.transactions = normalized.transactions.map((tx) => ({ ...tx }));
  session.chats = normalized.chats.map((chat) => ({ ...chat }));
  session.groups = normalized.groups.map((group) => ({ ...group }));
  session.takeoverRequests = normalized.takeoverRequests.map((request) => ({ ...request }));
}

export function isBlackoutActive(session: Session): boolean {
  return session.blackoutActive;
}

export function joinSession(
  session: Session,
  name: string,
  playerId?: string | null,
): Player {
  const trimmedName = name.trim() || "Player";
  if (playerId) {
    const existing = session.players.get(playerId);
    if (existing) {
      existing.connected = true;
      existing.isDummy = false;
      existing.name = trimmedName;
      existing.lastActiveAt = now();
      return existing;
    }
  }

  const id = createId();
  const joinedAt = now();
  const player: Player = {
    id,
    name: trimmedName,
    role: "player",
    balance: DEFAULT_START_BALANCE,
    connected: true,
    isDummy: false,
    joinedAt,
    lastActiveAt: joinedAt,
  };

  session.players.set(id, player);
  return player;
}

export function startSession(session: Session, bankerId: string): void {
  ensureBanker(session, bankerId);
  if (session.status === "ended") {
    throw new DomainError("Session has already ended");
  }
  session.status = "active";
}

export function endSession(session: Session, bankerId: string): void {
  ensureBanker(session, bankerId);
  session.status = "ended";
}

export function disconnectPlayer(session: Session, playerId: string): void {
  const player = getPlayer(session, playerId);
  player.connected = false;
  player.lastActiveAt = now();
  if (player.role !== "banker") {
    player.isDummy = true;
  }
}

export function setBlackout(
  session: Session,
  bankerId: string,
  active: boolean,
  reason?: string | null,
): void {
  ensureOpenSession(session);
  ensureBanker(session, bankerId);
  session.blackoutActive = Boolean(active);
  session.blackoutReason = active ? reason?.trim() || "EMP in effect" : null;
}

export function transfer(
  session: Session,
  fromId: string,
  toId: string,
  amountValue: unknown,
  note?: string | null,
): Transaction {
  ensureActiveSession(session);
  ensureNotBlackout(session, fromId);
  const amount = parseAmount(amountValue);
  if (amount === null || amount <= 0) {
    throw new DomainError("Transfer amount must be positive");
  }
  const from = getPlayer(session, fromId);
  const to = getPlayer(session, toId);
  if (from.role === "banker" || to.role === "banker") {
    throw new DomainError("Banker does not hold a balance");
  }
  if (from.id === to.id) {
    throw new DomainError("Cannot transfer to yourself");
  }
  if (from.balance < amount) {
    throw new DomainError("Insufficient funds");
  }
  from.balance = clampBalance(from.balance - amount);
  to.balance = clampBalance(to.balance + amount);
  const transaction = createTransaction("transfer", from.id, to.id, amount, note, "player");
  session.transactions.unshift(transaction);
  return transaction;
}

export function bankerAdjust(
  session: Session,
  bankerId: string,
  targetId: string,
  amountValue: unknown,
  note?: string | null,
): Transaction {
  ensureActiveSession(session);
  ensureNotBlackout(session, bankerId);
  ensureBanker(session, bankerId);
  const amount = parseAmount(amountValue);
  if (amount === null || amount === 0) {
    throw new DomainError("Adjustment amount must be non-zero");
  }
  const target = getPlayer(session, targetId);
  if (target.role === "banker") {
    throw new DomainError("Banker does not hold a balance");
  }
  target.balance = clampBalance(target.balance + amount);
  const transaction = createTransaction(
    "banker_adjust",
    null,
    target.id,
    amount,
    note,
    "banker",
  );
  session.transactions.unshift(transaction);
  return transaction;
}

export function bankerForceTransfer(
  session: Session,
  bankerId: string,
  fromId: string,
  toId: string,
  amountValue: unknown,
  note?: string | null,
): Transaction {
  ensureActiveSession(session);
  ensureNotBlackout(session, bankerId);
  ensureBanker(session, bankerId);
  const amount = parseAmount(amountValue);
  if (amount === null || amount <= 0) {
    throw new DomainError("Transfer amount must be positive");
  }
  const from = getPlayer(session, fromId);
  const to = getPlayer(session, toId);
  if (from.role === "banker" || to.role === "banker") {
    throw new DomainError("Banker does not hold a balance");
  }
  if (from.id === to.id) {
    throw new DomainError("Cannot transfer to the same player");
  }
  if (from.balance < amount) {
    throw new DomainError("Source player lacks funds");
  }
  from.balance = clampBalance(from.balance - amount);
  to.balance = clampBalance(to.balance + amount);
  const transaction = createTransaction(
    "banker_force_transfer",
    from.id,
    to.id,
    amount,
    note,
    "banker",
  );
  session.transactions.unshift(transaction);
  return transaction;
}

export function createDummyPlayer(
  session: Session,
  bankerId: string,
  name: string,
  balanceValue?: unknown,
): Player {
  ensureOpenSession(session);
  ensureBanker(session, bankerId);
  const joinedAt = now();
  const balance = parseAmount(balanceValue ?? DEFAULT_START_BALANCE) ?? DEFAULT_START_BALANCE;
  const player: Player = {
    id: createId(),
    name: name.trim() || "Dummy",
    role: "player",
    balance: clampBalance(balance),
    connected: false,
    isDummy: true,
    joinedAt,
    lastActiveAt: joinedAt,
  };
  session.players.set(player.id, player);
  return player;
}

export function addChatMessage(
  session: Session,
  playerId: string,
  body: string,
  groupId?: string | null,
): ChatMessage {
  ensureOpenSession(session);
  ensureNotBlackout(session, playerId);
  const trimmed = body.trim();
  if (!trimmed) {
    throw new DomainError("Message cannot be empty");
  }
  if (trimmed.length > MAX_CHAT_LENGTH) {
    throw new DomainError("Message is too long");
  }
  const sender = getPlayer(session, playerId);
  const message: ChatMessage = {
    id: createId(),
    fromId: sender.id,
    body: trimmed,
    createdAt: now(),
    groupId: groupId ?? null,
  };
  session.chats.unshift(message);
  return message;
}

export function createChatGroup(
  session: Session,
  playerId: string,
  name: string,
  memberIds: string[],
): ChatGroup {
  ensureOpenSession(session);
  ensureNotBlackout(session, playerId);
  getPlayer(session, playerId);
  const trimmed = name.trim();
  if (!trimmed) {
    throw new DomainError("Group name is required");
  }
  const deduped = Array.from(new Set([playerId, ...memberIds]));
  deduped.forEach((memberId) => getPlayer(session, memberId));
  const group: ChatGroup = {
    id: createId(),
    name: trimmed,
    memberIds: deduped,
    createdAt: now(),
    createdBy: playerId,
  };
  session.groups.unshift(group);
  return group;
}

export function requestTakeover(
  session: Session,
  requesterId: string,
  dummyId: string,
): TakeoverRequest {
  ensureOpenSession(session);
  const requester = getPlayer(session, requesterId);
  const dummy = getPlayer(session, dummyId);
  if (!dummy.isDummy) {
    throw new DomainError("Selected player is not available for takeover");
  }
  if (requester.role === "banker") {
    throw new DomainError("Banker cannot request takeover");
  }
  const existing = session.takeoverRequests.find(
    (request) =>
      request.dummyId === dummyId &&
      request.requesterId === requesterId &&
      request.status === "pending",
  );
  if (existing) {
    return existing;
  }
  const request: TakeoverRequest = {
    id: createId(),
    dummyId,
    requesterId,
    createdAt: now(),
    status: "pending",
  };
  session.takeoverRequests.unshift(request);
  return request;
}

export function requestTakeoverByToken(
  session: Session,
  dummyId: string,
  requesterName?: string | null,
): string {
  ensureOpenSession(session);
  const dummy = getPlayer(session, dummyId);
  if (!dummy.isDummy) {
    throw new DomainError("Selected player is not available for takeover");
  }
  const token = createId();
  const name = requesterName?.trim() || null;
  const request: TakeoverRequest = {
    id: createId(),
    dummyId,
    requesterId: token,
    requesterName: name,
    requesterToken: token,
    createdAt: now(),
    status: "pending",
  };
  session.takeoverRequests.unshift(request);
  return token;
}

export function approveTakeover(
  session: Session,
  bankerId: string,
  dummyId: string,
  requesterId: string,
): string {
  ensureOpenSession(session);
  ensureBanker(session, bankerId);
  const dummy = getPlayer(session, dummyId);
  if (!dummy.isDummy) {
    throw new DomainError("Selected player is not a dummy");
  }
  const targetRequest = session.takeoverRequests.find(
    (request) =>
      request.dummyId === dummyId &&
      request.requesterId === requesterId &&
      request.status === "pending",
  );
  if (!targetRequest) {
    throw new DomainError("No takeover request found");
  }
  const requester = session.players.get(requesterId);
  if (!requester && targetRequest.requesterToken) {
    targetRequest.status = "approved";
    session.takeoverRequests.forEach((request) => {
      if (
        request.dummyId === dummyId &&
        request.status === "pending" &&
        request.requesterId !== requesterId
      ) {
        request.status = "rejected";
      }
    });
    return dummy.id;
  }
  if (!requester) {
    throw new DomainError("Requester not found");
  }
  targetRequest.status = "approved";
  // Reject any other pending requests for the same dummy.
  session.takeoverRequests.forEach((request) => {
    if (
      request.dummyId === dummyId &&
      request.status === "pending" &&
      request.requesterId !== requesterId
    ) {
      request.status = "rejected";
    }
  });

  dummy.isDummy = false;
  dummy.connected = requester.connected;
  dummy.name = requester.name;
  dummy.lastActiveAt = now();
  session.players.delete(requesterId);

  return dummy.id;
}

export function claimTakeover(session: Session, requesterToken: string): string {
  ensureOpenSession(session);
  const targetRequest = session.takeoverRequests.find(
    (request) =>
      request.requesterToken === requesterToken && request.status === "approved",
  );
  if (!targetRequest) {
    throw new DomainError("Takeover not approved");
  }
  const dummy = getPlayer(session, targetRequest.dummyId);
  if (!dummy.isDummy) {
    throw new DomainError("Dummy already taken");
  }
  dummy.isDummy = false;
  dummy.connected = true;
  dummy.name = targetRequest.requesterName?.trim() || dummy.name;
  dummy.lastActiveAt = now();
  session.takeoverRequests = session.takeoverRequests.filter(
    (request) => request.id !== targetRequest.id,
  );
  return dummy.id;
}

function ensureActiveSession(session: Session): void {
  ensureOpenSession(session);
}

function ensureNotBlackout(session: Session, actorId?: string): void {
  if (!isBlackoutActive(session)) {
    return;
  }
  if (actorId) {
    const actor = session.players.get(actorId);
    if (actor?.role === "banker") {
      return;
    }
  }
  throw new DomainError("EMP in effect");
}

function ensureOpenSession(session: Session): void {
  if (session.status === "ended") {
    throw new DomainError("Session has ended");
  }
}

function ensureBanker(session: Session, bankerId: string): void {
  const banker = getPlayer(session, bankerId);
  if (banker.role !== "banker") {
    throw new DomainError("Only the banker can perform this action");
  }
}

function getPlayer(session: Session, playerId: string): Player {
  const player = session.players.get(playerId);
  if (!player) {
    throw new DomainError("Player not found");
  }
  return player;
}

function createTransaction(
  kind: Transaction["kind"],
  fromId: string | null,
  toId: string | null,
  amount: number,
  note: string | null | undefined,
  initiatedBy: Transaction["initiatedBy"],
): Transaction {
  return {
    id: createId(),
    kind,
    fromId,
    toId,
    amount: clampBalance(amount),
    note: note?.trim() || null,
    createdAt: now(),
    initiatedBy,
  };
}
