import { useEffect, useState } from "react";
import { api } from "./api";
import CompareView from "./components/CompareView";
import ModelCard from "./components/ModelCard";
import Sentinel2View from "./components/Sentinel2View";
import type { CuratedPair, ModelSummary, PredictResult, Sentinel2AOI } from "./types";

type Tab = "curated" | "sentinel2" | "card";

// friendly labels for the served bundle ids (falls back to the raw id)
const MODEL_LABELS: Record<string, string> = {
  levircd_dinov2: "DINOv2 + LoRA",
  levircd_segformer: "SegFormer MiT-b2",
};
const modelLabel = (id: string) => MODEL_LABELS[id] ?? id;

// short scene tag from a source string like "LEVIR-CD test_10" -> "T10"
const sceneTag = (source: string) => {
  const m = source.match(/(\d+)\s*$/);
  return m ? `T${m[1]}` : source.slice(0, 3).toUpperCase();
};
// strip the "(test N)" suffix from a pair title for a tighter label
const sceneTitle = (title: string) => title.replace(/\s*\(test[^)]*\)\s*$/i, "");
// annotated ground-truth change % from the description, if present
const annotatedPct = (desc: string): number | null => {
  const m = desc.match(/([\d.]+)\s*%/);
  return m ? parseFloat(m[1]) : null;
};
// full-scene tile+stitch inference is seconds on CPU (baked once, then served from cache) — format
// the real model cost sensibly instead of a raw millisecond count.
const fmtDuration = (ms: number): { v: string; u: string } =>
  ms >= 1000 ? { v: (ms / 1000).toFixed(ms >= 10000 ? 0 : 1), u: "s" } : { v: String(Math.round(ms)), u: "ms" };
const fmtDurStr = (ms: number): string => {
  const d = fmtDuration(ms);
  return `${d.v} ${d.u}`;
};

export default function App() {
  const [tab, setTab] = useState<Tab>("curated");
  const [models, setModels] = useState<ModelSummary[]>([]);
  const [pairs, setPairs] = useState<CuratedPair[]>([]);
  const [s2, setS2] = useState<Sentinel2AOI[]>([]);
  const [modelId, setModelId] = useState<string>("");
  const [pairId, setPairId] = useState<string>("");
  const [result, setResult] = useState<PredictResult | null>(null);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const [showOverlay, setShowOverlay] = useState(true);
  const [opacity, setOpacity] = useState(0.75);

  useEffect(() => {
    Promise.all([api.models(), api.curated()])
      .then(([m, p]) => {
        setModels(m);
        setPairs(p);
        if (m.length) setModelId(m[0].id);
        if (p.length) setPairId(p[0].id);
      })
      .catch((e) => setError(String(e)));
    // Sentinel-2 AOIs load independently; a missing S2 cache must not break the aerial tab.
    api
      .sentinel2()
      .then(setS2)
      .catch((e) => console.warn("sentinel2 list failed:", e));
  }, []);

  useEffect(() => {
    if (!modelId || !pairId) return;
    let cancelled = false;
    setBusy(true);
    setError(null);
    setResult(null); // clear stale overlay/stats so the new imagery never shows the old mask
    api
      .predict(pairId, modelId)
      .then((r) => {
        if (!cancelled) setResult(r);
      })
      .catch((e) => !cancelled && setError(String(e)))
      .finally(() => !cancelled && setBusy(false));
    return () => {
      cancelled = true;
    };
  }, [modelId, pairId]);

  const model = models.find((m) => m.id === modelId);
  const pair = pairs.find((p) => p.id === pairId);
  const placeholder = result?.is_placeholder ?? model?.is_placeholder ?? false;
  const healthy = models.length > 0 && !error;
  const activeThreshold = result?.threshold ?? model?.threshold;

  return (
    <div className="app">
      <header className="topbar">
        <div className="brand">
          <svg className="mark" viewBox="0 0 32 32" fill="none" aria-hidden="true">
            <circle cx="16" cy="16" r="11" stroke="currentColor" strokeWidth="1.3" opacity=".5" />
            <circle cx="16" cy="16" r="4.4" stroke="currentColor" strokeWidth="1.3" />
            <path d="M16 1v7M16 24v7M1 16h7M24 16h7" stroke="currentColor" strokeWidth="1.3" />
            <rect x="13.4" y="13.4" width="5.2" height="5.2" fill="currentColor" />
          </svg>
          <div>
            <h1>Satellite Change Detection</h1>
            <div className="sub">
              {tab === "sentinel2" ? (
                <>
                  TRACK&nbsp;B · SENTINEL-2 10&nbsp;M/PX · <b>ONNX · CPU</b>
                </>
              ) : (
                <>
                  TRACK&nbsp;A · AERIAL 0.5&nbsp;M/PX · <b>ONNX · CPU</b>
                </>
              )}
            </div>
          </div>
        </div>
        <nav className="nav">
          <button className={`tab ${tab === "curated" ? "on" : ""}`} onClick={() => setTab("curated")}>
            Aerial
          </button>
          <button
            className={`tab ${tab === "sentinel2" ? "on" : ""}`}
            onClick={() => setTab("sentinel2")}
          >
            Sentinel-2
          </button>
          <button className={`tab ${tab === "card" ? "on" : ""}`} onClick={() => setTab("card")}>
            Model card
          </button>
          <div className="status">
            <span className={`dot ${healthy ? "" : "down"}`} />
            {healthy ? "RUNTIME NOMINAL" : "RUNTIME OFFLINE"}
          </div>
        </nav>
      </header>

      {placeholder && tab === "curated" && (
        <div className="banner">
          <span className="bicon">⚠</span> Serving <strong>placeholder (random-init) weights</strong> — the
          pipeline is real, the predictions are not. Swap in the trained bundle to ship.
        </div>
      )}
      {error && (
        <div className="banner err">
          <span className="bicon">✕</span> {error}
        </div>
      )}

      {tab === "card" ? (
        <ModelCard />
      ) : tab === "sentinel2" ? (
        <Sentinel2View aois={s2} />
      ) : (
        <div className="body">
          <aside className="rail">
            <section className="sec">
              <h2>
                <span className="idx">01</span> Scene pair <span className="rule" />
              </h2>
              {pairs.map((p) => {
                const pct = annotatedPct(p.description);
                return (
                  <button
                    key={p.id}
                    className={`scene ${p.id === pairId ? "on" : ""}`}
                    onClick={() => setPairId(p.id)}
                    title={p.description}
                  >
                    <span className="tag">{sceneTag(p.source || p.id)}</span>
                    <span className="meta">
                      <span className="t">{sceneTitle(p.title)}</span>
                      <span className="bar">
                        <i style={{ width: `${pct ?? 2}%` }} />
                      </span>
                    </span>
                    <span className="pct">{pct != null ? `${pct}%` : "ctrl"}</span>
                  </button>
                );
              })}
              <p className="scene-note">
                Real LEVIR-CD test crops · the bar is <b>annotated</b> change (ground truth) · 0.5 m/px
                aerial.
              </p>
            </section>

            <section className="sec">
              <h2>
                <span className="idx">02</span> Model <span className="rule" />
              </h2>
              <div className="select">
                <select value={modelId} onChange={(e) => setModelId(e.target.value)}>
                  {models.map((m) => (
                    <option key={m.id} value={m.id}>
                      {modelLabel(m.id)}
                      {m.is_placeholder ? " (placeholder)" : ""}
                    </option>
                  ))}
                </select>
              </div>
              {model && (
                <div className="kv">
                  <span className="chip">
                    grid <b>{model.input_size}²</b>
                  </span>
                  <span className="chip">{model.fixed_grid ? "fixed grid" : "dynamic H/W"}</span>
                  <span className="chip">
                    thr <b>{model.threshold.toFixed(3)}</b>
                  </span>
                </div>
              )}
            </section>

            <section className="sec">
              <h2>
                <span className="idx">03</span> Change overlay <span className="rule" />
              </h2>
              <div className="toggle">
                <span className="lbl">Show detected change</span>
                <div
                  className={`sw ${showOverlay ? "on" : ""}`}
                  role="switch"
                  aria-checked={showOverlay}
                  onClick={() => setShowOverlay((v) => !v)}
                />
              </div>
              <div className="slabel">
                Opacity <b>{Math.round(opacity * 100)}%</b>
              </div>
              <input
                type="range"
                min={0}
                max={1}
                step={0.01}
                value={opacity}
                style={{ ["--fill" as string]: `${opacity * 100}%` }}
                onChange={(e) => setOpacity(Number(e.target.value))}
              />
              <div className="legend">
                <span className="sq" /> Detected building change
                {activeThreshold != null && <> · thr {activeThreshold.toFixed(3)}</>}
              </div>
            </section>

            <section className="sec">
              <h2>
                <span className="idx">04</span> Analysis <span className="rule" />
              </h2>
              {result ? (
                <>
                  <div className="readout">
                    <div className="row">
                      <span className="k">Area changed</span>
                      <span className="v">
                        {result.stats.changed_percent}
                        <small>%</small>
                      </span>
                    </div>
                    <div className="row">
                      <span className="k">Mean conf · changed</span>
                      <span className="v">{result.stats.mean_confidence_changed.toFixed(3)}</span>
                    </div>
                    <div className="row">
                      <span className="k">Changed pixels</span>
                      <span className="v">{result.stats.changed_pixels.toLocaleString()}</span>
                    </div>
                    <div className="row">
                      <span className="k">Full-scene inference · CPU</span>
                      <span className="v">
                        {fmtDuration(result.elapsed_ms).v}
                        <small>{fmtDuration(result.elapsed_ms).u}</small>
                      </span>
                    </div>
                  </div>
                  <div className="gauge">
                    <div className="track">
                      <i style={{ width: `${Math.min(100, result.stats.changed_percent)}%` }} />
                    </div>
                    <div className="cap">
                      <span>0%</span>
                      <span>AREA CHANGED</span>
                      <span>100%</span>
                    </div>
                  </div>
                  <p className="readout-caption">
                    {result.n_tiles ?? 16}× 256&nbsp;px tiles, stitched · precomputed, served from
                    cache
                  </p>
                </>
              ) : busy ? (
                <div className="readout">
                  {["Area changed", "Mean conf · changed", "Changed pixels", "Inference · CPU"].map(
                    (k) => (
                      <div className="row" key={k}>
                        <span className="k">{k}</span>
                        <span className="v skeleton" />
                      </div>
                    ),
                  )}
                </div>
              ) : (
                <p className="readout-empty">Select a scene to run inference.</p>
              )}
            </section>
          </aside>

          <main className="stage">
            <div className="viewer">
              {pairId && modelId ? (
                <CompareView
                  beforeUrl={api.imageUrl(pairId, "before")}
                  afterUrl={api.imageUrl(pairId, "after")}
                  overlayUrl={result?.overlay_png ?? null}
                  overlayOpacity={opacity}
                  showOverlay={showOverlay}
                  loading={busy && !result}
                  legendLabel={modelLabel(modelId)}
                />
              ) : (
                <div className="empty">Initializing…</div>
              )}
            </div>

            <footer className="telemetry">
              <div className="cell accent">
                <span className="k">MODEL</span> <b>{modelId || "—"}</b>
              </div>
              <div className="cell">
                <span className="k">INPUT</span>{" "}
                <b>{model ? `${model.input_size}×${model.input_size}` : "—"}</b>
              </div>
              <div className="cell hide-narrow">
                <span className="k">GRID</span> <b>{model?.fixed_grid ? "FIXED" : "DYN H/W"}</b>
              </div>
              <div className="cell">
                <span className="k">THR</span>{" "}
                <b>{activeThreshold != null ? activeThreshold.toFixed(3) : "—"}</b>
              </div>
              <div className="cell">
                <span className="k">INFER</span>{" "}
                <b>{result ? fmtDurStr(result.elapsed_ms) : busy ? "…" : "—"}</b>
              </div>
              <div className="spacer" />
              <div className="cell hide-narrow">
                <span className="k">BANDS</span> <b>{model ? model.band_order.join("") : "—"}</b>
              </div>
              <div className="cell hide-narrow">
                <span className="k">GSD</span> <b>0.5 m/px</b>
              </div>
              <div className="cell">
                <span className="k">SRC</span> <b>{pair?.source || pair?.id || "—"}</b>
              </div>
            </footer>
          </main>
        </div>
      )}
    </div>
  );
}
