// Main app shell — wired to the FastAPI backend at /api/*.

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "warm",
  "compareMode": "toggle",
  "showOverlays": false
}/*EDITMODE-END*/;

// ── API client ──────────────────────────────────────────────────────────────
const api = {
  async samples() {
    const r = await fetch("/api/samples");
    return r.json();
  },
  async uploadFile(file) {
    const fd = new FormData();
    fd.append("file", file);
    const r = await fetch("/api/session", { method: "POST", body: fd });
    if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
    return r.json();
  },
  async loadSample(n) {
    const r = await fetch(`/api/sample/${n}`, { method: "POST" });
    if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
    return r.json();
  },
  async warp(sessionId, params) {
    const r = await fetch("/api/warp", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ session_id: sessionId, ...params }),
    });
    if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
    return r.json();
  },
  async recommend(sessionId, region, gender) {
    const r = await fetch("/api/recommend", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ session_id: sessionId, region, gender }),
    });
    if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
    return r.json();
  },
  async heal(sessionId, warpParams, healParams) {
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), 150000); // hard cap so it never hangs
    try {
      const r = await fetch("/api/heal", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          session_id: sessionId,
          warp: { session_id: sessionId, ...warpParams },
          ...healParams,
        }),
        signal: ctrl.signal,
      });
      if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
      return r.json();
    } catch (e) {
      if (e.name === "AbortError") throw new Error("Generation timed out after 150s — please try again.");
      throw e;
    } finally {
      clearTimeout(timer);
    }
  },
  async logHistory(sessionId, desc, detail, kind = "EVENT") {
    return fetch("/api/history", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ session_id: sessionId, desc, detail, kind }),
    });
  },
};
window.api = api;

// Pick the single most-changed measurement from a warp's deltas, so the live
// warp card always reflects what actually moved (hump edits don't touch
// projection, alar edits don't touch the bridge, etc.).
const DELTA_UNITS = { alar_mm: "mm", dorsal_mm: "mm", bridge_mm: "mm", tip_w_mm: "mm", nose_len_mm: "mm", projection_mm: "mm", nasolabial: "°", tip_rotation: "°", symmetry: "" };
function dominantDelta(deltas) {
  if (!deltas) return "live";
  let best = null;
  for (const k in DELTA_UNITS) {
    const v = deltas[k];
    if (typeof v === "number" && Math.abs(v) > Math.abs(best ? best.v : 0)) best = { k, v };
  }
  if (best && best.v !== 0) {
    const digits = best.k === "symmetry" ? 2 : 1;
    return `Δ ${best.v > 0 ? "+" : ""}${best.v.toFixed(digits)}${DELTA_UNITS[best.k]}`;
  }
  // Fallback for silhouette-only profile/3-4 edits that move no named metric:
  // show how far the nose moved on average so the slider still reads as live.
  if (typeof deltas.warp_mm === "number" && deltas.warp_mm > 0.05) {
    return `~${deltas.warp_mm.toFixed(1)}mm moved`;
  }
  return "live";
}

const emptyWarp = { alar: 0, hump: 0, tipLift: 0, humpProf: 0, tipRot: 0, bridgeWidth: 0, tipWidth: 0, noseLen: 0 };
const emptyHeal = { strength: 0.35, guidance: 3.0, steps: 28, feather: 18 };
const DEFAULT_PROMPT = "photoreal skin texture, natural lighting, preserve identity, seamless integration";

const initialState = () => ({
  session: null,
  loadingSession: false,
  sessionError: null,

  warp: { ...emptyWarp },
  warpDebounce: null,
  warping: false,
  warpImageUrl: null,
  warpKey: null,
  deltas: {},

  heal: { ...emptyHeal },
  healPrompt: DEFAULT_PROMPT,
  healing: false,

  results: [],
  activeResult: null,

  preset: "Custom",
  view: "frontal",
  overlays: { landmarks: false, warp: false, mask: false },
  compareMode: "toggle",

  step: "intake",
  region: null,
  gender: null,
  recommendation: null,
  intensityProfile: null,
  selectedStyle: null,
  selectedVariant: null,
});

const App = () => {
  const tweaksState = window.useTweaks ? window.useTweaks(TWEAK_DEFAULTS) : null;
  const tweaks = tweaksState ? tweaksState[0] : TWEAK_DEFAULTS;
  const setTweak = tweaksState ? tweaksState[1] : () => {};
  const [showConsent, setShowConsent] = React.useState(true);
  const [state, setState] = React.useState(initialState);
  const set = (patch) => setState(s => ({ ...s, ...(typeof patch === "function" ? patch(s) : patch) }));

  // Warm the face-detection model the moment the page loads, so the first
  // upload doesn't pay the model-init cold-start. Fire-and-forget.
  React.useEffect(() => { fetch("/api/warmup").catch(() => {}); }, []);

  React.useEffect(() => {
    const palettes = {
      warm: { bg: "#f4f1ec", bg2: "#ebe7e0", bgCard: "#fbfaf7", ink: "#1a1815", warn: "#b85a2a" },
      cool: { bg: "#eef1f4", bg2: "#e2e7ec", bgCard: "#f8fafc", ink: "#15181c", warn: "#2a6fb8" },
      dark: { bg: "#1a1815", bg2: "#252220", bgCard: "#2e2a26", ink: "#f4f1ec", warn: "#d68a5a" },
      paper: { bg: "#f0ece2", bg2: "#e4dfd2", bgCard: "#faf6ea", ink: "#2a2620", warn: "#8a5a2a" },
    };
    const p = palettes[tweaks.accent] || palettes.warm;
    const r = document.documentElement;
    r.style.setProperty("--bg", p.bg);
    r.style.setProperty("--bg-2", p.bg2);
    r.style.setProperty("--bg-card", p.bgCard);
    r.style.setProperty("--ink", p.ink);
    r.style.setProperty("--warn", p.warn);
    if (tweaks.accent === "dark") {
      r.style.setProperty("--ink-2", "#d8d3ca");
      r.style.setProperty("--ink-3", "#9c958a");
      r.style.setProperty("--ink-4", "#6a6358");
      r.style.setProperty("--hairline", "#3a3530");
      r.style.setProperty("--hairline-2", "#4a443d");
    } else {
      r.style.setProperty("--ink-2", "#3d3833");
      r.style.setProperty("--ink-3", "#7a736a");
      r.style.setProperty("--ink-4", "#aea69a");
      r.style.setProperty("--hairline", "#d9d3c8");
      r.style.setProperty("--hairline-2", "#c8c1b3");
    }
  }, [tweaks.accent]);

  const onSessionLoaded = (payload) => {
    set({
      session: payload,
      loadingSession: false,
      sessionError: null,
      view: payload.view.base === "reject" ? "frontal" : payload.view.base,
      warp: { ...emptyWarp },
      deltas: {},
      warpImageUrl: null,
      warpKey: null,
      recommendation: null,
      intensityProfile: null,
      selectedStyle: null,
      selectedVariant: null,
      results: [{
        id: "orig",
        kind: "original",
        label: "Original",
        url: payload.image_url,
        time: payload.history[0]?.time || "00:00",
        metric: "T0",
        pinned: false,
      }],
      activeResult: "orig",
      step: "analysis",
    });
  };

  const loadFile = async (file) => {
    if (!file) return;
    set({ loadingSession: true, sessionError: null });
    try {
      const payload = await api.uploadFile(file);
      onSessionLoaded(payload);
    } catch (e) {
      set({ loadingSession: false, sessionError: e.message });
    }
  };

  const loadSample = async (n) => {
    set({ loadingSession: true, sessionError: null });
    try {
      const payload = await api.loadSample(n);
      onSessionLoaded(payload);
    } catch (e) {
      set({ loadingSession: false, sessionError: e.message });
    }
  };

  const triggerWarp = (nextWarp) => {
    if (!state.session) return;
    if (state.warpDebounce) clearTimeout(state.warpDebounce);
    const isZero = Object.values(nextWarp).every(v => v === 0);
    if (isZero) {
      set({ warp: nextWarp, warpImageUrl: null, warpKey: null, deltas: {}, warping: false });
      return;
    }
    const t = setTimeout(async () => {
      set({ warping: true });
      try {
        const out = await api.warp(state.session.session_id, nextWarp);
        setState(s => {
          if (JSON.stringify(s.warp) !== JSON.stringify(nextWarp)) return { ...s, warping: false };
          let results = s.results.filter(r => r.id !== "warp-live");
          results = [...results, {
            id: "warp-live",
            kind: "warp",
            label: "Warp · live",
            url: out.image_url,
            time: "now",
            // Show the most-changed metric, not just projection — otherwise a
            // hump-only edit (projection Δ 0) misleadingly reads "Δ 0.0 mm".
            metric: dominantDelta(out.deltas),
            pinned: false,
            key: out.key,
          }];
          return { ...s, warping: false, warpImageUrl: out.image_url, warpKey: out.key, deltas: out.deltas, results };
        });
      } catch (e) {
        set({ warping: false, sessionError: e.message });
      }
    }, 320);
    set({ warp: nextWarp, warpDebounce: t });
  };

  const runHealFor = async (warp, meta) => {
    if (!state.session) return;
    set({ healing: true });
    try {
      const out = await api.heal(
        state.session.session_id,
        warp,
        { strength: state.heal.strength, guidance: state.heal.guidance, prompt: state.healPrompt, meta: meta || null },
      );
      setState(s => {
        const n = s.results.filter(r => r.kind === "heal").length + 1;
        const label = meta ? `${meta.style} · ${meta.variant}` : `Heal · v${n}`;
        const newCard = {
          id: "heal-" + out.key,
          kind: "heal",
          label,
          url: out.image_url,
          time: out.history_item.time,
          metric: `${out.elapsed_s.toFixed(1)}s`,
          pinned: false,
          key: out.key,
          meta: meta || null,
        };
        const history = [out.history_item, ...s.session.history];
        return {
          ...s,
          healing: false,
          results: [...s.results, newCard],
          activeResult: newCard.id,
          session: { ...s.session, history },
        };
      });
    } catch (e) {
      set({ healing: false, sessionError: e.message });
    }
  };
  const runHeal = () => runHealFor(state.warp, null);

  const ctx = { state, set, loadFile, loadSample, triggerWarp, runHeal, runHealFor };

  return (
    <>
      <div className="app">
        <div className="topbar">
          <div className="brand">
            <div className="brand-mark">Refrakt<em style={{ color: "var(--ink-3)" }}>·sim</em></div>
            <div className="brand-divider"></div>
            <div className="brand-context">Rhinoplasty studio</div>
          </div>
          <div className="session-meta">
            <span><span className="dot"></span>FAL · {state.session ? "connected" : "ready"}</span>
            {state.session && <span>VIEW · <span className="v">{state.session.view.tag}</span></span>}
            {state.session && <span>LANDMARKS · <span className="v">{state.session.landmarks_count}</span></span>}
            {state.session && <span>YAW · <span className="v">{state.session.view.yaw}°</span></span>}
          </div>
          <div className="topbar-actions">
            <button className="btn-ghost btn">Share with clinician</button>
            <button className="btn" disabled={!state.session}>Download</button>
            <button className="btn btn-primary" disabled={!state.session}>Save look</button>
          </div>
        </div>

        <div className="step-shell">
          <StepRail step={state.step} setStep={(s) => set({ step: s })} hasSession={!!state.session} />
          <div className="step-body">
            <StepRouter step={state.step} ctx={ctx} />
          </div>
        </div>

        <div className="bottombar">
          <div className="left">
            <span>© 2026 Refrakt</span>
            <span style={{ color: "var(--ink-4)" }}>·</span>
            <em>This is a simulation, not a medical recommendation.</em>
          </div>
          <div className="left">
            <span>Photo processed via fal.ai · encrypted in transit</span>
            <span style={{ color: "var(--ink-4)" }}>·</span>
            <span>HIPAA-aligned</span>
          </div>
        </div>
      </div>

      {showConsent && <ConsentDialog onAccept={() => setShowConsent(false)} />}

      {state.sessionError && (
        <div style={{
          position: "fixed", bottom: 56, left: 24, background: "var(--warn)",
          color: "#fff", padding: "10px 14px", fontFamily: "var(--mono)",
          fontSize: 11, letterSpacing: "0.1em", textTransform: "uppercase", zIndex: 40,
          maxWidth: 420, cursor: "pointer",
        }} onClick={() => set({ sessionError: null })}>
          {state.sessionError} · tap to dismiss
        </div>
      )}

      {window.TweaksPanel && (
        <TweaksPanel title="Tweaks">
          <TweakSection title="Theme">
            <TweakColor
              label="Palette"
              value={tweaks.accent}
              onChange={(v) => setTweak("accent", v)}
              options={[
                ["#f4f1ec", "#1a1815", "#b85a2a"],
                ["#eef1f4", "#15181c", "#2a6fb8"],
                ["#1a1815", "#f4f1ec", "#d68a5a"],
                ["#f0ece2", "#2a2620", "#8a5a2a"],
              ]}
            />
          </TweakSection>
          <TweakSection title="Actions">
            <TweakButton onClick={() => setState(initialState())}>Reset session</TweakButton>
          </TweakSection>
        </TweaksPanel>
      )}
    </>
  );
};

const ConsentDialog = ({ onAccept }) => (
  <div className="consent-overlay">
    <div className="consent">
      <h2>Before you <em>begin.</em></h2>
      <p>What you make here is a <em style={{ fontFamily: "var(--serif)", fontStyle: "italic" }}>simulation</em>, not a promise. Refrakt predicts physically honest possibilities — your actual surgical result will be determined by your anatomy, your surgeon's hand, and how you heal.</p>
      <ul>
        <li><span>01</span>Your photo is processed by our AI partner (fal.ai) and is never used for training.</li>
        <li><span>02</span>Results are predictive visualizations. They are not a medical opinion.</li>
        <li><span>03</span>You can share your session with a board-certified clinician at any time.</li>
      </ul>
      <div className="consent-actions">
        <button className="btn" onClick={onAccept}>Decline · leave</button>
        <button className="btn btn-primary" onClick={onAccept} style={{ flex: 1 }}>I understand · continue</button>
      </div>
    </div>
  </div>
);

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
