import { useMemo, useRef, useState } from 'react';
import {
  Move, ScanEye, EyeOff, Ruler, Cloud, PenTool, Crosshair, Spline, CirclePlus,
  ChevronRight, type LucideIcon,
} from 'lucide-react';
import type { BattleMap, Campaign, MapDrawing, MapToken } from '@/lib/schemas';
import { mapsRepo, encountersRepo } from '@/lib/db/repositories';
import { nextTurn, previousTurn, applyDamage, applyHealing, updateCombatant, logEvent } from '@/lib/combat/engine';
import { newId } from '@/lib/ids';
import { useDebouncedCallback } from '@/lib/useDebouncedCallback';
import { fileToDataUrl, squareThumbnail } from '@/lib/img/resize';
import {
  brushCells, rectCells, polygonCells, circleCells, coneCells, lineCells, squareCells,
  gridDistance, toFeet, applyReveal, applyHide, revealAll, hideAll, worldToCell,
  blockingSegments, computeVisibleCells,
  type Point,
} from '@/lib/map';
import { useCharacters } from '@/features/characters/hooks';
import { useEncounters } from '@/features/combat/hooks';
import { Button } from '@/components/ui/Button';
import { Input, Select } from '@/components/ui/Input';
import { NumberField } from '@/components/ui/NumberField';
import { Modal } from '@/components/ui/Modal';
import { cn } from '@/lib/cn';
import { MapCanvas, type CanvasToken, type Overlay } from './MapCanvas';
import { TokenPalette, type TokenSpec } from './TokenPalette';

const TOKEN_COLORS = ['#d4af37', '#ef5350', '#66bb6a', '#64b5f6', '#ba68c8', '#ffa726', '#bdbdbd', '#000000'];
type Tool = 'move' | 'reveal' | 'hide' | 'measure' | 'aoe' | 'draw' | 'ping' | 'walls';

const TOOL_ICON: Record<Tool, LucideIcon> = {
  move: Move, reveal: ScanEye, hide: EyeOff, measure: Ruler,
  aoe: Cloud, draw: PenTool, ping: Crosshair, walls: Spline,
};

/** Distance from point p to segment a-b (image px). */
function distToSegment(p: Point, a: Point, b: Point): number {
  const dx = b.x - a.x, dy = b.y - a.y;
  const len2 = dx * dx + dy * dy;
  const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2));
  const cx = a.x + t * dx, cy = a.y + t * dy;
  return Math.hypot(p.x - cx, p.y - cy);
}
type FogShape = 'brush' | 'rect' | 'poly';
type AoeShape = 'circle' | 'cone' | 'line' | 'square';
type DrawKind = MapDrawing['kind'];

export function MapEditor({ map, campaign }: { map: BattleMap; campaign: Campaign }) {
  const [m, setM] = useState<BattleMap>(map);
  const characters = useCharacters(campaign.id);
  const encounters = useEncounters(campaign.id);
  const save = useDebouncedCallback((next: BattleMap) => void mapsRepo.save(next), 400);
  const update = (patch: Partial<BattleMap>) => setM((prev) => { const next = { ...prev, ...patch }; save(next); return next; });

  const [tool, setTool] = useState<Tool>('move');
  const [fogShape, setFogShape] = useState<FogShape>('brush');
  const [brush, setBrush] = useState(0);
  const [aoeShape, setAoeShape] = useState<AoeShape>('circle');
  const [aoeFeet, setAoeFeet] = useState(20);
  const [drawKind, setDrawKind] = useState<DrawKind>('freehand');
  const [drawColor, setDrawColor] = useState('#d4af37');
  const [gmDraw, setGmDraw] = useState(true);
  const [dims, setDims] = useState({ cols: 0, rows: 0 });
  const [editToken, setEditToken] = useState<string | null>(null);
  // Collapsed by default on small screens so the map gets the width.
  const [showPalette, setShowPalette] = useState(() => typeof window === 'undefined' || window.innerWidth >= 880);

  // transient interaction state
  const [overlay, setOverlay] = useState<Overlay>({});
  const anchor = useRef<Point | null>(null);
  const [poly, setPoly] = useState<Point[]>([]);
  const draftPts = useRef<Point[]>([]);

  const distMode = campaign.system === 'pf2e' ? 'pf2e' : '5e';
  const activeEncounter = useMemo(() => encounters.find((e) => e.status === 'active') ?? null, [encounters]);
  const activeCombatant = activeEncounter ? activeEncounter.combatants[activeEncounter.turnIndex] : undefined;
  const activeTokenId = activeCombatant ? m.tokens.find((t) => t.combatantId === activeCombatant.id)?.id : undefined;

  const damageCombatant = (id: string, amt: number) => {
    if (!activeEncounter) return;
    void encountersRepo.mutate(activeEncounter.id, (e) => {
      const cb = e.combatants.find((c) => c.id === id);
      if (!cb) return e;
      const updated = amt < 0 ? applyDamage(cb, -amt) : applyHealing(cb, amt);
      return logEvent(updateCombatant(e, id, { hp: updated.hp }), `${cb.name} ${amt < 0 ? `takes ${-amt} damage` : `heals ${amt}`}`);
    });
  };

  // Live HP/conditions: prefer an active-encounter combatant, then a linked
  // character, then the token's denormalized snapshot.
  const canvasTokens: CanvasToken[] = useMemo(() => m.tokens.map((t) => {
    const linkedChar = t.characterId ? characters.find((c) => c.id === t.characterId) : undefined;
    const linkedCb = t.combatantId && activeEncounter ? activeEncounter.combatants.find((c) => c.id === t.combatantId) : undefined;
    const image = t.image ?? linkedChar?.portrait;
    return {
      id: t.id, label: t.label || (linkedChar?.name ?? linkedCb?.name ?? ''), color: t.color, col: t.col, row: t.row,
      size: t.size, kind: t.kind,
      ...(image ? { image } : {}),
      hp: linkedCb?.hp ?? linkedChar?.hp ?? t.hp,
      conditions: linkedCb?.conditions ?? t.conditions,
    };
  }), [m.tokens, characters, activeEncounter]);

  const commitReveal = (cells: string[], reveal: boolean) =>
    update({ revealed: reveal ? applyReveal(m.revealed, cells) : applyHide(m.revealed, cells) });

  const onPointer = (phase: 'down' | 'move' | 'up' | 'hover', p: { point: Point; col: number; row: number }) => {
    // Live previews when no button is held.
    if (phase === 'hover') {
      if (tool === 'reveal' || tool === 'hide') {
        if (fogShape === 'brush') setOverlay({ brush: { col: p.col, row: p.row, size: brush } });
        else if (fogShape === 'poly') setOverlay({ poly: { points: poly, cursor: p.point }, ...(poly.length >= 2 ? { cells: polygonCells([...poly, p.point], gridSpec()) } : {}) });
      } else if (tool === 'aoe' && (aoeShape === 'circle' || aoeShape === 'square')) {
        setOverlay({ cells: aoeShape === 'circle' ? circleCells(p.point, aoeFeet, gridSpec()) : squareCells(p.point, aoeFeet, gridSpec()) });
      } else if (tool === 'walls') {
        setOverlay({ poly: { points: poly, cursor: p.point } });
      }
      return;
    }
    if (tool === 'walls') {
      if (phase === 'down') {
        if (toggleDoorNear(p.point)) return; // click a door to open/close it
        const next = [...poly, p.point];
        setPoly(next);
        setOverlay({ poly: { points: next, cursor: p.point } });
      }
      return;
    }
    const reveal = tool === 'reveal';
    if (tool === 'reveal' || tool === 'hide') {
      if (fogShape === 'brush') {
        if (phase !== 'up') { commitReveal(brushCells(p.col, p.row, brush, dims.cols, dims.rows), reveal); setOverlay({ brush: { col: p.col, row: p.row, size: brush } }); }
      } else if (fogShape === 'rect') {
        if (phase === 'down') anchor.current = p.point;
        else if (anchor.current) {
          const a = worldToCell(anchor.current, m.gridSize);
          const cells = rectCells(a, { col: p.col, row: p.row }, dims.cols, dims.rows);
          if (phase === 'move') setOverlay({ cells });
          else { commitReveal(cells, reveal); anchor.current = null; setOverlay({}); }
        }
      } else if (fogShape === 'poly') {
        if (phase === 'down') { const next = [...poly, p.point]; setPoly(next); setOverlay({ poly: { points: next, cursor: p.point }, ...(next.length >= 3 ? { cells: polygonCells(next, gridSpec()) } : {}) }); }
      }
      return;
    }
    if (tool === 'measure') {
      if (phase === 'down') anchor.current = p.point;
      else if (anchor.current) {
        const a = worldToCell(anchor.current, m.gridSize);
        const cells = gridDistance(a, { col: p.col, row: p.row }, distMode);
        setOverlay({ line: { a: anchor.current, b: p.point }, label: { at: p.point, text: `${toFeet(cells, m.gridUnit.feet)} ft` } });
        if (phase === 'up') anchor.current = null;
      }
      return;
    }
    if (tool === 'aoe') {
      if (phase === 'down') anchor.current = p.point;
      const origin = anchor.current ?? p.point;
      let cells: string[] = [];
      if (aoeShape === 'circle') cells = circleCells(p.point, aoeFeet, gridSpec());
      else if (aoeShape === 'square') cells = squareCells(p.point, aoeFeet, gridSpec());
      else if (aoeShape === 'cone') cells = coneCells(origin, p.point, aoeFeet, 53, gridSpec());
      else cells = lineCells(origin, p.point, 5, gridSpec());
      setOverlay({ cells });
      if (phase === 'up' && aoeShape !== 'circle' && aoeShape !== 'square') anchor.current = null;
      return;
    }
    if (tool === 'draw') {
      if (drawKind === 'text') {
        if (phase === 'down') {
          const text = window.prompt('Label text:')?.trim();
          if (text) update({ drawings: [...m.drawings, { id: newId(), kind: 'text', points: [p.point], color: drawColor, width: 4, text, gmOnly: gmDraw }] });
        }
        return;
      }
      if (phase === 'down') { draftPts.current = [p.point]; }
      else if (phase === 'move') {
        draftPts.current = drawKind === 'freehand' ? [...draftPts.current, p.point] : [draftPts.current[0]!, p.point];
        setOverlay({ draft: { id: 'draft', kind: drawKind, points: draftPts.current, color: drawColor, width: 3 } });
      } else {
        if (draftPts.current.length >= 2) update({ drawings: [...m.drawings, { id: newId(), kind: drawKind, points: draftPts.current, color: drawColor, width: 3, gmOnly: gmDraw }] });
        draftPts.current = []; setOverlay({});
      }
      return;
    }
    if (tool === 'ping' && phase === 'down') {
      const at = p.point;
      setOverlay({ cells: [`${p.col},${p.row}`] });
      window.setTimeout(() => setOverlay((o) => (o.cells?.[0] === `${p.col},${p.row}` ? {} : o)), 1500);
      void at;
    }
  };

  const gridSpec = () => ({ gridSize: m.gridSize, feetPerCell: m.gridUnit.feet, cols: dims.cols, rows: dims.rows });

  const finishPoly = () => { if (poly.length >= 3) commitReveal(polygonCells(poly, gridSpec()), tool === 'reveal'); setPoly([]); setOverlay({}); };

  // --- walls & dynamic vision ---
  const finishWall = () => { if (poly.length >= 2) update({ walls: [...m.walls, { id: newId(), points: poly }] }); setPoly([]); setOverlay({}); };
  const toggleDoorNear = (p: Point): boolean => {
    const thresh = m.gridSize * 0.5;
    let bestI = -1, best = thresh;
    m.doors.forEach((d, i) => { const dist = distToSegment(p, d.a, d.b); if (dist < best) { best = dist; bestI = i; } });
    if (bestI < 0) return false;
    update({ doors: m.doors.map((d, i) => (i === bestI ? { ...d, open: !d.open } : d)) });
    return true;
  };
  /** Cells visible to the party (pc tokens), honouring walls + closed doors. */
  const visionReveal = (tokens: MapToken[], revealed: string[]): string[] => {
    const viewers = tokens.filter((t) => t.kind === 'pc').map((t) => ({ x: (t.col + t.size / 2) * m.gridSize, y: (t.row + t.size / 2) * m.gridSize }));
    if (viewers.length === 0 || dims.cols === 0) return revealed;
    const segments = blockingSegments({ walls: m.walls, doors: m.doors });
    const radiusPx = m.sightRadiusFeet > 0 ? (m.sightRadiusFeet / m.gridUnit.feet) * m.gridSize : 0;
    const vis = computeVisibleCells({ viewers, segments, gridSize: m.gridSize, cols: dims.cols, rows: dims.rows, radiusPx });
    return applyReveal(revealed, [...vis]);
  };
  const revealFromParty = () => update({ fogEnabled: true, revealed: visionReveal(m.tokens, m.revealed) });

  // Clear transient hover previews when the cursor leaves, but keep an in-progress polygon.
  const onLeave = () => setOverlay(poly.length ? { poly: { points: poly } } : {});

  const addToken = () => update({ tokens: [...m.tokens, { id: newId(), label: String(m.tokens.length + 1), color: TOKEN_COLORS[m.tokens.length % TOKEN_COLORS.length]!, col: 0, row: 0, size: 1, kind: 'npc', conditions: [], gmOnly: false }] });
  const addTokenSpec = (spec: TokenSpec) => update({ tokens: [...m.tokens, { id: newId(), ...spec }] });
  const addTokenSpecs = (specs: TokenSpec[]) => { if (specs.length) update({ tokens: [...m.tokens, ...specs.map((s) => ({ id: newId(), ...s }))] }); };
  const patchToken = (id: string, patch: Partial<MapToken>) => update({ tokens: m.tokens.map((t) => (t.id === id ? { ...t, ...patch } : t)) });
  const moveToken = (id: string, col: number, row: number) => setM((prev) => {
    const tokens = prev.tokens.map((t) => (t.id === id ? { ...t, col, row } : t));
    const revealed = prev.dynamicVision ? visionReveal(tokens, prev.revealed) : prev.revealed;
    const next = { ...prev, tokens, revealed };
    save(next);
    return next;
  });
  const removeToken = (id: string) => update({ tokens: m.tokens.filter((t) => t.id !== id) });
  const onTokenIcon = async (id: string, file: File) => {
    const thumb = await squareThumbnail(await fileToDataUrl(file), 160);
    patchToken(id, { image: thumb });
  };
  const token = m.tokens.find((t) => t.id === editToken) ?? null;
  const tokenLinkedChar = token?.characterId ? characters.find((c) => c.id === token.characterId) : undefined;

  return (
    <div>
      <div className="grid gap-2" style={{ gridTemplateColumns: showPalette ? '210px minmax(0, 1fr)' : 'minmax(0, 1fr)' }}>
        {showPalette && (
          <TokenPalette characters={characters} encounters={encounters} existingTokens={m.tokens}
            onPlace={addTokenSpec} onPlaceMany={addTokenSpecs} onClose={() => setShowPalette(false)} />
        )}

        <div className="min-w-0">
          <div className="mb-2 flex flex-wrap items-center gap-2 rounded-xl border border-line bg-panel p-2 text-sm paper-grain">
            {!showPalette && (
              <Button size="sm" variant="ghost" onClick={() => setShowPalette(true)}>
                Tokens <ChevronRight size={14} aria-hidden />
              </Button>
            )}
            <Input className="h-8 max-w-36 font-display" value={m.name} onChange={(e) => update({ name: e.target.value })} aria-label="Map name" />
            <span className="mx-1 h-5 w-px bg-line" aria-hidden />
            {(['move', 'reveal', 'hide', 'measure', 'aoe', 'draw', 'ping', 'walls'] as Tool[]).map((t) => {
              const ToolIcon = TOOL_ICON[t];
              return (
                <Button key={t} size="sm" variant={tool === t ? 'primary' : 'secondary'} aria-pressed={tool === t}
                  onClick={() => { setTool(t); setPoly([]); setOverlay({}); anchor.current = null; }} className="capitalize">
                  <ToolIcon size={15} aria-hidden />
                  {t}
                </Button>
              );
            })}
          </div>

          {/* Contextual sub-toolbar */}
          <div className="mb-2 flex flex-wrap items-center gap-2 rounded-xl border border-line bg-panel-2 p-2 text-xs text-muted">
            <label className="flex items-center gap-1"><input type="checkbox" checked={m.showGrid} onChange={(e) => update({ showGrid: e.target.checked })} /> Grid</label>
            <label className="flex items-center gap-1"><input type="checkbox" checked={m.fogEnabled} onChange={(e) => update({ fogEnabled: e.target.checked })} /> Fog</label>
            <label className="flex items-center gap-1" title="Auto-reveal fog from party line of sight (walls block sight)"><input type="checkbox" checked={m.dynamicVision} onChange={(e) => update(e.target.checked ? { dynamicVision: true, fogEnabled: true, revealed: visionReveal(m.tokens, m.revealed) } : { dynamicVision: false })} /> Vision</label>
            <label className="flex items-center gap-1">Cell px<NumberField className="w-16" value={m.gridSize} min={10} max={400} onChange={(gridSize) => update({ gridSize })} aria-label="Grid cell size" /></label>
            <label className="flex items-center gap-1">Feet/cell<NumberField className="w-14" value={m.gridUnit.feet} min={1} max={100} onChange={(feet) => update({ gridUnit: { feet } })} aria-label="Feet per cell" /></label>

            {(tool === 'reveal' || tool === 'hide') && (
              <>
                <span className="mx-1 h-4 w-px bg-line" />
                {(['brush', 'rect', 'poly'] as FogShape[]).map((s) => (
                  <Button key={s} size="sm" variant={fogShape === s ? 'primary' : 'ghost'} onClick={() => { setFogShape(s); setPoly([]); setOverlay({}); }} className="capitalize">{s}</Button>
                ))}
                {fogShape === 'brush' && <label className="flex items-center gap-1">Size<NumberField className="w-12" value={brush} min={0} max={5} onChange={setBrush} aria-label="Brush size" /></label>}
                {fogShape === 'poly' && <Button size="sm" variant="secondary" disabled={poly.length < 3} onClick={finishPoly}>Finish polygon ({poly.length})</Button>}
                <Button size="sm" variant="ghost" onClick={() => update({ revealed: revealAll(dims.cols, dims.rows) })}>Reveal all</Button>
                <Button size="sm" variant="ghost" onClick={() => update({ revealed: hideAll() })}>Hide all</Button>
              </>
            )}
            {tool === 'aoe' && (
              <>
                <span className="mx-1 h-4 w-px bg-line" />
                {(['circle', 'cone', 'line', 'square'] as AoeShape[]).map((s) => (
                  <Button key={s} size="sm" variant={aoeShape === s ? 'primary' : 'ghost'} onClick={() => { setAoeShape(s); setOverlay({}); }} className="capitalize">{s}</Button>
                ))}
                <label className="flex items-center gap-1">Feet<NumberField className="w-14" value={aoeFeet} min={5} max={120} onChange={setAoeFeet} aria-label="AoE size feet" /></label>
                <Button size="sm" variant="ghost" onClick={() => setOverlay({})}>Clear</Button>
              </>
            )}
            {tool === 'draw' && (
              <>
                <span className="mx-1 h-4 w-px bg-line" />
                <Select className="h-8 w-28" value={drawKind} onChange={(e) => setDrawKind(e.target.value as DrawKind)} aria-label="Draw kind">
                  {(['freehand', 'line', 'arrow', 'rect', 'circle', 'text'] as DrawKind[]).map((k) => <option key={k} value={k}>{k}</option>)}
                </Select>
                <input type="color" value={drawColor} onChange={(e) => setDrawColor(e.target.value)} aria-label="Draw color" className="h-7 w-9" />
                <label className="flex items-center gap-1"><input type="checkbox" checked={gmDraw} onChange={(e) => setGmDraw(e.target.checked)} /> GM only</label>
                <Button size="sm" variant="ghost" disabled={m.drawings.length === 0} onClick={() => update({ drawings: m.drawings.slice(0, -1) })}>Undo draw</Button>
                <Button size="sm" variant="ghost" disabled={m.drawings.length === 0} onClick={() => update({ drawings: [] })}>Clear drawings</Button>
              </>
            )}
            {tool === 'walls' && (
              <>
                <span className="mx-1 h-4 w-px bg-line" />
                <span className="text-[11px]">Click to add wall points; click a door to open/close.</span>
                <Button size="sm" variant="secondary" disabled={poly.length < 2} onClick={finishWall}>Finish wall ({poly.length})</Button>
                <Button size="sm" variant="ghost" disabled={m.walls.length === 0} onClick={() => update({ walls: [] })}>Clear walls</Button>
                <label className="flex items-center gap-1">Sight ft<NumberField className="w-14" value={m.sightRadiusFeet} min={0} max={500} onChange={(v) => update({ sightRadiusFeet: v })} aria-label="Sight radius feet" /></label>
                <Button size="sm" variant="secondary" onClick={revealFromParty}>Reveal from party</Button>
              </>
            )}
            <span className="mx-1 h-4 w-px bg-line" />
            <Button size="sm" variant="secondary" onClick={addToken}>
              <CirclePlus size={14} aria-hidden />
              + Token
            </Button>
          </div>

          {activeEncounter && (
            <div className="mb-2 flex flex-wrap items-center gap-2 rounded-xl border border-accent/40 bg-accent-glow p-2 text-sm">
              <span className="smallcaps text-accent-deep">Combat · Round {activeEncounter.round}</span>
              <span className="font-display font-semibold text-ink">▶ {activeCombatant?.name ?? '—'}</span>
              <Button size="sm" variant="ghost" onClick={() => void encountersRepo.mutate(activeEncounter.id, previousTurn)}>‹ Prev</Button>
              <Button size="sm" variant="primary" onClick={() => void encountersRepo.mutate(activeEncounter.id, nextTurn)}>Next turn ›</Button>
              {activeTokenId ? <span className="text-xs text-muted">— their token glows on the map</span> : <span className="text-xs text-muted">— place their token from the palette</span>}
            </div>
          )}

          <MapCanvas
            viewportHeight="calc(100vh - 17rem)"
            view={{ image: m.image, gridSize: m.gridSize, showGrid: m.showGrid, fogEnabled: m.fogEnabled, revealed: m.revealed, tokens: canvasTokens, drawings: m.drawings, walls: m.walls, doors: m.doors }}
            overlay={overlay}
            tokensDraggable={tool === 'move'}
            activeTokenId={activeTokenId}
            {...(tool !== 'move' ? { onPointer, onLeave } : {})}
            onTokenMove={moveToken}
            onTokenClick={(id) => setEditToken(id)}
            onReady={setDims}
          />
        </div>
      </div>

      <p className="mt-2 text-xs text-muted">
        Place party/foe tokens from the left palette. Drag to move (Move tool); scroll to zoom, drag empty space (or middle/right-drag) to pan.
        Use <strong>Reveal/Hide</strong> for fog, <strong>Measure</strong>/<strong>AoE</strong> for ranges, <strong>Draw</strong> for annotations, then “Show to players”.
      </p>

      {token && (
        <Modal open onClose={() => setEditToken(null)} title="Token"
          footer={<><Button variant="ghost" onClick={() => setEditToken(null)}>Done</Button><Button variant="danger" onClick={() => { removeToken(token.id); setEditToken(null); }}>Delete</Button></>}>
          <div className="space-y-3">
            <div className="grid grid-cols-2 gap-3">
              <label className="block text-xs text-muted">Label<Input value={token.label} onChange={(e) => patchToken(token.id, { label: e.target.value })} /></label>
              <label className="block text-xs text-muted">Size<Select value={token.size} onChange={(e) => patchToken(token.id, { size: Number(e.target.value) })}>{[1, 2, 3, 4].map((s) => <option key={s} value={s}>{s}×{s}</option>)}</Select></label>
              <label className="block text-xs text-muted">Kind<Select value={token.kind} onChange={(e) => patchToken(token.id, { kind: e.target.value as MapToken['kind'] })}>{['pc', 'npc', 'monster', 'object'].map((k) => <option key={k} value={k}>{k}</option>)}</Select></label>
              <label className="block text-xs text-muted">Link character<Select value={token.characterId ?? ''} onChange={(e) => patchToken(token.id, e.target.value ? { characterId: e.target.value } : { characterId: undefined })}><option value="">— none —</option>{characters.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}</Select></label>
              {activeEncounter && (
                <label className="block text-xs text-muted">Link combatant<Select value={token.combatantId ?? ''} onChange={(e) => patchToken(token.id, e.target.value ? { combatantId: e.target.value } : { combatantId: undefined })}><option value="">— none —</option>{activeEncounter.combatants.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}</Select></label>
              )}
            </div>
            <div className="flex items-center gap-2">
              <span className="text-xs text-muted">Icon</span>
              {token.image ?? tokenLinkedChar?.portrait
                ? <img src={token.image ?? tokenLinkedChar?.portrait} alt="" className="h-10 w-10 rounded-full border border-line object-cover" />
                : <span className="grid h-10 w-10 place-items-center rounded-full border border-line text-[10px] text-muted">none</span>}
              <label className="cursor-pointer rounded-md border border-line px-2 py-1 text-xs text-ink hover:border-accent">
                Upload
                <input type="file" accept="image/*" className="hidden" onChange={(e) => { const f = e.target.files?.[0]; if (f) void onTokenIcon(token.id, f); e.target.value = ''; }} />
              </label>
              {token.image && <Button size="sm" variant="ghost" onClick={() => patchToken(token.id, { image: undefined })}>Clear</Button>}
              {!token.image && tokenLinkedChar?.portrait && <span className="text-[10px] text-muted">using {tokenLinkedChar.name}'s portrait</span>}
            </div>
            <div className="flex flex-wrap items-center gap-1">
              {TOKEN_COLORS.map((c) => <button key={c} aria-label={`color ${c}`} onClick={() => patchToken(token.id, { color: c })} className={cn('h-6 w-6 rounded-full border', token.color === c ? 'border-accent' : 'border-line')} style={{ background: c }} />)}
            </div>
            <label className="flex items-center gap-2 text-sm text-ink"><input type="checkbox" checked={token.gmOnly} onChange={(e) => patchToken(token.id, { gmOnly: e.target.checked })} /> Hidden from players (GM-only)</label>

            {token.combatantId && activeEncounter?.combatants.some((c) => c.id === token.combatantId) && (() => {
              const cb = activeEncounter.combatants.find((c) => c.id === token.combatantId)!;
              return (
                <div className="rounded-md border border-line bg-surface p-2">
                  <div className="mb-1 flex items-center justify-between text-xs"><span className="text-muted">Combat HP (syncs to the tracker)</span><span className="tabular-nums text-ink">{cb.hp.current}/{cb.hp.max}</span></div>
                  <div className="flex flex-wrap gap-1">
                    <Button size="sm" variant="danger" onClick={() => damageCombatant(token.combatantId!, -5)}>−5</Button>
                    <Button size="sm" variant="secondary" onClick={() => damageCombatant(token.combatantId!, -1)}>−1</Button>
                    <Button size="sm" variant="secondary" onClick={() => damageCombatant(token.combatantId!, 1)}>+1</Button>
                    <Button size="sm" variant="primary" onClick={() => damageCombatant(token.combatantId!, 5)}>+5</Button>
                  </div>
                </div>
              );
            })()}
          </div>
        </Modal>
      )}
    </div>
  );
}
