import { parseLlmJson, ensureArray, flattenValue } from "../lib/llm-parse";
import { useMemo, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { addToLore } from "../lib/lore";
import { useToast } from "./Toast";
import { addGeneration, extractTitle, type Generation } from "../lib/generations";
import { usePrefillEffect } from "../lib/usePrefill";
import { bus, Events, type AddCombatantsPayload } from "../lib/bus";
import { encounterBudget, difficultyForXp, DIFFICULTY_COLOR, type Difficulty } from "../lib/encounter-budget";

interface Encounter {
  monsters: string[];
  terrain: string;
  difficulty: string;
  loot: string;
}

interface Props {
  prefill?: Generation | null;
  onPrefillConsumed?: () => void;
}

// ponytail: 5e terrains with icons. The plan called for 12; we now have
// 12 (added planar, haunted, ship, siege to the original 8).
const TERRAINS: { value: string; icon: string }[] = [
  { value: "Forest", icon: "🌲" },
  { value: "Dungeon", icon: "🏰" },
  { value: "Urban", icon: "🏛" },
  { value: "Mountain", icon: "⛰" },
  { value: "Desert", icon: "🏜" },
  { value: "Swamp", icon: "🌫" },
  { value: "Coastal", icon: "🌊" },
  { value: "Underdark", icon: "🕳" },
  { value: "Planar", icon: "✈" },
  { value: "Haunted", icon: "🏚" },
  { value: "Ship", icon: "🚢" },
  { value: "Siege", icon: "🏰" },
];

// ponytail: parse "3x Goblin Scouts" or "2 bandits" out of the LLM's monster
// string. Returns the count + the cleaned name. If no count is found, defaults
// to 1. Used when sending to the Initiative Tracker.
function parseMonsterLine(raw: string): { name: string; count: number } {
  const m = raw.match(/^\s*(\d+)\s*[xX*]?\s+(.+)$/);
  if (m) {
    return { count: Math.max(1, parseInt(m[1], 10)), name: m[2].trim() };
  }
  return { count: 1, name: raw.trim() };
}

// ponytail: rough HP estimate by CR. Not strictly DMG-correct, but it gives
// the DM a starting point that they can edit on the Initiative Tracker. The
// table is from a synthesis of DMG averages; the DM is expected to override
// per monster. "—" for unknown CR.
const CR_TO_HP: Record<string, number> = {
  "0": 4,
  "0.125": 9,
  "0.25": 17,
  "0.5": 28,
  "1": 39,
  "2": 49,
  "3": 64,
  "4": 84,
  "5": 109,
  "6": 133,
  "7": 156,
  "8": 178,
  "9": 200,
  "10": 222,
  "11": 244,
  "12": 267,
  "13": 289,
  "14": 311,
  "15": 333,
  "16": 356,
  "17": 378,
  "18": 400,
  "19": 422,
  "20": 444,
  "21": 467,
  "22": 489,
  "23": 511,
  "24": 533,
  "25": 556,
  "26": 578,
  "27": 600,
  "28": 622,
  "29": 644,
  "30": 667,
};

function hpForCr(cr: string | null | undefined): number {
  if (!cr) return 30; // ponytail: default to ~CR 1 HP if no CR known.
  const hit = CR_TO_HP[cr.trim()];
  return hit ?? 30;
}

// ponytail: very lightweight CR detection from a monster name. Looks for
// common ordinals ("CR 5"), trailing " (CR 1/4)" annotations, or specific
// monsters. The DM can edit the HP on the Initiative Tracker once imported.
const KNOWN_CR: Record<string, string> = {
  goblin: "0.25",
  "goblin scout": "0.25",
  hobgoblin: "0.5",
  orc: "0.5",
  "orc warrior": "1",
  kobold: "0.125",
  bandit: "0.125",
  "bandit captain": "2",
  "bugbear chief": "3",
  bugbear: "1",
  gnoll: "0.5",
  "dire wolf": "1",
  wolf: "0.25",
  skeleton: "0.25",
  zombie: "0.25",
  ghoul: "1",
  ghast: "2",
  wight: "3",
  ogre: "2",
  troll: "5",
  "young red dragon": "10",
  "adult red dragon": "17",
  "ancient red dragon": "24",
  manticore: "3",
  griffon: "2",
  wyvern: "6",
  "ogre chief": "3",
  shaman: "2",
};

function crForName(name: string): string | null {
  const lower = name.toLowerCase();
  if (KNOWN_CR[lower]) return KNOWN_CR[lower];
  for (const [key, cr] of Object.entries(KNOWN_CR)) {
    if (lower.includes(key)) return cr;
  }
  // Look for trailing "(CR X)" or "CR 1/2" annotations.
  const m = name.match(/\(?CR\s*(\d+\/\d+|\d+)\)?/i);
  if (m) return m[1];
  return null;
}

export function EncounterBuilder({ prefill, onPrefillConsumed }: Props = {}) {
  const [partyLevel, setPartyLevel] = useState(5);
  const [partySize, setPartySize] = useState(4);
  const [terrain, setTerrain] = useState("Forest");
  const [encounter, setEncounter] = useState<Encounter | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
  const { addToast } = useToast();

  usePrefillEffect(prefill ?? null, "encounter", () => onPrefillConsumed?.(), (g) => {
    const parsed = parseLlmJson<Encounter>(g.data);
    if (parsed) {
      setEncounter(parsed);
      addToast(`Loaded "${g.title}" from history`, "info");
    }
  });

  const budget = useMemo(
    () => encounterBudget(partyLevel, partySize),
    [partyLevel, partySize],
  );

  // ponytail: extract a 1-20 XP estimate from the LLM's "3x CR-1" or similar
  // wording. The LLM doesn't return CR directly, so we sniff the monster
  // names against our small table. If we can't, we fall back to a rough
  // estimate by difficulty and surface the uncertainty in the UI.
  const computedXp = useMemo(() => {
    if (!encounter) return 0;
    let total = 0;
    for (const raw of ensureArray(encounter.monsters)) {
      const { count, name } = parseMonsterLine(flattenValue(raw));
      const cr = crForName(name);
      if (cr) {
        // ponytail: CR to XP per DMG. 1/4 = 50, 1/2 = 100, 1 = 200, etc.
        const xp = CR_TO_XP[cr] ?? 0;
        total += xp * count;
      } else {
        total += 50 * count; // fallback: assume ~CR 1/4 average
      }
    }
    return total;
  }, [encounter]);

  const difficulty: Difficulty | null = encounter
    ? difficultyForXp(computedXp, budget)
    : null;

  async function generate() {
    setLoading(true);
    setError("");
    setEncounter(null);
    try {
      const result = await invoke<string>("generate", {
        req: {
          prompt: `Generate a D&D 5e encounter for a party of ${partySize} level-${partyLevel} characters in a ${terrain.toLowerCase()} setting.

Provide the response as a JSON object with exactly these keys:
- "monsters": an array of 2-4 monster descriptions with quantities (e.g. "3x Goblin Scouts") — each must be a string, not an object
- "terrain": a brief terrain description with environmental features
- "difficulty": one of Easy/Medium/Hard/Deadly
- "loot": a brief treasure description

IMPORTANT: Return ONLY a raw JSON object. NO markdown fences, NO code blocks, NO extra text. Start with { and end with }.`,
          system: "You are a D&D encounter designer. You MUST respond with ONLY valid JSON. No markdown fences, no code blocks, no explanation. Just the JSON object.",
          temperature: 0.9,
          max_tokens: 400,
        },
      });
      const parsed = parseLlmJson<Encounter>(result);
      if (parsed) {
        setEncounter(parsed);
        addToast("Encounter generated", "success");
        const title = extractTitle("encounter", result, `${parsed.difficulty ?? "?"} in ${terrain}`);
        const source = `Party of ${partySize} lvl ${partyLevel} in ${terrain}`;
        void addGeneration({ kind: "encounter", title, data: result, source });
        const monsters = ensureArray(parsed.monsters).map(flattenValue).join(", ");
        void addToLore(
          `Encounter: ${parsed.difficulty || "?"} in ${terrain}`,
          `Encounter (party of ${partySize} level-${partyLevel}) in ${terrain}. ${monsters}\nTerrain: ${flattenValue(parsed.terrain)}\nDifficulty: ${flattenValue(parsed.difficulty)}\nLoot: ${flattenValue(parsed.loot)}`,
        );
      } else {
        setError("Could not parse LLM response. Try again or check your LLM connection.");
        addToast("LLM returned an unparseable response", "error");
      }
    } catch (e) {
      setError(String(e));
      addToast(`Encounter generation failed: ${e}`, "error");
    }
    setLoading(false);
  }

  function sendToInitiative() {
    if (!encounter) return;
    const combatants = ensureArray(encounter.monsters).flatMap((raw) => {
      const { name, count } = parseMonsterLine(flattenValue(raw));
      const cr = crForName(name);
      const hp = hpForCr(cr);
      return Array.from({ length: count }, () => ({ name, hp }));
    });
    if (combatants.length === 0) {
      addToast("No monsters to send", "warning");
      return;
    }
    const payload: AddCombatantsPayload = {
      combatants,
      source: `${encounter.difficulty ?? "?"} in ${terrain}`,
    };
    bus.emit(Events.AddCombatants, payload);
    addToast(
      `Sent ${combatants.length} combatant${combatants.length === 1 ? "" : "s"} to Initiative Tracker — switch to Initiative to roll`,
      "success",
    );
  }

  return (
    <div className="flex flex-col gap-2 h-full overflow-y-auto">
      <div className="grid grid-cols-2 gap-2">
        <div className="flex flex-col gap-1">
          <label className="text-[var(--color-text-secondary)] text-[10px] font-medium">Party Level</label>
          <input
            type="number"
            min={1}
            max={20}
            value={partyLevel}
            onChange={(e) => setPartyLevel(parseInt(e.target.value) || 1)}
            className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-2 py-1 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs font-mono"
          />
        </div>
        <div className="flex flex-col gap-1">
          <label className="text-[var(--color-text-secondary)] text-[10px] font-medium">Party Size</label>
          <input
            type="number"
            min={1}
            max={10}
            value={partySize}
            onChange={(e) => setPartySize(parseInt(e.target.value) || 1)}
            className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-2 py-1 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs font-mono"
          />
        </div>
      </div>

      <div className="flex flex-col gap-1">
        <label className="text-[var(--color-text-secondary)] text-[10px] font-medium">Terrain</label>
        <select
          value={terrain}
          onChange={(e) => setTerrain(e.target.value)}
          className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-2 py-1.5 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs cursor-pointer"
        >
          {TERRAINS.map((t) => (
            <option key={t.value} value={t.value}>
              {t.icon} {t.value}
            </option>
          ))}
        </select>
      </div>

      {/* Budget readout — shows the DM what XP thresholds they're working with. */}
      <div className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-subtle)] p-2 text-[10px] font-mono">
        <div className="flex justify-between text-[var(--color-text-dim)] mb-1">
          <span>Budget (party of {partySize} lvl {partyLevel})</span>
        </div>
        <div className="grid grid-cols-4 gap-1 text-center">
          <BudgetCell label="Easy" value={budget.easy} color={DIFFICULTY_COLOR.Easy} active={difficulty === "Easy"} />
          <BudgetCell label="Med" value={budget.medium} color={DIFFICULTY_COLOR.Medium} active={difficulty === "Medium"} />
          <BudgetCell label="Hard" value={budget.hard} color={DIFFICULTY_COLOR.Hard} active={difficulty === "Hard"} />
          <BudgetCell label="Dead" value={budget.deadly} color={DIFFICULTY_COLOR.Deadly} active={difficulty === "Deadly"} />
        </div>
      </div>

      <button
        onClick={generate}
        disabled={loading}
        className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-xs font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer disabled:opacity-50"
      >
        {loading ? "⚔ Generating…" : "⚔ Generate Encounter"}
      </button>

      {error && (
        <div className="rounded-lg bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/30 p-2 text-[var(--color-danger)] text-xs">
          {error}
        </div>
      )}

      {encounter && (
        <div className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] p-3 flex flex-col gap-2">
          <div className="flex items-center justify-between">
            <span
              className="font-heading text-xs font-semibold rounded-full px-2 py-0.5"
              style={{
                color: difficulty ? DIFFICULTY_COLOR[difficulty] : "var(--color-gold-bright)",
                borderColor: difficulty ? DIFFICULTY_COLOR[difficulty] : "var(--color-gold-bright)",
                borderWidth: 1,
              }}
            >
              {difficulty ?? encounter.difficulty} Encounter
            </span>
            <span className="text-[var(--color-text-dim)] text-[10px]">
              ~{computedXp} XP · L{partyLevel}×{partySize}
            </span>
          </div>

          <div>
            <span className="text-[var(--color-text-secondary)] text-[10px] font-medium">Monsters</span>
            <ul className="list-disc list-inside text-[var(--color-text-primary)] text-xs mt-0.5">
              {ensureArray(encounter.monsters).map((m, i) => <li key={i}>{flattenValue(m)}</li>)}
            </ul>
          </div>

          <div>
            <span className="text-[var(--color-text-secondary)] text-[10px] font-medium">Terrain Features</span>
            <p className="text-[var(--color-text-primary)] text-xs mt-0.5">{encounter.terrain}</p>
          </div>

          <div>
            <span className="text-[var(--color-text-secondary)] text-[10px] font-medium">Loot</span>
            <p className="text-[var(--color-gold-bright)] text-xs mt-0.5">{encounter.loot}</p>
          </div>

          <button
            onClick={sendToInitiative}
            className="mt-1 rounded-lg bg-[var(--color-bg-card)] border border-[var(--color-border-glass)] text-[var(--color-gold-bright)] px-3 py-1.5 text-xs font-semibold hover:border-[var(--color-gold-bright)] transition-colors cursor-pointer flex items-center justify-center gap-1.5"
            title="Add these monsters to the Initiative Tracker"
          >
            ⚔ Send to Initiative Tracker
          </button>
        </div>
      )}
    </div>
  );
}

function BudgetCell({
  label,
  value,
  color,
  active,
}: {
  label: string;
  value: number;
  color: string;
  active: boolean;
}) {
  return (
    <div
      className={`rounded px-1 py-0.5 transition-colors ${
        active ? "bg-[var(--color-bg-card)]" : ""
      }`}
      style={{ color: active ? color : "var(--color-text-dim)" }}
    >
      <div className="text-[9px] uppercase tracking-wider">{label}</div>
      <div className="font-bold">{value}</div>
    </div>
  );
}

// ponytail: CR → XP. From the DMG encounter-building table. Used to estimate
// the XP total of a generated encounter when computing difficulty.
const CR_TO_XP: Record<string, number> = {
  "0": 10,
  "0.125": 25,
  "0.25": 50,
  "0.5": 100,
  "1": 200,
  "2": 450,
  "3": 700,
  "4": 1100,
  "5": 1800,
  "6": 2300,
  "7": 2900,
  "8": 3900,
  "9": 5000,
  "10": 5900,
  "11": 7200,
  "12": 8400,
  "13": 10000,
  "14": 11500,
  "15": 13000,
  "16": 15000,
  "17": 18000,
  "18": 20000,
  "19": 22000,
  "20": 25000,
  "21": 33000,
  "22": 41000,
  "23": 50000,
  "24": 62000,
  "25": 75000,
  "26": 90000,
  "27": 105000,
  "28": 120000,
  "29": 135000,
  "30": 155000,
};
