import { h } from 'preact';
import { useState, useEffect, useCallback } from 'preact/hooks';
import { diffLines, diffStats, sideBySide, DiffRow } from './lib/linediff';

// VersionMeta mirrors the Go VersionMeta returned by every versioned document
// type (interfaces, synthetic, controllogic, config sets/instances).
export interface VersionMeta {
  version: number;
  name: string;
  tag?: string;
  current: boolean;
  savedAt: string;
}

function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e); }

function fmtTime(iso: string): string {
  const d = new Date(iso);
  if (isNaN(d.getTime())) return iso;
  const pad = (n: number) => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

async function fetchJSON<T = any>(url: string, opts?: RequestInit): Promise<T | null> {
  const res = await fetch(url, opts);
  if (!res.ok && res.status !== 204) {
    let m = `HTTP ${res.status}`;
    try { const e = await res.json(); if (e && e.error) m = e.error; } catch { /* ignore */ }
    throw new Error(m);
  }
  if (res.status === 204) return null;
  return res.json();
}

// fetchVersionText returns a revision's serialized content as text, pretty-
// printing JSON so structural changes produce clean, stable line diffs (XML
// panels are returned verbatim).
async function fetchVersionText(base: string, v: number): Promise<string> {
  const res = await fetch(`${base}/versions/${v}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const raw = await res.text();
  try { return JSON.stringify(JSON.parse(raw), null, 2); } catch { return raw; }
}

// ── Version tree ──────────────────────────────────────────────────────────────

export function VersionTree({ base, viewing, dirty, canWrite = true, onView, onForked, onPromoted, onCompare, onError, reloadKey }: {
  base: string;                       // e.g. /api/v1/synthetic/<name>
  viewing: number | null;             // version currently open in the editor (active)
  dirty?: boolean;                    // unsaved edits → dashed connector at the top
  canWrite?: boolean;                 // gate promote/fork affordances
  onView: (version: number) => void;
  onForked: (newId: string) => void;
  onPromoted: (version: number) => void;
  onCompare: (av: number, bv: number) => void;
  onError: (m: string) => void;
  reloadKey?: number;                 // bump to force a versions reload
}) {
  const [versions, setVersions] = useState<VersionMeta[]>([]);
  const [a, setA] = useState<number | null>(null);
  const [b, setB] = useState<number | null>(null);

  const load = useCallback(async () => {
    try {
      const v = (await fetchJSON<VersionMeta[]>(`${base}/versions`)) ?? [];
      setVersions(v);
      if (v.length >= 2) { setA(v[1].version); setB(v[0].version); }
      else if (v.length === 1) { setA(v[0].version); setB(v[0].version); }
    } catch (e) { onError(`Versions failed: ${errMsg(e)}`); }
  }, [base, onError]);

  useEffect(() => { load(); }, [load, reloadKey]);

  async function fork(version: number) {
    try {
      const obj = await fetchJSON<{ id: string }>(`${base}/versions/${version}/fork`, { method: 'POST' });
      if (obj?.id) onForked(obj.id);
    } catch (e) { onError(`Fork failed: ${errMsg(e)}`); }
  }
  async function promote(version: number) {
    if (!confirm(`Promote v${version} to a new current version?`)) return;
    try {
      await fetchJSON(`${base}/versions/${version}/promote`, { method: 'POST' });
      await load();
      onPromoted(version);
    } catch (e) { onError(`Promote failed: ${errMsg(e)}`); }
  }

  const activeVer = viewing ?? versions.find(v => v.current)?.version ?? null;

  return (
    <div class="ver-tree">
      {dirty && (
        <div class="ver-node ver-node-unsaved">
          <div class="ver-rail">
            <span class="ver-dot ver-dot-unsaved" />
            <span class="ver-line ver-line-dashed" />
          </div>
          <div class="ver-body"><span class="ver-num">unsaved changes</span></div>
        </div>
      )}
      {versions.length === 0 && <div class="hint ver-empty">No versions yet.</div>}
      {versions.map((v, idx) => {
        const active = v.version === activeVer;
        const last = idx === versions.length - 1;
        const cls = `ver-dot${v.current ? ' ver-dot-current' : ''}${active ? ' ver-dot-active' : ''}`;
        return (
          <div key={v.version} class={`ver-node${active ? ' ver-node-active' : ''}`}>
            <div class="ver-rail">
              <span class={cls} title={v.current ? 'Selected (executed) version' : ''} />
              {!last && <span class="ver-line" />}
            </div>
            <div class="ver-body">
              <button class="ver-num" title="View this version" onClick={() => onView(v.version)}>
                v{v.version}
                {v.current && <span class="ver-badge">current</span>}
                {active && !v.current && <span class="ver-badge ver-badge-view">viewing</span>}
              </button>
              {v.tag && <span class="ver-tag hint" title={v.tag}>{v.tag}</span>}
              <span class="ver-time hint">{fmtTime(v.savedAt)}</span>
              <span class="ver-actions">
                {activeVer !== null && v.version !== activeVer && (
                  <button class="cl-mini-btn" title={`Diff against the viewed version (v${activeVer})`}
                    onClick={() => onCompare(Math.min(activeVer, v.version), Math.max(activeVer, v.version))}>diff</button>
                )}
                {canWrite && <button class="cl-mini-btn" title="Fork into a new document" onClick={() => fork(v.version)}>fork</button>}
                {canWrite && !v.current && <button class="cl-mini-btn" title="Promote to current" onClick={() => promote(v.version)}>promote</button>}
              </span>
            </div>
          </div>
        );
      })}
      {versions.length >= 2 && (
        <div class="ver-compare">
          <span class="hint">Compare</span>
          <select class="prop-select" value={String(a ?? '')} onChange={(e) => setA(Number((e.target as HTMLSelectElement).value))}>
            {versions.map(v => <option key={v.version} value={v.version}>v{v.version}</option>)}
          </select>
          <span>→</span>
          <select class="prop-select" value={String(b ?? '')} onChange={(e) => setB(Number((e.target as HTMLSelectElement).value))}>
            {versions.map(v => <option key={v.version} value={v.version}>v{v.version}</option>)}
          </select>
          <button class="panel-btn" disabled={a === null || b === null} onClick={() => onCompare(a!, b!)}>Diff</button>
        </div>
      )}
    </div>
  );
}

// ── Diff viewer ───────────────────────────────────────────────────────────────

export function DiffViewer({ base, av, bv, title, onClose }: {
  base: string;
  av: number;
  bv: number;
  title: string;
  onClose: () => void;
}) {
  const [rows, setRows] = useState<DiffRow[] | null>(null);
  const [mode, setMode] = useState<'unified' | 'split'>('split');
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let alive = true;
    (async () => {
      try {
        const [left, right] = await Promise.all([fetchVersionText(base, av), fetchVersionText(base, bv)]);
        if (alive) setRows(diffLines(left, right));
      } catch (e) { if (alive) setError(errMsg(e)); }
    })();
    return () => { alive = false; };
  }, [base, av, bv]);

  const stats = rows ? diffStats(rows) : { added: 0, removed: 0 };

  return (
    <div class="cl-confirm-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div class="cl-confirm ver-diff-modal">
        <div class="ver-diff-head">
          <span class="cl-confirm-title">Diff — {title}</span>
          <span class="ver-diff-stats">
            <span class="ver-diff-add">+{stats.added}</span> <span class="ver-diff-del">−{stats.removed}</span>
          </span>
          <span class="ver-diff-modes">
            <button class={`panel-btn${mode === 'split' ? ' panel-btn-primary' : ''}`} onClick={() => setMode('split')}>Side by side</button>
            <button class={`panel-btn${mode === 'unified' ? ' panel-btn-primary' : ''}`} onClick={() => setMode('unified')}>Unified</button>
          </span>
          <button class="panel-btn" onClick={onClose}>Close</button>
        </div>
        {error && <div class="cl-error">{error}</div>}
        {!rows && !error && <div class="hint ver-diff-loading">Loading…</div>}
        {rows && (mode === 'unified' ? <UnifiedDiff rows={rows} /> : <SplitDiff rows={rows} />)}
      </div>
    </div>
  );
}

function UnifiedDiff({ rows }: { rows: DiffRow[] }) {
  return (
    <div class="ver-diff ver-diff-unified">
      {rows.map((r, i) => (
        <div key={i} class={`ver-diff-row ver-diff-${r.op}`}>
          <span class="ver-diff-ln">{r.leftNo ?? ''}</span>
          <span class="ver-diff-ln">{r.rightNo ?? ''}</span>
          <span class="ver-diff-sign">{r.op === 'add' ? '+' : r.op === 'del' ? '−' : ' '}</span>
          <span class="ver-diff-text">{r.text || '\u00a0'}</span>
        </div>
      ))}
    </div>
  );
}

function SplitDiff({ rows }: { rows: DiffRow[] }) {
  const pairs = sideBySide(rows);
  return (
    <div class="ver-diff ver-diff-split">
      {pairs.map((p, i) => (
        <div key={i} class="ver-diff-splitrow">
          <span class="ver-diff-ln">{p.left?.leftNo ?? ''}</span>
          <span class={`ver-diff-cell ver-diff-${p.left ? p.left.op : 'empty'}`}>{p.left ? (p.left.text || '\u00a0') : ''}</span>
          <span class="ver-diff-ln">{p.right?.rightNo ?? ''}</span>
          <span class={`ver-diff-cell ver-diff-${p.right ? p.right.op : 'empty'}`}>{p.right ? (p.right.text || '\u00a0') : ''}</span>
        </div>
      ))}
    </div>
  );
}
