// App.jsx — orchestrates the OKR → Epic → Confirm flow + Tweaks

const { useState: useStateApp, useEffect: useEffectApp, useCallback: useCallbackApp } = React;

// Map a picked OKR (from /api/regime-scan) into a Featureban story shape.
// The story tile renders { ref, want, tag, points } — we lift those from
// the regime card so the kanban backlog reads as the user's picks.
function okrToStory(okr, goal) {
  const src = okr._source || {};
  const tf = src.timeframe || "";
  const archetype = src.archetype || "";
  const archetypeShort = archetype.replace(/_/g, " ").toLowerCase();
  const direction = okr.direction || src.direction || "";
  const pairLabel = (src.pair || "").split(":")[0] || okr.tag || "";
  const points = pointsFromComposite(src.composite);
  return {
    id: `okr-${okr.id || pairLabel}-${tf}`,
    ref: `${pairLabel} · ${tf}`,
    who: archetype,
    want: okr.objective || "Trade this regime",
    so: okr.summary || "validated against Hyperliquid",
    points,
    tag: prettyTag(archetypeShort, direction),
    fresh: true,
    okrId: okr.id,
  };
}

function pointsFromComposite(c) {
  const v = Number(c) || 1;
  if (v >= 4) return 5;
  if (v >= 2) return 3;
  return 2;
}

function prettyTag(archetypeShort, direction) {
  if (!archetypeShort) return direction || "Regime";
  if (direction === "BULLISH") return `Long · ${archetypeShort}`;
  if (direction === "BEARISH") return `Short · ${archetypeShort}`;
  return archetypeShort;
}

// ── Session persistence ─────────────────────────────────────────────────
// Round-trip nickname + board + sprint goal + current stage to localStorage
// so a return visit lands the user back on home (or the kanban, if they
// were last there) instead of replaying the Cover → nickname intro.
// Only the durable parts of the session are saved — transient state like
// pickedOkrs, refreshStatus, and end-of-day review context are reset on
// every load.

const STORAGE_KEY = "tradewizard:session:v1";
const RESTORABLE_STAGES = new Set(["home", "kanban"]);

function loadPersistedSession() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== "object" || !parsed.nickname) return null;
    return parsed;
  } catch (e) {
    return null;
  }
}

function savePersistedSession(session) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
  } catch (e) {
    // Private mode / storage full — silent best-effort.
  }
}

function clearPersistedSession() {
  try { localStorage.removeItem(STORAGE_KEY); } catch (e) {}
}

// Captured once at module load. React state below initializes from this
// snapshot. Subsequent writes happen in a useEffect.
const PERSISTED = loadPersistedSession();
const PERSISTED_HAS_BOARD = Array.isArray(PERSISTED?.board) && PERSISTED.board.length > 0;
const INITIAL_STAGE = (() => {
  if (!PERSISTED || !PERSISTED.nickname) return "cover";
  if (!RESTORABLE_STAGES.has(PERSISTED.stage)) return "home";
  // Don't drop the user onto an empty kanban — fall back to home so they
  // can scan first.
  if (PERSISTED.stage === "kanban" && !PERSISTED_HAS_BOARD) return "home";
  return PERSISTED.stage;
})();

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "dark": false,
  "showStamps": true,
  "compactEpics": false,
  "accent": "#C41F3E"
}/*EDITMODE-END*/;

const ACCENT_SWATCHES = {
  "#C41F3E": { primary: "#C41F3E", hover: "#9E1832", mark: "#6B1E3C" },
  "#8B1D41": { primary: "#8B1D41", hover: "#6B1E3C", mark: "#4A1428" },
  "#1F1F1F": { primary: "#1F1F1F", hover: "#000000", mark: "#1F1F1F" },
};

function applyAccent(name) {
  const a = ACCENT_SWATCHES[name] || ACCENT_SWATCHES.crimson;
  const r = document.documentElement;
  r.style.setProperty("--color-brand-primary", a.primary);
  r.style.setProperty("--color-brand-primary-hover", a.hover);
  r.style.setProperty("--color-brand-mark", a.mark);
  r.style.setProperty("--color-text-link", a.primary);
}

function CIBCBar({ onRestart, theme, onToggleTheme }) {
  const dark = theme === "dark";
  const btnStyle = {
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    color: "rgba(255,255,255,0.85)",
    background: "rgba(255,255,255,0.08)",
    border: "1px solid rgba(255,255,255,0.18)",
    borderRadius: "var(--radius-pill)",
    padding: "3px 10px 3px 8px",
    fontFamily: "var(--font-family-sans)",
    fontSize: 11,
    fontWeight: 600,
    letterSpacing: "0.04em",
    cursor: "pointer",
    transition: "background var(--motion-base) var(--ease-standard)",
  };
  return (
    <div style={{
      height: 36,
      background: "var(--color-utility-bar, #3A3A3A)",
      display: "flex",
      alignItems: "center",
      justifyContent: "space-between",
      padding: "0 20px",
      borderBottom: "3px solid var(--color-brand-primary)",
    }}>
      <span style={{
        color: "#fff",
        fontFamily: "var(--font-family-sans)",
        fontWeight: 700,
        fontSize: 13,
        letterSpacing: "-0.01em",
        textTransform: "none",
      }}>TradeWizard</span>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <button
          onClick={onToggleTheme}
          title={dark ? "Switch to light" : "Switch to dark"}
          aria-label="Toggle theme"
          style={{ ...btnStyle, width: 28, height: 24, padding: 0, justifyContent: "center" }}
          onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(255,255,255,0.16)")}
          onMouseLeave={(e) => (e.currentTarget.style.background = "rgba(255,255,255,0.08)")}
        >
          {dark ? (
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="4.5" />
              <line x1="12" y1="2" x2="12" y2="4" /><line x1="12" y1="20" x2="12" y2="22" />
              <line x1="2" y1="12" x2="4" y2="12" /><line x1="20" y1="12" x2="22" y2="12" />
              <line x1="4.9" y1="4.9" x2="6.3" y2="6.3" /><line x1="17.7" y1="17.7" x2="19.1" y2="19.1" />
              <line x1="4.9" y1="19.1" x2="6.3" y2="17.7" /><line x1="17.7" y1="6.3" x2="19.1" y2="4.9" />
            </svg>
          ) : (
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
            </svg>
          )}
        </button>
        <button
          onClick={onRestart}
          title="Restart simulation"
          style={btnStyle}
          onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(255,255,255,0.16)")}
          onMouseLeave={(e) => (e.currentTarget.style.background = "rgba(255,255,255,0.08)")}
        >
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
            <polyline points="1 4 1 10 7 10" />
            <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
          </svg>
          Restart
        </button>
      </div>
    </div>
  );
}

function FlowApp() {
  // Stages: 'cover' | 'home' | 'deck' | 'validate' | 'kanban'
  //       | 'sprint-review' | 'retro'
  // Flow: scan deck → multi-pick → HL validation → trading desk kanban
  const [stage, setStage] = useStateApp(INITIAL_STAGE);
  const [nickname, setNickname] = useStateApp(PERSISTED?.nickname || "");
  const [retroDots, setRetroDots] = useStateApp(0);
  // The OKRs the user swiped right on, awaiting validation.
  const [pickedOkrs, setPickedOkrs] = useStateApp([]);
  // Synthetic "sprint goal" so the kanban + end-of-day review keep working
  // without dragging the old planning UI back in. Restored from the last
  // session so the kanban header reads "Today's scan · N opportunities"
  // even after a reload.
  const [sprintGoal, setSprintGoal] = useStateApp(PERSISTED?.sprintGoal || null);
  // The live board — persists across sessions so the trader can close the
  // tab mid-cycle and resume from exactly where they left off.
  const [board, setBoard] = useStateApp(PERSISTED?.board || []);
  // Featureban outcome carried into sprint review + retro
  const [shippedStories, setShippedStories] = useStateApp([]);
  const [featurebanCtx, setFeaturebanCtx] = useStateApp(null);
  // Sprint review outcome carried into retro
  const [reviewCtx, setReviewCtx] = useStateApp(null);
  // Strategies handed to the review: closed (booked) + still paper-trading.
  const [reviewStories, setReviewStories] = useStateApp([]);
  // Bonus scoring from dependency / escalation challenges
  const [depCorrect, setDepCorrect] = useStateApp(0);
  const [escalationCorrect, setEscalationCorrect] = useStateApp(0);
  const [escalationLog, setEscalationLog] = useStateApp([]);
  const [t, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
  const [theme, setTheme] = useStateApp(() => {
    try { return localStorage.getItem("agent-trading-theme") || "light"; } catch (e) { return "light"; }
  });
  // Live regime-scan: starts with the static mock and is replaced by the
  // worker's /api/regime-scan output if a market-timing cell grid has been
  // pushed. `scanSource` is purely informational (live | mock).
  const [okrs, setOkrs] = useStateApp(window.OKRS);
  const [scanSource, setScanSource] = useStateApp("mock");
  const [scanAsOf, setScanAsOf] = useStateApp(null);
  // Manual-refresh state — drives the spinner + disabled button on the deck.
  // status: 'idle' | 'dispatching' | 'waiting' | 'error'
  const [refreshStatus, setRefreshStatus] = useStateApp({ status: "idle" });

  const fetchScan = useCallbackApp((opts = {}) => {
    const themeForCharts = theme === "dark" ? "dark" : "light";
    return fetch(`/api/regime-scan?theme=${themeForCharts}`, { headers: { accept: "application/json" } })
      .then((r) => (r.ok ? r.json() : null))
      .then((data) => {
        if (!data || !Array.isArray(data.okrs) || data.okrs.length === 0) return null;
        setOkrs(data.okrs);
        setScanSource("live");
        setScanAsOf(data.as_of || null);
        for (const o of data.okrs) {
          if (o.chart_url) { const i = new Image(); i.src = o.chart_url; }
        }
        return data;
      })
      .catch(() => null);
  }, [theme]);

  useEffectApp(() => {
    let cancelled = false;
    fetchScan().then(() => { if (cancelled) return; });
    return () => { cancelled = true; };
  }, [fetchScan]);

  // POST /api/refresh, then poll /api/refresh/status until scan_as_of moves
  // past dispatched_at. When it does, refetch the deck. Caps total wait at
  // 3 minutes so a hung Action doesn't leave the UI stuck in 'waiting'.
  const triggerRefresh = useCallbackApp(async (mode = "full") => {
    if (refreshStatus.status === "dispatching" || refreshStatus.status === "waiting") return;
    setRefreshStatus({ status: "dispatching" });
    let dispatchedAt;
    try {
      const r = await fetch("/api/refresh", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ mode }),
      });
      const body = await r.json().catch(() => ({}));
      if (!r.ok) {
        const errLabel = body.upstream_status
          ? `${body.error || "error"} (upstream ${body.upstream_status})`
          : (body.error || `http_${r.status}`);
        setRefreshStatus({
          status: "error",
          error: errLabel,
          detail: body.detail,
          retry_after: body.retry_after_seconds,
        });
        return;
      }
      dispatchedAt = body.dispatched_at;
      setRefreshStatus({ status: "waiting", mode, dispatched_at: dispatchedAt, eta_seconds: body.eta_seconds });
    } catch (e) {
      setRefreshStatus({ status: "error", error: "network" });
      return;
    }

    // Poll for completion. Scan completion is the primary signal — once the
    // new as_of lands, the deck re-renders. TA/visual land later in the
    // background and will refresh again on the next mount or theme change.
    const start = Date.now();
    const dispatchedMs = new Date(dispatchedAt).getTime();
    const maxWaitMs = 3 * 60 * 1000;
    while (Date.now() - start < maxWaitMs) {
      await new Promise((res) => setTimeout(res, 5000));
      try {
        const s = await fetch("/api/refresh/status", { headers: { accept: "application/json" } });
        if (!s.ok) continue;
        const status = await s.json();
        const scanMs = status.scan_as_of ? new Date(status.scan_as_of).getTime() : 0;
        if (scanMs > dispatchedMs) {
          await fetchScan();
          setRefreshStatus({ status: "idle" });
          return;
        }
      } catch { /* keep polling */ }
    }
    setRefreshStatus({ status: "error", error: "timeout" });
  }, [refreshStatus.status, fetchScan]);

  useEffectApp(() => {
    document.documentElement.dataset.theme = theme === "dark" ? "dark" : "";
    try { localStorage.setItem("agent-trading-theme", theme); } catch (e) {}
  }, [theme]);

  // Persist the durable session state every time it changes. Skipping when
  // nickname is empty means the very first paint of the Cover screen
  // doesn't overwrite a previous session before the user has typed anything.
  useEffectApp(() => {
    if (!nickname) return;
    savePersistedSession({ nickname, stage, board, sprintGoal });
  }, [nickname, stage, board, sprintGoal]);

  useEffectApp(() => {
    applyAccent(t.accent);
  }, [t.accent]);

  const restart = () => {
    setPickedOkrs([]);
    setSprintGoal(null);
    setBoard([]);
    setShippedStories([]);
    setReviewStories([]);
    setFeaturebanCtx(null);
    setReviewCtx(null);
    setRetroDots(0);
    setDepCorrect(0);
    setEscalationCorrect(0);
    setEscalationLog([]);
    // Wipe the persisted session so a fresh restart actually returns to
    // Cover next time the tab opens. Nickname is cleared too — Restart
    // means "fully reset", which matches what the icon implies.
    setNickname("");
    clearPersistedSession();
    setStage("cover");
  };

  // "Run another scan" — clear picks but keep the kanban running.
  const runAnotherScan = () => {
    setPickedOkrs([]);
    setStage("deck");
  };

  // Build kanban stories from the validated OKRs.
  const seedBoardFromPicks = (validatedPicks) => {
    if (!validatedPicks.length) return;
    const goal = {
      id: `today-${Date.now()}`,
      goal: `Today's scan · ${validatedPicks.length} opportunit${validatedPicks.length === 1 ? "y" : "ies"}`,
    };
    setSprintGoal(goal);
    const seed = validatedPicks.map((p) => okrToStory(p, goal));
    const initial = window.fbInitStories(seed).map((s) => ({
      ...s,
      goalId: goal.id,
      goalLabel: goal.goal,
    }));
    setBoard((prev) => [...prev, ...initial]);
  };

  // Close the book from the desk home: book any live positions and review.
  const endTheDay = () => {
    const closed = board.map((s) => (s.column === "review" ? { ...s, column: "done" } : s));
    setBoard(closed);
    const shipped = closed.filter((s) => s.column === "done");
    const paper = closed.filter((s) => s.column === "doing");
    setShippedStories(shipped);
    setReviewStories([...shipped, ...paper]);
    setFeaturebanCtx({ shippedCount: shipped.length, totalStories: board.length });
    setStage("sprint-review");
  };

  // A trading goal is "complete in full" when every strategy in its lane
  // has reached Closed — that unlocks the deep process retrospective.
  const hasCompleteGoal = (() => {
    const groups = {};
    board.forEach((s) => {
      const k = s.goalLabel || "Trading goal";
      (groups[k] = groups[k] || []).push(s);
    });
    return Object.values(groups).some((g) => g.length > 0 && g.every((s) => s.column === "done"));
  })();

  return (
    <div style={{
      width: "100%",
      height: "100%",
      display: "flex",
      flexDirection: "column",
      background: "var(--color-surface-default)",
      boxSizing: "border-box",
    }}>
      <CIBCBar onRestart={restart} theme={theme} onToggleTheme={() => setTheme((p) => (p === "dark" ? "light" : "dark"))} />
      <div style={{ flex: 1, minHeight: 0, paddingTop: 8 }}>
        {stage === "cover" && (
          <window.Cover
            onStart={(nick) => { setNickname(nick); setStage("home"); }}
          />
        )}
        {stage === "home" && (
          <window.DeskHome
            nickname={nickname}
            board={board}
            retroReady={hasCompleteGoal}
            onScan={() => setStage("deck")}
            onBoard={() => setStage("kanban")}
            onEndDay={endTheDay}
            onRetro={() => setStage("retro")}
          />
        )}
        {stage === "deck" && (
          <OKRDeck
            okrs={okrs}
            scanAsOf={scanAsOf}
            scanSource={scanSource}
            refreshStatus={refreshStatus}
            onRefresh={triggerRefresh}
            onContinue={(picks) => { setPickedOkrs(picks); setStage("validate"); }}
          />
        )}
        {stage === "validate" && pickedOkrs.length > 0 && (
          <window.ValidateScreen
            picks={pickedOkrs}
            onBack={() => setStage("deck")}
            onSendToBacklog={(validPicks) => {
              seedBoardFromPicks(validPicks);
              setStage("kanban");
            }}
          />
        )}
        {stage === "kanban" && (
          <window.Featureban
            sg={sprintGoal || { goal: "Trading desk" }}
            board={board}
            setBoard={setBoard}
            onExit={() => setStage("home")}
            onReview={({ shipped, paper, totalStories }) => {
              setShippedStories(shipped);
              setReviewStories([...shipped, ...(paper || [])]);
              setFeaturebanCtx({ shippedCount: shipped.length, totalStories });
              setStage("sprint-review");
            }}
          />
        )}
        {stage === "sprint-review" && sprintGoal && (
          <window.SprintReview
            sg={sprintGoal}
            shippedStories={shippedStories}
            reviewStories={reviewStories}
            canRetro={hasCompleteGoal}
            onExit={() => setStage("kanban")}
            onRetro={(ctx) => { setReviewCtx(ctx); setStage("retro"); }}
            onPlanAnother={restart}
          />
        )}
        {stage === "retro" && sprintGoal && (
          <window.Retro
            sg={sprintGoal}
            nickname={nickname}
            onDotsChange={setRetroDots}
            journey={{
              sprintGoal: sprintGoal?.goal,
              pickedCount: pickedOkrs.length,
              shippedCount: shippedStories.length,
              bv: reviewCtx?.bv,
              maxBV: reviewCtx?.maxBV,
              blockerCount: featurebanCtx?.blockerCount || 0,
              retroDots,
              depCorrect,
              escalationCorrect,
              featureban: {
                shippedCount: featurebanCtx?.shippedCount || 0,
                totalStories: featurebanCtx?.totalStories || 0,
                blockerCount: featurebanCtx?.blockerCount || 0,
                day: featurebanCtx?.day || 0,
                bv: reviewCtx?.bv || 0,
                maxBV: reviewCtx?.maxBV || 0,
              },
            }}
            onExit={() => setStage("sprint-review")}
            onPlanAnother={restart}
            onRunAnotherSprint={runAnotherScan}
          />
        )}
      </div>

      {/* Tweaks panel */}
      <window.TweaksPanel title="Tweaks">
        <window.TweakSection label="Brand">
          <window.TweakColor
            label="Accent"
            value={t.accent}
            options={["#C41F3E", "#8B1D41", "#1F1F1F"]}
            onChange={(v) => setTweak("accent", v)}
          />
        </window.TweakSection>
        <window.TweakSection label="Regime deck">
          <window.TweakToggle
            label="Show swipe stamps"
            value={t.showStamps}
            onChange={(v) => setTweak("showStamps", v)}
          />
        </window.TweakSection>
        <window.TweakSection label="Reset">
          <window.TweakButton onClick={restart} label="Restart session" />
        </window.TweakSection>
      </window.TweaksPanel>
    </div>
  );
}

// Mount the simulation directly — no phone bezel.
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<FlowApp />);
