import { Router } from "express";
import type { ParamsDictionary } from "express-serve-static-core";
import type { RowDataPacket, ResultSetHeader, ExecuteValues } from "mysql2";
import type { Server } from "socket.io";
import db from "../db.js";
import { broadcastToCampaign } from "../socket.js";
import { parseJson } from "../utils/parseJson.js";
import { requireAuth } from "../auth/middleware.js";

type CampaignParams = ParamsDictionary & { campaignId: string };

const router = Router({ mergeParams: true });

const DEFAULT_STATS = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];

function generateCharacterColor(): string {
  const hue = Math.floor(Math.random() * 360);
  return `hsl(${hue}, 60%, 65%)`;
}

function parseGear(rows: RowDataPacket[]) {
  return rows.map((r) => ({
    ...r,
    properties: parseJson(r.properties),
    effects: parseJson(r.effects),
  }));
}

function parseTalents(rows: RowDataPacket[]) {
  return rows.map((r) => ({ ...r, effect: parseJson(r.effect) }));
}

async function canModifyCharacter(characterId: string, userId: number): Promise<boolean> {
  // DMs can modify any character in their campaign
  const [dmCheck] = await db.execute<RowDataPacket[]>(
    `SELECT cm.role FROM campaign_members cm
     JOIN characters c ON c.campaign_id = cm.campaign_id
     WHERE c.id = ? AND cm.user_id = ? AND cm.role = 'dm'`,
    [characterId, userId]
  );
  if (dmCheck.length > 0) return true;

  // Players can only modify their own characters
  const [ownerCheck] = await db.execute<RowDataPacket[]>(
    "SELECT id FROM characters WHERE id = ? AND user_id = ?",
    [characterId, userId]
  );
  return ownerCheck.length > 0;
}

async function enrichCharacters(characters: RowDataPacket[]) {
  return Promise.all(
    characters.map(async (char) => {
      const [stats] = await db.execute<RowDataPacket[]>(
        "SELECT stat_name, value FROM character_stats WHERE character_id = ?",
        [char.id]
      );
      const [gear] = await db.execute<RowDataPacket[]>(
        "SELECT * FROM character_gear WHERE character_id = ?",
        [char.id]
      );
      const [talents] = await db.execute<RowDataPacket[]>(
        "SELECT * FROM character_talents WHERE character_id = ?",
        [char.id]
      );
      return {
        ...char,
        overrides: parseJson(char.overrides),
        stats,
        gear: parseGear(gear),
        talents: parseTalents(talents),
      };
    })
  );
}

// GET /api/campaigns/:campaignId/characters
router.get<CampaignParams>("/", async (req, res) => {
  try {
    const { campaignId } = req.params;
    const [characters] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM characters WHERE campaign_id = ? ORDER BY name",
      [campaignId]
    );
    const enriched = await enrichCharacters(characters);
    res.json(enriched);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// POST /api/campaigns/:campaignId/characters
router.post<CampaignParams>("/", requireAuth, async (req, res) => {
  try {
    const { campaignId } = req.params;
    const { name, class: charClass, ancestry, hp_max } = req.body;

    if (!name?.trim()) {
      res.status(400).json({ error: "Character name is required" });
      return;
    }

    const userId = req.user?.userId ?? null;

    const [result] = await db.execute<ResultSetHeader>(
      `INSERT INTO characters
         (campaign_id, user_id, name, class, ancestry, hp_current, hp_max, color)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
      [
        campaignId,
        userId,
        name.trim(),
        charClass ?? "Fighter",
        ancestry ?? "Human",
        hp_max ?? 0,
        hp_max ?? 0,
        generateCharacterColor(),
      ]
    );
    const characterId = result.insertId;

    await Promise.all(
      DEFAULT_STATS.map((stat) =>
        db.execute(
          "INSERT INTO character_stats (character_id, stat_name, value) VALUES (?, ?, 10)",
          [characterId, stat]
        )
      )
    );

    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM characters WHERE id = ?",
      [characterId]
    );
    const enriched = {
      ...charRows[0],
      overrides: {},
      stats: DEFAULT_STATS.map((s) => ({ stat_name: s, value: 10 })),
      gear: [],
      talents: [],
    };

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(campaignId), "character:created", enriched);
    res.status(201).json(enriched);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// PATCH /api/characters/:id
router.patch("/:id", requireAuth, async (req, res) => {
  try {
    const { id } = req.params;

    const allowed = await canModifyCharacter(String(id), req.user!.userId);
    if (!allowed) {
      res.status(403).json({ error: "Not authorized to modify this character" });
      return;
    }

    const allowedFields = [
      "name", "class", "ancestry", "level", "xp", "hp_current", "hp_max",
      "ac", "alignment", "title", "notes", "background", "deity", "languages",
      "gp", "sp", "cp", "gear_slots_max", "overrides", "color", "luck_token",
      "torch_lit_at",
    ];

    const updates: string[] = [];
    const values: unknown[] = [];

    for (const field of allowedFields) {
      if (req.body[field] !== undefined) {
        updates.push(`${field} = ?`);
        const val = req.body[field];
        values.push(typeof val === "object" && val !== null ? JSON.stringify(val) : val);
      }
    }

    if (updates.length === 0) {
      res.status(400).json({ error: "No valid fields to update" });
      return;
    }

    values.push(id);
    const [updateResult] = await db.execute<ResultSetHeader>(`UPDATE characters SET ${updates.join(", ")} WHERE id = ?`, values as ExecuteValues);
    if (updateResult.affectedRows === 0) {
      res.status(404).json({ error: "Character not found" });
      return;
    }

    const [rows] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM characters WHERE id = ?",
      [id]
    );

    const enriched = {
      ...rows[0],
      overrides: parseJson(rows[0].overrides),
    };

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(rows[0].campaign_id), "character:updated", enriched);
    res.json(enriched);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// DELETE /api/characters/:id
router.delete("/:id", requireAuth, async (req, res) => {
  try {
    const [rows] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM characters WHERE id = ?",
      [req.params.id]
    );
    if (rows.length === 0) {
      res.status(404).json({ error: "Character not found" });
      return;
    }

    const allowed = await canModifyCharacter(String(req.params.id), req.user!.userId);
    if (!allowed) {
      res.status(403).json({ error: "Not authorized to delete this character" });
      return;
    }

    await db.execute("DELETE FROM characters WHERE id = ?", [req.params.id]);

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(rows[0].campaign_id), "character:deleted", {
      id: Number(req.params.id),
    });
    res.status(204).end();
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// PATCH /api/characters/:id/stats/:statName
router.patch("/:id/stats/:statName", async (req, res) => {
  try {
    const { id, statName } = req.params;
    const { value } = req.body;
    const upper = statName.toUpperCase();

    if (!DEFAULT_STATS.includes(upper)) {
      res.status(400).json({ error: "Invalid stat name" });
      return;
    }

    await db.execute(
      "UPDATE character_stats SET value = ? WHERE character_id = ? AND stat_name = ?",
      [value, id, upper]
    );

    const [rows] = await db.execute<RowDataPacket[]>(
      "SELECT campaign_id FROM characters WHERE id = ?",
      [id]
    );
    if (rows.length === 0) {
      res.status(404).json({ error: "Character not found" });
      return;
    }

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(rows[0].campaign_id), "stat:updated", {
      characterId: Number(id),
      statName: upper,
      value,
    });
    res.json({ characterId: Number(id), statName: upper, value });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// POST /api/characters/:id/gear
router.post("/:id/gear", async (req, res) => {
  try {
    const { id } = req.params;
    const { name, type, slot_count, properties, effects, game_item_id } = req.body;

    if (!name?.trim()) {
      res.status(400).json({ error: "Gear name is required" });
      return;
    }

    const [result] = await db.execute<ResultSetHeader>(
      `INSERT INTO character_gear
         (character_id, name, type, slot_count, properties, effects, game_item_id)
       VALUES (?, ?, ?, ?, ?, ?, ?)`,
      [
        id, name.trim(), type ?? "gear", slot_count ?? 1,
        JSON.stringify(properties ?? {}),
        JSON.stringify(effects ?? {}),
        game_item_id ?? null,
      ]
    );

    const [gearRows] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM character_gear WHERE id = ?",
      [result.insertId]
    );
    const gear = {
      ...gearRows[0],
      properties: parseJson(gearRows[0].properties),
      effects: parseJson(gearRows[0].effects),
    };

    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT campaign_id FROM characters WHERE id = ?",
      [id]
    );
    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(charRows[0].campaign_id), "gear:added", {
      characterId: Number(id),
      gear,
    });
    res.status(201).json(gear);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// DELETE /api/characters/:id/gear/:gearId
router.delete("/:id/gear/:gearId", async (req, res) => {
  try {
    const { id, gearId } = req.params;
    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT campaign_id FROM characters WHERE id = ?",
      [id]
    );
    if (charRows.length === 0) {
      res.status(404).json({ error: "Character not found" });
      return;
    }

    const [result] = await db.execute<ResultSetHeader>(
      "DELETE FROM character_gear WHERE id = ? AND character_id = ?",
      [gearId, id]
    );
    if (result.affectedRows === 0) {
      res.status(404).json({ error: "Gear not found" });
      return;
    }

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(charRows[0].campaign_id), "gear:removed", {
      characterId: Number(id),
      gearId: Number(gearId),
    });
    res.status(204).end();
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// POST /api/characters/:id/talents
router.post("/:id/talents", async (req, res) => {
  try {
    const { id } = req.params;
    const { name, description, effect, game_talent_id } = req.body;

    if (!name?.trim()) {
      res.status(400).json({ error: "Talent name is required" });
      return;
    }

    const [result] = await db.execute<ResultSetHeader>(
      `INSERT INTO character_talents
         (character_id, name, description, effect, game_talent_id)
       VALUES (?, ?, ?, ?, ?)`,
      [id, name.trim(), description ?? "", JSON.stringify(effect ?? {}), game_talent_id ?? null]
    );

    const [talentRows] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM character_talents WHERE id = ?",
      [result.insertId]
    );
    const talent = { ...talentRows[0], effect: parseJson(talentRows[0].effect) };

    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT campaign_id FROM characters WHERE id = ?",
      [id]
    );
    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(charRows[0].campaign_id), "talent:added", {
      characterId: Number(id),
      talent,
    });
    res.status(201).json(talent);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// DELETE /api/characters/:id/talents/:talentId
router.delete("/:id/talents/:talentId", async (req, res) => {
  try {
    const { id, talentId } = req.params;
    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT campaign_id FROM characters WHERE id = ?",
      [id]
    );
    if (charRows.length === 0) {
      res.status(404).json({ error: "Character not found" });
      return;
    }

    const [result] = await db.execute<ResultSetHeader>(
      "DELETE FROM character_talents WHERE id = ? AND character_id = ?",
      [talentId, id]
    );
    if (result.affectedRows === 0) {
      res.status(404).json({ error: "Talent not found" });
      return;
    }

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(charRows[0].campaign_id), "talent:removed", {
      characterId: Number(id),
      talentId: Number(talentId),
    });
    res.status(204).end();
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

export default router;
