import type { Server, Socket } from "socket.io";
import type { RowDataPacket, ResultSetHeader } from "mysql2";
import { randomUUID } from "crypto";
import db from "../db.js";
import { rollDice } from "../dice.js";

// ── Types ──────────────────────────────────────────────────────────────────

export interface CombatEnemy {
  id: string;
  name: string;
  hp_current?: number;
  hp_max?: number;
}

export interface CombatState {
  id: string;
  label?: string;
  mode: "team";
  round: number;
  phase: "rolling" | "active";
  current_side: "party" | "enemies";
  party_roll: number | null;
  enemy_roll: number | null;
  party_rolls: Record<number, number>;
  character_ids: number[];
  enemies: CombatEnemy[];
}

// ── Helpers ────────────────────────────────────────────────────────────────

export async function loadCombats(campaignId: number): Promise<CombatState[]> {
  const [rows] = await db.execute<RowDataPacket[]>(
    "SELECT combat_state FROM campaigns WHERE id = ?",
    [campaignId]
  );
  if (rows.length === 0 || rows[0].combat_state === null) return [];
  const raw = rows[0].combat_state;
  return typeof raw === "string" ? JSON.parse(raw) : raw;
}

async function saveCombats(campaignId: number, combats: CombatState[]): Promise<void> {
  await db.execute(
    "UPDATE campaigns SET combat_state = ? WHERE id = ?",
    [combats.length > 0 ? JSON.stringify(combats) : null, campaignId]
  );
}

// Returns combats with hp_current and hp_max removed from every enemy.
export function stripHp(combats: CombatState[]): CombatState[] {
  return combats.map((c) => ({
    ...c,
    enemies: c.enemies.map(({ id, name }) => ({ id, name })),
  }));
}

// Broadcast full state to sender (if DM), HP-stripped to everyone else in the room.
function broadcast(
  io: Server,
  socket: Socket,
  campaignId: number,
  combats: CombatState[],
  callerIsDM: boolean
): void {
  socket.to(`campaign:${campaignId}`).emit("initiative:updated", stripHp(combats));
  socket.emit("initiative:updated", callerIsDM ? combats : stripHp(combats));
}

async function checkDM(socket: Socket, campaignId: number): Promise<boolean> {
  const userId = socket.data.user?.userId;
  const [rows] = await db.execute<RowDataPacket[]>(
    "SELECT role FROM campaign_members WHERE campaign_id = ? AND user_id = ?",
    [campaignId, userId]
  );
  return rows.length > 0 && rows[0].role === "dm";
}

async function insertRollLog(
  campaignId: number,
  characterId: number | null,
  characterName: string,
  characterColor: string,
  label: string,
  rollValue: number,
  rolls: number[]
): Promise<RowDataPacket> {
  const [ins] = await db.execute<ResultSetHeader>(
    `INSERT INTO roll_log
       (campaign_id, character_id, character_name, character_color,
        type, subtype, label, dice_expression, rolls, modifier, total,
        advantage, disadvantage, nat20)
     VALUES (?, ?, ?, ?, 'custom', 'initiative', ?, '1d20', ?, 0, ?, 0, 0, ?)`,
    [
      campaignId, characterId, characterName, characterColor,
      label, JSON.stringify(rolls), rollValue, rollValue === 20 ? 1 : 0,
    ]
  );
  const [saved] = await db.execute<RowDataPacket[]>(
    "SELECT * FROM roll_log WHERE id = ?",
    [ins.insertId]
  );
  if (!saved[0]) throw new Error("insertRollLog: SELECT after INSERT returned no row");
  return saved[0];
}

// ── Handler registration ───────────────────────────────────────────────────

export function registerInitiativeHandlers(io: Server, socket: Socket): void {
  // Send state to requesting client (called right after join-campaign).
  socket.on("initiative:request-state", async (campaignId: number) => {
    try {
      const userId = socket.data.user?.userId;
      const [rows] = await db.execute<RowDataPacket[]>(
        "SELECT role FROM campaign_members WHERE campaign_id = ? AND user_id = ?",
        [campaignId, userId]
      );
      if (rows.length === 0) return; // not a member of this campaign
      const dm = rows[0].role === "dm";
      const combats = await loadCombats(campaignId);
      socket.emit("initiative:state", dm ? combats : stripHp(combats));
    } catch (err) {
      console.error("[initiative]", err);
    }
  });

  // DM: open rolling phase with enemies configured.
  socket.on("initiative:start", async (data: {
    campaignId: number;
    label?: string;
    character_ids: number[];
    enemies: Array<{ name: string; hp_max: number }>;
  }) => {
    try {
      if (!await checkDM(socket, data.campaignId)) return;

      const combats = await loadCombats(data.campaignId);
      const newCombat: CombatState = {
        id: randomUUID(),
        label: data.label,
        mode: "team",
        round: 1,
        phase: "rolling",
        current_side: "party",
        party_roll: null,
        enemy_roll: null,
        party_rolls: {},
        character_ids: data.character_ids,
        enemies: data.enemies.map((e) => ({
          id: randomUUID(),
          name: e.name,
          hp_current: e.hp_max,
          hp_max: e.hp_max,
        })),
      };
      const updated = [...combats, newCombat];
      await saveCombats(data.campaignId, updated);
      broadcast(io, socket, data.campaignId, updated, true);
    } catch (err) {
      console.error("[initiative]", err);
    }
  });

  // Any campaign member: roll d20 for their character's initiative.
  socket.on("initiative:roll", async (data: {
    campaignId: number;
    combatId: string;
    characterId: number;
    characterName: string;
    characterColor: string;
  }) => {
    try {
      const userId = socket.data.user?.userId;
      const dm = await checkDM(socket, data.campaignId);
      if (!dm) {
        const [charRows] = await db.execute<RowDataPacket[]>(
          "SELECT user_id FROM characters WHERE id = ?",
          [data.characterId]
        );
        if (charRows.length === 0 || charRows[0].user_id !== userId) return;
      }

      const result = rollDice("1d20", {});
      const rollValue = result.total;

      const saved = await insertRollLog(
        data.campaignId, data.characterId, data.characterName, data.characterColor,
        "Initiative", rollValue, result.rolls
      );
      io.to(`campaign:${data.campaignId}`).emit("roll:result", {
        ...saved,
        rolls: result.rolls,
        advantage: false,
        disadvantage: false,
        nat20: rollValue === 20,
      });

      const combats = await loadCombats(data.campaignId);
      const updated = combats.map((c) => {
        if (c.id !== data.combatId) return c;
        const newRolls = { ...c.party_rolls, [data.characterId]: rollValue };
        return {
          ...c,
          party_rolls: newRolls,
          party_roll: Math.max(...Object.values(newRolls)),
        };
      });
      await saveCombats(data.campaignId, updated);
      broadcast(io, socket, data.campaignId, updated, dm);
    } catch (err) {
      console.error("[initiative]", err);
    }
  });

  // DM: roll d20 for enemies.
  socket.on("initiative:enemy-roll", async (data: {
    campaignId: number;
    combatId: string;
  }) => {
    try {
      if (!await checkDM(socket, data.campaignId)) return;

      const result = rollDice("1d20", {});
      const rollValue = result.total;

      const saved = await insertRollLog(
        data.campaignId, null, "DM", "#888888",
        "Enemy Initiative", rollValue, result.rolls
      );
      io.to(`campaign:${data.campaignId}`).emit("roll:result", {
        ...saved,
        rolls: result.rolls,
        advantage: false,
        disadvantage: false,
        nat20: rollValue === 20,
      });

      const combats = await loadCombats(data.campaignId);
      const updated = combats.map((c) =>
        c.id !== data.combatId ? c : { ...c, enemy_roll: rollValue }
      );
      await saveCombats(data.campaignId, updated);
      broadcast(io, socket, data.campaignId, updated, true);
    } catch (err) {
      console.error("[initiative]", err);
    }
  });

  // DM: end rolling phase, set current_side based on results. Ties go to party.
  socket.on("initiative:begin", async (data: {
    campaignId: number;
    combatId: string;
  }) => {
    try {
      if (!await checkDM(socket, data.campaignId)) return;

      const combats = await loadCombats(data.campaignId);
      const updated = combats.map((c) => {
        if (c.id !== data.combatId) return c;
        const partyWins = (c.party_roll ?? 0) >= (c.enemy_roll ?? 0);
        return {
          ...c,
          phase: "active" as const,
          current_side: (partyWins ? "party" : "enemies") as "party" | "enemies",
        };
      });
      await saveCombats(data.campaignId, updated);
      broadcast(io, socket, data.campaignId, updated, true);
    } catch (err) {
      console.error("[initiative]", err);
    }
  });

  // DM: flip active side; increment round when side flips back to party.
  socket.on("initiative:next", async (data: {
    campaignId: number;
    combatId: string;
  }) => {
    try {
      if (!await checkDM(socket, data.campaignId)) return;

      const combats = await loadCombats(data.campaignId);
      const updated = combats.map((c) => {
        if (c.id !== data.combatId) return c;
        const nextSide: "party" | "enemies" =
          c.current_side === "party" ? "enemies" : "party";
        return {
          ...c,
          current_side: nextSide,
          round: nextSide === "party" ? c.round + 1 : c.round,
        };
      });
      await saveCombats(data.campaignId, updated);
      broadcast(io, socket, data.campaignId, updated, true);
    } catch (err) {
      console.error("[initiative]", err);
    }
  });

  // DM: edit an enemy's name or HP.
  socket.on("initiative:update-enemy", async (data: {
    campaignId: number;
    combatId: string;
    enemyId: string;
    name?: string;
    hp_current?: number;
    hp_max?: number;
  }) => {
    try {
      if (!await checkDM(socket, data.campaignId)) return;

      const combats = await loadCombats(data.campaignId);
      const updated = combats.map((c) => {
        if (c.id !== data.combatId) return c;
        return {
          ...c,
          enemies: c.enemies.map((e) =>
            e.id !== data.enemyId ? e : {
              ...e,
              ...(data.name !== undefined ? { name: data.name } : {}),
              ...(data.hp_current !== undefined ? { hp_current: data.hp_current } : {}),
              ...(data.hp_max !== undefined ? { hp_max: data.hp_max } : {}),
            }
          ),
        };
      });
      await saveCombats(data.campaignId, updated);
      broadcast(io, socket, data.campaignId, updated, true);
    } catch (err) {
      console.error("[initiative]", err);
    }
  });

  // DM: add a new enemy mid-combat.
  socket.on("initiative:add-enemy", async (data: {
    campaignId: number;
    combatId: string;
    name: string;
    hp_max: number;
  }) => {
    try {
      if (!await checkDM(socket, data.campaignId)) return;

      const combats = await loadCombats(data.campaignId);
      const updated = combats.map((c) =>
        c.id !== data.combatId ? c : {
          ...c,
          enemies: [...c.enemies, {
            id: randomUUID(),
            name: data.name,
            hp_current: data.hp_max,
            hp_max: data.hp_max,
          }],
        }
      );
      await saveCombats(data.campaignId, updated);
      broadcast(io, socket, data.campaignId, updated, true);
    } catch (err) {
      console.error("[initiative]", err);
    }
  });

  // DM: remove an enemy (died, fled).
  socket.on("initiative:remove-enemy", async (data: {
    campaignId: number;
    combatId: string;
    enemyId: string;
  }) => {
    try {
      if (!await checkDM(socket, data.campaignId)) return;

      const combats = await loadCombats(data.campaignId);
      const updated = combats.map((c) =>
        c.id !== data.combatId ? c : {
          ...c,
          enemies: c.enemies.filter((e) => e.id !== data.enemyId),
        }
      );
      await saveCombats(data.campaignId, updated);
      broadcast(io, socket, data.campaignId, updated, true);
    } catch (err) {
      console.error("[initiative]", err);
    }
  });

  // DM: end combat, remove from array (saves NULL when last combat ends).
  socket.on("initiative:end", async (data: {
    campaignId: number;
    combatId: string;
  }) => {
    try {
      if (!await checkDM(socket, data.campaignId)) return;

      const combats = await loadCombats(data.campaignId);
      const updated = combats.filter((c) => c.id !== data.combatId);
      await saveCombats(data.campaignId, updated);
      broadcast(io, socket, data.campaignId, updated, true);
    } catch (err) {
      console.error("[initiative]", err);
    }
  });
}
