import { useEffect, useState, useCallback, useMemo, useRef } from "react";
import { LayoutEngine } from "./LayoutEngine";
import { Sidebar } from "./Sidebar";
import { TopBar } from "./TopBar";
import { CenterToolbar } from "./CenterToolbar";
import { Wizard } from "./Wizard";
import { SurfacePicker } from "./SurfacePicker";
import { PRESETS } from "./PresetPicker";
import { ConfirmDelete } from "./ConfirmDelete";
import { Settings } from "./Settings";
import { EventCenter } from "./EventCenter";
import { maybeNotify } from "./notify";
import { COLORS, FONT, applyTheme, resolvePalette } from "./theme";
import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface, getEventLog, markEventsRead, clearEvents, getHealth, closeWorkspaceCmd, getConfig, checkUpdate, splitSurface, closeSurfaceCmd, setZoom, readImageDataUrl, hasFullDiskAccess, openFullDiskAccessSettings } from "./socketBridge";
import type { EventRecord, DaemonHealth, ConfigView, UpdateInfo } from "./socketBridge";
import { leafIds } from "./layoutTypes";
import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes";
import { HOTKEYS, loadBindings, saveBindings, matches, hasModifier } from "./hotkeys";
import type { Bindings, HotkeyId } from "./hotkeys";

/** Read a boolean UI flag from localStorage, falling back to `def`. */
function loadFlag(key: string, def: boolean): boolean {
  try { const v = localStorage.getItem(key); return v === null ? def : v === "1"; }
  catch { return def; }
}
function saveFlag(key: string, value: boolean): void {
  try { localStorage.setItem(key, value ? "1" : "0"); } catch { /* ignore */ }
}

export function App() {
  const [groups, setGroups] = useState<Group[]>([]);
  const [workspaces, setWorkspaces] = useState<WorkspaceView[]>([]);
  const [activeId, setActiveId] = useState<string | null>(null);
  const [running, setRunning] = useState<Record<string, boolean>>({});
  const [states, setStates] = useState<Record<string, SurfaceState>>({});
  const [events, setEvents] = useState<EventRecord[]>([]);
  const [wizard, setWizard] = useState(false);
  // Pending additive preset awaiting the per-panel "what to open" choice.
  const [pendingPreset, setPendingPreset] = useState<{ id: string; delta: number; base: number } | null>(null);
  const [deleteTarget, setDeleteTarget] = useState<WorkspaceView | null>(null);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [eventsOpen, setEventsOpen] = useState(() => loadFlag("spacesh.eventsOpen", true));
  const [sidebarOpen, setSidebarOpen] = useState(() => loadFlag("spacesh.sidebarOpen", true));
  const [health, setHealth] = useState<DaemonHealth | null>(null);
  const [update, setUpdate] = useState<UpdateInfo | null>(null);
  const [updateChecking, setUpdateChecking] = useState(false);
  const [config, setConfigState] = useState<ConfigView | null>(null);
  // Bumped when the daemon connection is re-established; used to remount the
  // layout so terminals re-attach (snapshot + live stream) to the restarted daemon.
  const [connEpoch, setConnEpoch] = useState(0);
  const [connected, setConnected] = useState(false);
  const [focusedId, setFocusedId] = useState<string | null>(null);
  const [searchSurfaceId, setSearchSurfaceId] = useState<string | null>(null);
  const [searchNonce, setSearchNonce] = useState(0);
  const [bindings, setBindings] = useState<Bindings>(loadBindings);
  // Full Disk Access: terminals inherit spaceshell's TCC grants, so without FDA
  // tools like tmutil fail inside panels. Default true to avoid a flash before the probe.
  const [fdaOk, setFdaOk] = useState(true);
  const [fdaDismissed, setFdaDismissed] = useState(() => loadFlag("spacesh.fdaDismissed", false));
  const activeRef = useRef<string | null>(null);
  const effectiveFocusRef = useRef<string | null>(null);
  const wsRef = useRef<WorkspaceView[]>([]);
  const leavesRef = useRef<string[]>([]);
  const modalOpenRef = useRef(false);
  const bindingsRef = useRef<Bindings>(bindings);
  activeRef.current = activeId;
  wsRef.current = workspaces;
  bindingsRef.current = bindings;

  const seedEvents = useCallback(async () => {
    const log = await getEventLog();
    setEvents((existing) => {
      const byId = new Map<number, EventRecord>();
      for (const e of log.events) byId.set(e.id, e);          // daemon is authoritative for overlapping ids
      for (const e of existing) if (!byId.has(e.id)) byId.set(e.id, e); // keep live events not in the snapshot
      return [...byId.values()].sort((a, b) => b.id - a.id).slice(0, 1000);
    });
  }, []);

  const refresh = useCallback(async () => {
    const st = await getStatusFull();
    setGroups(st.groups);
    setWorkspaces(st.workspaces);
    const run: Record<string, boolean> = {};
    const stt: Record<string, SurfaceState> = {};
    st.workspaces.forEach((w) => Object.entries(w.surfaces).forEach(([id, sv]) => { run[id] = sv.running; stt[id] = sv.state; }));
    setRunning(run);
    setStates(stt);
    if (!activeRef.current && st.workspaces.length) setActiveId(st.workspaces[0].id);
  }, []);

  const loadHealth = useCallback(async () => {
    try { setHealth(await getHealth()); setConnected(true); }
    catch { setConnected(false); }
  }, []);

  const runUpdateCheck = useCallback(async () => {
    setUpdateChecking(true);
    try { setUpdate(await checkUpdate()); }
    catch { /* offline / server unreachable — leave the last known result */ }
    finally { setUpdateChecking(false); }
  }, []);

  const wsOf = (surfaceId: string): WorkspaceView | undefined =>
    wsRef.current.find((w) => surfaceId in w.surfaces);

  useEffect(() => {
    void refresh();
    void seedEvents();
    void loadHealth();
    void getConfig().then((c) => { setConfigState(c); }).catch(() => {});
    const unlisten = onDaemonEvent((evt) => {
      if (evt.evt === "event") {
        const rec = evt.data.record;
        setEvents((es) => [rec, ...es].slice(0, 1000));
        const w = wsOf(rec.surface_id);
        if (w && w.id !== activeRef.current) void setWorkspaceMeta(w.id, { unread: true });
        void maybeNotify(rec.surface_id, rec.agent_label ?? "shell", rec.workspace_name, rec.kind);
      } else if (evt.evt === "events_read") {
        const ids = new Set(evt.data.ids);
        setEvents((es) => es.map((e) => (ids.has(e.id) ? { ...e, read: true } : e)));
      } else if (evt.evt === "events_cleared") {
        setEvents([]);
      } else if (evt.evt === "state") {
        setStates((m) => ({ ...m, [evt.data.surface_id]: evt.data.state }));
        void refresh();
      } else if (evt.evt === "exit") {
        void refresh();
      } else if (evt.evt === "config_changed") {
        setConfigState(evt.data.config);
      } else {
        void refresh();
      }
    });
    const reconnect = onDaemonRawEvent("spacesh:disconnected", () => {
      setConnected(false);
      void refresh();
      void seedEvents();
      void loadHealth();
      void getConfig().then((c) => { setConfigState(c); }).catch(() => {});
    });
    const reconnected = onDaemonRawEvent("spacesh:reconnected", () => {
      setConnected(true);
      setConnEpoch((n) => n + 1); // remount layout → terminals re-attach to the new daemon
      void refresh();
      void seedEvents();
      void loadHealth();
      void getConfig().then((c) => { setConfigState(c); }).catch(() => {});
    });
    return () => { void unlisten.then((f) => f()); void reconnect.then((f) => f()); void reconnected.then((f) => f()); };
  }, [refresh, seedEvents, loadHealth]);

  // Cycle keyboard focus through the active workspace's panels.
  const cycleFocus = useCallback((dir: 1 | -1) => {
    const ls = leavesRef.current;
    if (ls.length < 2) return;
    const cur = effectiveFocusRef.current;
    const i = Math.max(0, ls.indexOf(cur ?? ls[0]));
    const next = ls[(i + dir + ls.length) % ls.length];
    setFocusedId(next);
    void focusSurface(next);
  }, []);

  // Scrollback search is meaningless on an agent surface: claude/codex/… run a
  // full-screen TUI in the alternate buffer that repaints every frame, so match
  // decorations are drawn and immediately clobbered (the counter updates but no
  // highlight shows). Gate the search affordance off for agent panels.
  const isAgentSurface = useCallback((id: string | null): boolean => {
    if (!id) return false;
    const w = wsRef.current.find((ws) => id in ws.surfaces);
    return !!w?.surfaces[id]?.spec?.agent_label;
  }, []);

  // Probe Full Disk Access on launch and again whenever the window regains focus
  // (so granting it in System Settings and returning clears the banner immediately).
  useEffect(() => {
    const recheck = () => { void hasFullDiskAccess().then(setFdaOk); };
    recheck();
    window.addEventListener("focus", recheck);
    return () => window.removeEventListener("focus", recheck);
  }, []);

  // Action dispatch table for hotkeys. Reads live state through refs so the
  // window listener can mount once.
  const actions = useMemo<Record<HotkeyId, () => void>>(() => ({
    newWorkspace: () => setWizard(true),
    openSettings: () => { if (config) setSettingsOpen(true); },
    toggleSidebar: () => setSidebarOpen((v) => !v),
    toggleEvents: () => setEventsOpen((v) => !v),
    splitRight: () => { const f = effectiveFocusRef.current; if (f) void splitSurface(f, "right"); },
    splitDown: () => { const f = effectiveFocusRef.current; if (f) void splitSurface(f, "down"); },
    closePanel: () => { const f = effectiveFocusRef.current; if (f) void closeSurfaceCmd(f); },
    focusNext: () => cycleFocus(1),
    focusPrev: () => cycleFocus(-1),
    zoomToggle: () => {
      const a = wsRef.current.find((w) => w.id === activeRef.current);
      if (!a) return;
      void setZoom(a.id, a.zoomed ? null : effectiveFocusRef.current);
    },
    search: () => {
      const f = effectiveFocusRef.current;
      if (f && !isAgentSurface(f)) { setSearchSurfaceId(f); setSearchNonce((n) => n + 1); }
    },
  }), [cycleFocus, config, isAgentSurface]);

  const actionsRef = useRef(actions);
  actionsRef.current = actions;

  // Central hotkey listener (mounted once). Skips while a modal is open, and only
  // ever swallows keys that carry a modifier so terminal input is never stolen.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (modalOpenRef.current) return;
      for (const h of HOTKEYS) {
        const b = bindingsRef.current[h.id];
        if (hasModifier(b) && matches(b, e)) {
          e.preventDefault();
          e.stopPropagation(); // capture phase: keep the chord out of the focused terminal
          actionsRef.current[h.id]();
          return;
        }
      }
    };
    // Capture phase so the chord is seen even when xterm has keyboard focus.
    window.addEventListener("keydown", onKey, true);
    return () => window.removeEventListener("keydown", onKey, true);
  }, []);

  // Update check: once on launch, then every 6h.
  useEffect(() => {
    void runUpdateCheck();
    const id = setInterval(() => { void runUpdateCheck(); }, 6 * 60 * 60 * 1000);
    return () => clearInterval(id);
  }, [runUpdateCheck]);

  useEffect(() => { saveFlag("spacesh.eventsOpen", eventsOpen); }, [eventsOpen]);
  useEffect(() => { saveFlag("spacesh.sidebarOpen", sidebarOpen); }, [sidebarOpen]);

  const unread = useMemo(() => events.filter((e) => !e.read).length, [events]);
  const active = workspaces.find((w) => w.id === activeId) ?? null;
  const leaves = active ? leafIds(active.layout) : [];
  const effectiveFocus = focusedId && leaves.includes(focusedId) ? focusedId : leaves[0] ?? null;
  effectiveFocusRef.current = effectiveFocus;
  leavesRef.current = leaves;
  modalOpenRef.current = wizard || settingsOpen || !!pendingPreset || !!deleteTarget;

  // Apply theme + background fill whenever appearance changes. A custom image is
  // loaded (file → data URL) before re-applying so the panel glass and root fill
  // paint together. Centralized here so every config source (initial load,
  // reconnect, config_changed) flows through one place.
  const [bgImage, setBgImage] = useState<string | null>(null);
  useEffect(() => {
    if (!config) return;
    let cancelled = false;
    if (config.background === "custom" && config.background_image) {
      void readImageDataUrl(config.background_image)
        .then((url) => { if (!cancelled) { setBgImage(url); applyTheme(config.theme, config.accent, config.background, url); } })
        .catch(() => { if (!cancelled) { setBgImage(null); applyTheme(config.theme, config.accent, "none", null); } });
    } else {
      setBgImage(null);
      applyTheme(config.theme, config.accent, config.background, null);
    }
    return () => { cancelled = true; };
  }, [config?.theme, config?.accent, config?.background, config?.background_image]); // eslint-disable-line react-hooks/exhaustive-deps

  const termPalette = useMemo(() => (config ? resolvePalette(config.theme, config.accent, config.background) : null), [config?.theme, config?.accent, config?.background, bgImage]);
  const termFont = useMemo(() => (config ? { family: config.font_family, size: config.font_size } : null), [config?.font_family, config?.font_size]);

  function selectWorkspace(id: string) {
    setActiveId(id);
    setFocusedId(null);
    void setWorkspaceMeta(id, { unread: false });
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100vh", background: COLORS.appBg }}>
      <TopBar active={active} eventsOpen={eventsOpen} onToggleEvents={() => setEventsOpen((v) => !v)} onShowEvents={() => setEventsOpen(true)} sidebarOpen={sidebarOpen} onToggleSidebar={() => setSidebarOpen((v) => !v)} unread={unread} onOpenSettings={() => { if (config) setSettingsOpen(true); }} update={update} updateChecking={updateChecking} onCheckUpdate={() => { void runUpdateCheck(); }} />
      {!fdaOk && !fdaDismissed && (
        <div style={{ position: "relative", zIndex: 25, display: "flex", alignItems: "center", gap: 12, padding: "8px 14px", background: "rgba(242,184,75,0.12)", borderBottom: `1px solid ${COLORS.stWait}`, fontFamily: FONT.ui, fontSize: 12, color: COLORS.textPrimary }}>
          <span style={{ flex: 1 }}>
            <b>Нет Full Disk Access.</b> Терминалы внутри spaceshell наследуют права приложения — без него команды вроде <code style={{ fontFamily: FONT.mono }}>tmutil</code> и доступ к защищённым папкам падают. Добавьте spaceshell в System Settings → Privacy &amp; Security → Full Disk Access.
          </span>
          <button onClick={() => void openFullDiskAccessSettings()}
            style={{ flex: "0 0 auto", padding: "5px 12px", background: COLORS.accent, color: COLORS.bgApp, border: "none", borderRadius: 7, fontSize: 12, fontWeight: 600, cursor: "pointer" }}>Открыть настройки</button>
          <button onClick={() => void hasFullDiskAccess().then(setFdaOk)}
            style={{ flex: "0 0 auto", padding: "5px 12px", background: COLORS.bgElevated, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12, cursor: "pointer" }}>Проверить снова</button>
          <button onClick={() => { setFdaDismissed(true); saveFlag("spacesh.fdaDismissed", true); }} aria-label="Скрыть"
            style={{ flex: "0 0 auto", padding: "5px 8px", background: "transparent", color: COLORS.textMuted, border: "none", borderRadius: 7, fontSize: 12, cursor: "pointer" }}>✕</button>
        </div>
      )}
      <div style={{ flex: 1, display: "flex", minHeight: 0 }}>
        <Sidebar railMode={!sidebarOpen} groups={groups} workspaces={workspaces} activeId={activeId} onSelect={selectWorkspace} onNew={() => setWizard(true)} onDelete={setDeleteTarget} health={health} connected={connected} />
        <div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
          {active && (
            <CenterToolbar selected="" paneCount={leaves.length} onSelect={(p) => {
              if (!active) return;
              const target = PRESETS.find((x) => x.id === p)?.slots ?? leaves.length;
              const delta = target - leaves.length;
              if (delta <= 0) { void applyPreset(active.id, p, []); return; } // reshape only — no new panels
              setPendingPreset({ id: p, delta, base: leaves.length });
            }} onOpenSearch={() => { if (effectiveFocus && !isAgentSurface(effectiveFocus)) { setSearchSurfaceId(effectiveFocus); setSearchNonce((n) => n + 1); } }} searchDisabled={isAgentSurface(effectiveFocus)} />
          )}
          <div style={{ flex: 1, minHeight: 0, position: "relative" }}>
            {active
              ? <LayoutEngine key={connEpoch} workspaceId={active.id} workspaceName={active.name} layout={active.layout} running={running} states={states} surfaces={active.surfaces} focusedId={effectiveFocus} onFocus={setFocusedId} zoomed={active.zoomed} searchSurfaceId={searchSurfaceId} searchNonce={searchNonce} onCloseSearch={() => setSearchSurfaceId(null)} font={termFont} palette={termPalette} />
              : <div style={{ color: COLORS.textMuted, padding: 24 }}>No workspace — create one to begin.</div>}
          </div>
        </div>
        {eventsOpen && (
          <EventCenter
            events={events}
            onMarkAllRead={() => { void markEventsRead({ target: "all" }); }}
            onClear={() => { void clearEvents(); }}
            onSelect={(sid, id) => { void focusSurface(sid); void markEventsRead({ target: "ids", value: [id] }); }}
          />
        )}
      </div>
      {settingsOpen && config && <Settings config={config} health={health} bindings={bindings} onBindingsChange={(b) => { setBindings(b); saveBindings(b); }} onClose={() => setSettingsOpen(false)} onReload={() => { void loadHealth(); void refresh(); }} />}
      {wizard && <Wizard onDone={(id) => { setWizard(false); setActiveId(id); void refresh(); }} onCancel={() => setWizard(false)} />}
      {pendingPreset && active && (
        <SurfacePicker
          count={pendingPreset.delta}
          onCancel={() => setPendingPreset(null)}
          onConfirm={(specs) => {
            const padded = [...Array(pendingPreset.base).fill({}), ...specs]; // align to daemon's slots.get(existing.len()+j)
            const wsId = active.id;
            setPendingPreset(null);
            void applyPreset(wsId, pendingPreset.id, padded).then(() => void refresh());
          }}
        />
      )}
      {deleteTarget && (
        <ConfirmDelete
          name={deleteTarget.name}
          activeCount={Object.values(deleteTarget.surfaces).filter((s) => s.running).length}
          onCancel={() => setDeleteTarget(null)}
          onConfirm={() => {
            const tgt = deleteTarget;
            setDeleteTarget(null);
            void closeWorkspaceCmd(tgt.id).then(() => {
              if (activeId === tgt.id) {
                const next = workspaces.find((w) => w.id !== tgt.id);
                setActiveId(next ? next.id : null);
              }
              void refresh();
            });
          }}
        />
      )}
    </div>
  );
}
