import { useState, useCallback, useMemo } from "react";
import { usePersistentState } from "../lib/usePersistentState";
import { useToast } from "./Toast";

interface TableEntry {
  min: number;
  max: number;
  result: string;
}

interface RandomTable {
  name: string;
  dice: string;
  entries: TableEntry[];
  /** true for user-created tables (editable / deletable). */
  custom?: boolean;
}

const BUILTIN_TABLES: RandomTable[] = [
  {
    name: "Tavern Names",
    dice: "1d20",
    entries: [
      { min: 1, max: 2, result: "The Rusty Tankard" },
      { min: 3, max: 4, result: "The Sleeping Dragon" },
      { min: 5, max: 6, result: "The Gilded Flask" },
      { min: 7, max: 8, result: "The Blind Beholder" },
      { min: 9, max: 10, result: "The Copper Kettle" },
      { min: 11, max: 12, result: "The Prancing Pony" },
      { min: 13, max: 14, result: "The Wandering Wyvern" },
      { min: 15, max: 16, result: "The Blacksmith's Rest" },
      { min: 17, max: 18, result: "The Tipsy Goblin" },
      { min: 19, max: 20, result: "The Moonlit Hearth" },
    ],
  },
  {
    name: "Weather",
    dice: "1d12",
    entries: [
      { min: 1, max: 2, result: "Clear skies, warm breeze" },
      { min: 3, max: 4, result: "Overcast, cool" },
      { min: 5, max: 6, result: "Light rain, fog in the morning" },
      { min: 7, max: 8, result: "Thunderstorm, heavy rain" },
      { min: 9, max: 10, result: "Strong winds, debris flying" },
      { min: 11, max: 12, result: "Unnatural: green-tinted fog or blood rain" },
    ],
  },
  {
    name: "NPC Quirks",
    dice: "1d10",
    entries: [
      { min: 1, max: 1, result: "Speaks in rhyming couplets" },
      { min: 2, max: 2, result: "Constantly checks over their shoulder" },
      { min: 3, max: 3, result: "Has a pet mouse on their shoulder" },
      { min: 4, max: 4, result: "Insists on being called 'Your Eminence'" },
      { min: 5, max: 5, result: "Smells strongly of cinnamon" },
      { min: 6, max: 6, result: "Never makes eye contact" },
      { min: 7, max: 7, result: "Finishes everyone else's sentences" },
      { min: 8, max: 8, result: "Carries a broken sword and won't say why" },
      { min: 9, max: 9, result: "Laughs at inappropriate moments" },
      { min: 10, max: 10, result: "Writes everything down in a tiny notebook" },
    ],
  },
  {
    name: "NPC Names",
    dice: "1d20",
    entries: [
      { min: 1, max: 2, result: "Aldric Holloway" },
      { min: 3, max: 4, result: "Mira Copperkettle" },
      { min: 5, max: 6, result: "Theron Ashwood" },
      { min: 7, max: 8, result: "Sable Nightwhisper" },
      { min: 9, max: 10, result: "Brom Stonefist" },
      { min: 11, max: 12, result: "Elara Dawnbrook" },
      { min: 13, max: 14, result: "Vaelris Moonsong" },
      { min: 15, max: 16, result: "Greta Bracken" },
      { min: 17, max: 18, result: "Corvus Vane" },
      { min: 19, max: 20, result: "Isolde Ferrow" },
    ],
  },
  {
    name: "Traps",
    dice: "1d12",
    entries: [
      { min: 1, max: 2, result: "Pressure plates release a poison dart volley" },
      { min: 3, max: 4, result: "Floor tilts into a spiked pit" },
      { min: 5, max: 6, result: "Rune flares: failing save teleports the victim 30ft up" },
      { min: 7, max: 8, result: "A false door slams and locks, flooding the room with water" },
      { min: 9, max: 10, result: "Gas vent: everyone in 10ft saves or falls unconscious" },
      { min: 11, max: 12, result: "Sculpted eyes animate and cast Hold Person" },
    ],
  },
  {
    name: "Dreams",
    dice: "1d10",
    entries: [
      { min: 1, max: 2, result: "A song you almost remember teaching you a secret" },
      { min: 3, max: 4, result: "You fall through endless clouds and wake before landing" },
      { min: 5, max: 6, result: "A dead friend warns you of a coming betrayal" },
      { min: 7, max: 8, result: "You are the dragon, and the party are the monsters" },
      { min: 9, max: 10, result: "A faceless figure offers you a gift you can't refuse" },
    ],
  },
  {
    name: "Dungeon Encounters",
    dice: "1d12",
    entries: [
      { min: 1, max: 2, result: "1d4 goblins arguing over found loot" },
      { min: 3, max: 4, result: "A skeleton polishing its own skull" },
      { min: 5, max: 6, result: "A gelatinous cube drifting down the corridor" },
      { min: 7, max: 8, result: "Cultists mid-ritual around a dark pool" },
      { min: 9, max: 10, result: "A wounded adventurer begging for water" },
      { min: 11, max: 12, result: "A mimic chest that's already eaten someone" },
    ],
  },
  {
    name: "Wilderness Encounters",
    dice: "1d12",
    entries: [
      { min: 1, max: 2, result: "A deer that watches without fleeing" },
      { min: 3, max: 4, result: "A traveler's shrine with a fresh offering" },
      { min: 5, max: 6, result: "Wolf tracks circling the party's camp" },
      { min: 7, max: 8, result: "A group of foraging halflings" },
      { min: 9, max: 10, result: "An old battlefield, bones half-buried" },
      { min: 11, max: 12, result: "A fae ring of mushrooms humming faintly" },
    ],
  },
  {
    name: "Urban Encounters",
    dice: "1d10",
    entries: [
      { min: 1, max: 2, result: "A pickpocket bumps a party member" },
      { min: 3, max: 4, result: "A street preacher predicts doom" },
      { min: 5, max: 6, result: "A merchant chases a thief through the crowd" },
      { min: 7, max: 8, result: "A noble's carriage blocks the street, demanding right of way" },
      { min: 9, max: 10, result: "A wanted poster bearing a party member's likeness" },
    ],
  },
  {
    name: "Inn Names",
    dice: "1d12",
    entries: [
      { min: 1, max: 2, result: "The Crooked Candle" },
      { min: 3, max: 4, result: "The Sour Barrel" },
      { min: 5, max: 6, result: "The Drowned Cat" },
      { min: 7, max: 8, result: "The Honest Thief" },
      { min: 9, max: 10, result: "The Last Lantern" },
      { min: 11, max: 12, result: "The Brass Sow" },
    ],
  },
  {
    name: "Ship Names",
    dice: "1d10",
    entries: [
      { min: 1, max: 2, result: "The Salt-Scarred" },
      { min: 3, max: 4, result: "Drifting Promise" },
      { min: 5, max: 6, result: "The Widow's Joy" },
      { min: 7, max: 8, result: "Tide Liar" },
      { min: 9, max: 10, result: "The Gilded Hull" },
    ],
  },
  {
    name: "Caravan Cargo",
    dice: "1d8",
    entries: [
      { min: 1, max: 1, result: "Casks of sour wine" },
      { min: 2, max: 2, result: "Bolts of dyed wool" },
      { min: 3, max: 3, result: "A caged, very unhappy owlbear cub" },
      { min: 4, max: 4, result: "Iron ingots, smudged with old runes" },
      { min: 5, max: 5, result: "A sealed coffin, labeled 'do not open'" },
      { min: 6, max: 6, result: "Crates of fragile glassware" },
      { min: 7, max: 7, result: "Sacks of exotic spice" },
      { min: 8, max: 8, result: "A peasant family fleeing trouble" },
    ],
  },
  {
    name: "Treasure Hoards",
    dice: "1d12",
    entries: [
      { min: 1, max: 3, result: "500 gp, a silver chalice, and a scroll of Fireball" },
      { min: 4, max: 6, result: "2,000 gp, a +1 weapon, and a worn spellbook" },
      { min: 7, max: 9, result: "A gem-studded idol worth 1,500 gp and a cursed ring" },
      { min: 10, max: 10, result: "A deck of many things (the DMG kind)" },
      { min: 11, max: 12, result: "A map to a dragon's hoard and a single ancient coin" },
    ],
  },
  {
    name: "Treasure (Low)",
    dice: "1d6",
    entries: [
      { min: 1, max: 1, result: "5d6 copper pieces" },
      { min: 2, max: 2, result: "2d6 silver pieces and a garnet ring" },
      { min: 3, max: 3, result: "1d6 gold pieces and a potion of healing" },
      { min: 4, max: 4, result: "A scroll with a cantrip and 3d6 silver" },
      { min: 5, max: 5, result: "A mundane item of unusual craftsmanship" },
      { min: 6, max: 6, result: "A map fragment leading to a nearby ruin" },
    ],
  },
];

function rollDice(notation: string): number {
  const match = notation.toLowerCase().match(/^(\d+)d(\d+)([+-]\d+)?$/);
  if (!match) return Math.floor(Math.random() * 20) + 1;
  const count = parseInt(match[1]);
  const sides = parseInt(match[2]);
  const modifier = parseInt(match[3] || "0");
  let total = 0;
  for (let i = 0; i < count; i++) {
    total += Math.floor(Math.random() * sides) + 1;
  }
  return total + modifier;
}

// ponytail: total faces of the table's dice (e.g. "1d20" → 20). Used to show
// per-entry weights and flag gaps where a roll would hit "No result".
function diceSides(notation: string): number {
  const m = notation.toLowerCase().match(/^(\d+)d(\d+)/);
  return m ? parseInt(m[2]) * parseInt(m[1]) : 20;
}

// ponytail: parse a pasted markdown/plain list into table entries. Accepts
// "1d20" style dice on a header line, or defaults to 1dN where N = entry count.
// Each non-empty line becomes one entry; ranges like "3-4 result" are honoured.
function parseMarkdownTable(raw: string): { name: string; dice: string; entries: TableEntry[] } | null {
  const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
  if (lines.length === 0) return null;
  let name = "Imported Table";
  let dice = "";
  const entryLines: string[] = [];
  for (const l of lines) {
    const hdr = l.match(/^#{1,6}\s+(.+)$/);
    const diceMatch = l.match(/(\d+d\d+(?:[+-]\d+)?)/i);
    if (hdr) {
      name = hdr[1].trim();
      continue;
    }
    if (diceMatch && !dice && entryLines.length === 0) {
      dice = diceMatch[1];
      // keep the line if it also has a result, else it's a dice-only header
      if (l.replace(diceMatch[1], "").trim()) entryLines.push(l);
      continue;
    }
    entryLines.push(l);
  }
  if (entryLines.length === 0) return null;
  const entries: TableEntry[] = entryLines.map((l, i) => {
    // strip leading list markers / numbers
    const cleaned = l.replace(/^[-*+]\s+/, "").replace(/^\d+[.)]\s+/, "");
    const range = cleaned.match(/^(\d+)\s*[-–]\s*(\d+)\s+(.+)$/);
    if (range) {
      return { min: parseInt(range[1]), max: parseInt(range[2]), result: range[3].trim() };
    }
    // single leading number: "5 Gilded Flask" → entry 5
    const single = cleaned.match(/^(\d+)\s+(.+)$/);
    if (single) {
      return { min: parseInt(single[1]), max: parseInt(single[1]), result: single[2].trim() };
    }
    return { min: i + 1, max: i + 1, result: cleaned };
  });
  if (!dice) {
    const max = entries.reduce((m, e) => Math.max(m, e.max), 0);
    dice = `1d${Math.max(2, max)}`;
  }
  return { name, dice, entries };
}

interface RollLog {
  tableName: string;
  roll: number;
  result: string;
}

export function RandomTables() {
  const [selected, setSelected] = useState(0);
  const [log, setLog] = useState<RollLog[]>([]);
  const [customRoll, setCustomRoll] = useState<number | null>(null);
  // ponytail: user-defined tables persist alongside builtins. Editing only
  // affects custom tables; builtins are read-only.
  const [customTables, setCustomTables] = usePersistentState<RandomTable[]>("tables.custom", []);
  const [editing, setEditing] = useState(false);
  const [importOpen, setImportOpen] = useState(false);
  const [importText, setImportText] = useState("");
  const [importName, setImportName] = useState("");
  const [importDice, setImportDice] = useState("");
  const { addToast } = useToast();

  const allTables = useMemo(() => [...BUILTIN_TABLES, ...customTables], [customTables]);
  const table = allTables[selected] ?? BUILTIN_TABLES[0];
  const isCustom = table.custom === true;
  // ponytail: weighted tables - count dice faces not covered by any entry.
  const sides = diceSides(table.dice);
  const coveredFaces = new Set<number>();
  for (const e of table.entries) for (let v = e.min; v <= e.max; v++) coveredFaces.add(v);
  const gaps = sides - coveredFaces.size;

  function exportTable() {
    const blob = new Blob([JSON.stringify(table, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `${table.name.toLowerCase().replace(/\s+/g, "-")}.json`;
    a.click();
    URL.revokeObjectURL(url);
  }

  const roll = useCallback(() => {
    const rollVal = rollDice(table.dice);
    const entry = table.entries.find((e) => rollVal >= e.min && rollVal <= e.max);
    const result = entry?.result ?? "No result found";
    setLog((prev) => [{ tableName: table.name, roll: rollVal, result }, ...prev].slice(0, 30));
    setCustomRoll(rollVal);
  }, [table]);

  function newCustomTable() {
    const t: RandomTable = {
      name: `My Table ${customTables.length + 1}`,
      dice: "1d6",
      custom: true,
      entries: [
        { min: 1, max: 1, result: "First result" },
        { min: 2, max: 2, result: "Second result" },
      ],
    };
    setCustomTables((prev) => [...prev, t]);
    setSelected(BUILTIN_TABLES.length + customTables.length);
    setEditing(true);
    addToast("New table created — edit it below", "info");
  }

  function updateCurrent(updater: (t: RandomTable) => RandomTable) {
    if (!isCustom) return;
    const idx = selected - BUILTIN_TABLES.length;
    setCustomTables((prev) => prev.map((t, i) => (i === idx ? updater(t) : t)));
  }

  function deleteCurrent() {
    if (!isCustom) return;
    const idx = selected - BUILTIN_TABLES.length;
    setCustomTables((prev) => prev.filter((_, i) => i !== idx));
    setSelected(0);
    setEditing(false);
    addToast("Table deleted", "info");
  }

  function doImport() {
    let parsed: { name: string; dice: string; entries: TableEntry[] } | null = null;
    if (importText.trim()) {
      parsed = parseMarkdownTable(importText);
      if (!parsed) {
        addToast("Couldn't parse the pasted table", "error");
        return;
      }
    } else if (importName.trim()) {
      parsed = { name: importName.trim(), dice: importDice.trim() || "1d6", entries: [] };
    }
    if (!parsed) return;
    const t: RandomTable = { ...parsed, custom: true };
    setCustomTables((prev) => [...prev, t]);
    setSelected(BUILTIN_TABLES.length + customTables.length);
    setImportOpen(false);
    setImportText("");
    setImportName("");
    setImportDice("");
    addToast(`Imported "${t.name}"`, "success");
  }

  return (
    <div className="flex flex-col gap-2 h-full">
      {/* Table selector */}
      <div className="flex gap-1.5">
        <select
          className="flex-1 min-w-0 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"
          value={selected}
          onChange={(e) => {
            setSelected(parseInt(e.target.value));
            setCustomRoll(null);
            setEditing(false);
          }}
        >
          {allTables.map((t, i) => (
            <option key={`${t.name}-${i}`} value={i}>
              {t.custom ? "✎ " : ""}{t.name} ({t.dice})
            </option>
          ))}
        </select>
        <button
          onClick={newCustomTable}
          className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-dim)] px-2 py-1.5 text-xs hover:text-[var(--color-gold-bright)] hover:border-[var(--color-gold-bright)] transition-colors cursor-pointer"
          title="New custom table"
        >
          +
        </button>
        <button
          onClick={() => setImportOpen((o) => !o)}
          className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-dim)] px-2 py-1.5 text-xs hover:text-[var(--color-gold-bright)] hover:border-[var(--color-gold-bright)] transition-colors cursor-pointer"
          title="Import a table from pasted Markdown"
        >
          ⬆
        </button>
      </div>

      {/* Import panel */}
      {importOpen && (
        <div className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] p-2 flex flex-col gap-1.5">
          <span className="text-[10px] text-[var(--color-text-secondary)] font-medium">Import table</span>
          <p className="text-[10px] text-[var(--color-text-dim)]">
            Paste a list — one result per line. A <code># Title</code> line sets the name, a line with dice (e.g. <code>1d20</code>) sets the dice. Lines like <code>3-4 result</code> set ranges.
          </p>
          <textarea
            className="rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] px-2 py-1.5 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs min-h-24 resize-y font-mono"
            value={importText}
            onChange={(e) => setImportText(e.target.value)}
            placeholder={"# Tavern Names\n1d20\n1-2 The Rusty Tankard\n3-4 The Sleeping Dragon"}
          />
          <div className="flex gap-1.5">
            <input
              className="flex-1 min-w-0 rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] px-2 py-1 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs"
              value={importName}
              onChange={(e) => setImportName(e.target.value)}
              placeholder="Name (optional, overrides #)"
            />
            <input
              className="w-20 rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] px-2 py-1 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs"
              value={importDice}
              onChange={(e) => setImportDice(e.target.value)}
              placeholder="1d20"
            />
            <button
              onClick={doImport}
              className="rounded bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-3 py-1 text-xs font-semibold cursor-pointer hover:bg-[var(--color-gold-muted)]"
            >
              Import
            </button>
          </div>
        </div>
      )}

      {/* Roll + export */}
      <div className="flex gap-2">
        <button
          onClick={roll}
          className="flex-1 rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer"
        >
          🎲 Roll {table.dice}
        </button>
        <button
          onClick={exportTable}
          className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-dim)] px-3 py-2 text-sm hover:text-[var(--color-gold-bright)] hover:border-[var(--color-gold-bright)] transition-colors cursor-pointer"
          title="Export this table as JSON"
        >
          ⬇
        </button>
        {isCustom && (
          <button
            onClick={() => setEditing((e) => !e)}
            className={`rounded-lg border px-3 py-2 text-sm cursor-pointer transition-colors ${
              editing
                ? "bg-[var(--color-gold-glow)] border-[var(--color-gold-bright)] text-[var(--color-gold-bright)]"
                : "bg-[var(--color-bg-surface)] border-[var(--color-border-glass)] text-[var(--color-text-dim)] hover:text-[var(--color-gold-bright)] hover:border-[var(--color-gold-bright)]"
            }`}
            title="Edit this custom table"
          >
            ✎
          </button>
        )}
        {isCustom && (
          <button
            onClick={deleteCurrent}
            className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-dim)] px-3 py-2 text-sm hover:text-[var(--color-danger)] hover:border-[var(--color-danger)] transition-colors cursor-pointer"
            title="Delete this custom table"
          >
            🗑
          </button>
        )}
      </div>

      {/* Editor for custom tables */}
      {isCustom && editing && (
        <div className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] p-2 flex flex-col gap-1.5">
          <div className="flex gap-1.5">
            <input
              className="flex-1 min-w-0 rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] px-2 py-1 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs"
              value={table.name}
              onChange={(e) => updateCurrent((t) => ({ ...t, name: e.target.value }))}
              placeholder="Table name"
            />
            <input
              className="w-20 rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] px-2 py-1 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs font-mono"
              value={table.dice}
              onChange={(e) => updateCurrent((t) => ({ ...t, dice: e.target.value }))}
              placeholder="1d20"
            />
          </div>
          <div className="flex flex-col gap-0.5 max-h-40 overflow-y-auto">
            {table.entries.map((e, i) => (
              <div key={i} className="flex gap-1 items-center">
                <input
                  type="number"
                  className="w-10 rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] px-1 py-1 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-[10px] font-mono"
                  value={e.min}
                  onChange={(ev) => updateCurrent((t) => ({ ...t, entries: t.entries.map((x, j) => j === i ? { ...x, min: parseInt(ev.target.value) || 0 } : x) }))}
                />
                <span className="text-[var(--color-text-dim)] text-[10px]">-</span>
                <input
                  type="number"
                  className="w-10 rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] px-1 py-1 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-[10px] font-mono"
                  value={e.max}
                  onChange={(ev) => updateCurrent((t) => ({ ...t, entries: t.entries.map((x, j) => j === i ? { ...x, max: parseInt(ev.target.value) || 0 } : x) }))}
                />
                <input
                  className="flex-1 min-w-0 rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] px-2 py-1 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs"
                  value={e.result}
                  onChange={(ev) => updateCurrent((t) => ({ ...t, entries: t.entries.map((x, j) => j === i ? { ...x, result: ev.target.value } : x) }))}
                />
                <button
                  onClick={() => updateCurrent((t) => ({ ...t, entries: t.entries.filter((_, j) => j !== i) }))}
                  className="text-[var(--color-text-dim)] hover:text-[var(--color-danger)] text-[10px] cursor-pointer shrink-0"
                  title="Remove entry"
                >
                  ×
                </button>
              </div>
            ))}
          </div>
          <button
            onClick={() => updateCurrent((t) => ({ ...t, entries: [...t.entries, { min: t.entries.length + 1, max: t.entries.length + 1, result: "" }] }))}
            className="rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] text-[var(--color-text-dim)] px-2 py-1 text-[10px] hover:text-[var(--color-gold-bright)] hover:border-[var(--color-gold-bright)] transition-colors cursor-pointer"
          >
            + entry
          </button>
        </div>
      )}

      {/* Result */}
      {customRoll !== null && (
        <div className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] p-3 text-center">
          <div className="font-mono text-2xl font-bold text-[var(--color-gold-bright)]">
            {customRoll}
          </div>
          <div className="text-[var(--color-text-primary)] text-sm mt-1">
            {table.entries.find((e) => customRoll >= e.min && customRoll <= e.max)?.result}
          </div>
        </div>
      )}

      {/* Table preview */}
      <div className="flex-1 overflow-y-auto">
        {gaps > 0 ? (
          <div className="text-[9px] text-[var(--color-danger)] mb-1">
            ! {gaps} of {sides} faces uncovered - some rolls hit No result.
          </div>
        ) : null}
        <div className="flex flex-col gap-0.5">
          {table.entries.map((e) => (
            <div
              key={`${e.min}-${e.max}`}
              className={`flex items-center gap-2 rounded px-2 py-0.5 text-xs ${
                customRoll !== null && customRoll >= e.min && customRoll <= e.max
                  ? "bg-[var(--color-gold-glow)] text-[var(--color-gold-bright)]"
                  : "text-[var(--color-text-secondary)]"
              }`}
            >
              <span className="font-mono w-8 text-right shrink-0">{e.min === e.max ? e.min : `${e.min}-${e.max}`}</span>
              <span className="truncate flex-1">{e.result}</span>
              {(() => { const s = diceSides(table.dice); const w = Math.round(((e.max - e.min + 1) / s) * 100); return w !== 100 ? <span className="font-mono text-[9px] text-[var(--color-text-dim)] shrink-0" title="Entry weight">{w}%</span> : null; })()}
            </div>
          ))}
        </div>
      </div>

      {/* Roll history */}
      {log.length > 0 && (
        <div className="border-t border-[var(--color-border-subtle)] pt-2 max-h-24 overflow-y-auto">
          <div className="flex items-center justify-between mb-1">
            <div className="text-[10px] font-medium text-[var(--color-text-dim)]">History</div>
            <button
              onClick={() => setLog([])}
              className="text-[10px] text-[var(--color-text-dim)] hover:text-[var(--color-danger)] cursor-pointer"
              title="Clear history"
            >
              clear
            </button>
          </div>
          {log.map((l, i) => (
            <div key={i} className="flex items-center gap-2 text-[10px] py-0.5">
              <span className="text-[var(--color-text-dim)]">{l.tableName}</span>
              <span className="font-mono text-[var(--color-gold-bright)]">{l.roll}</span>
              <span className="text-[var(--color-text-secondary)] truncate">{l.result}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}