import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { listen } from "@tauri-apps/api/event";

import * as api from "./api";
import Nav from "./components/Nav";
import Settings from "./components/Settings";
import { BTN_PRIMARY, Dialog } from "./components/ui";
import { useAppearance, type Theme } from "./hooks/useAppearance";
import type {
  Config,
  Group,
  HiddenEvent,
  NotificationClick,
  PasswordOffer,
  SwitchEvent,
} from "./types";

export default function App() {
  const [config, setConfig] = useState<Config | null>(null);
  const [activeId, setActiveId] = useState<string | null>(null);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [focusHidden, setFocusHidden] = useState<string | null>(null);
  const [theme, setTheme] = useAppearance("system");
  const [unread, setUnread] = useState<Record<string, number>>({});
  const [backdrop, setBackdrop] = useState<string | null>(null);
  const [offer, setOffer] = useState<PasswordOffer | null>(null);
  const [saved, setSaved] = useState<string | null>(null);

  const stageRef = useRef<HTMLDivElement>(null);
  const booted = useRef(false);
  // Read inside listeners, which are registered once and would otherwise
  // capture the first render's value forever.
  const activeRef = useRef<string | null>(null);
  activeRef.current = activeId;

  const collapsed = config?.settings.navCollapsed ?? false;


  useEffect(() => {
    api.getConfig().then((c) => {
      setConfig(c);
      setTheme(c.settings.theme);
      const first = [...c.apps].sort((a, b) => a.order - b.order)[0];
      setActiveId(first?.id ?? null);
    });
  }, [setTheme]);

  /**
   * Tells Rust where the stage is.
   *
   * An app's webview is a native view that takes no part in CSS layout, so the
   * only way it lands in the right place is for the shell to measure the hole
   * it left and report it.
   */
  const report = useCallback(() => {
    const el = stageRef.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    /* Rounded to whole pixels, and to the same edges each time. A fractional
       left edge leaves a sliver of the stage's own background showing between
       the nav's border and the app — which reads as a second, lighter border
       line running down the whole window. */
    const x = Math.round(r.x);
    const y = Math.round(r.y);
    void api.setStage(x, y, Math.round(r.right) - x, Math.round(r.bottom) - y, 0);
  }, []);

  useLayoutEffect(() => {
    const el = stageRef.current;
    if (!el || !config) return;
    report();
    const ro = new ResizeObserver(report);
    ro.observe(el);
    window.addEventListener("resize", report);
    return () => {
      ro.disconnect();
      window.removeEventListener("resize", report);
    };
  }, [config, report]);

  // Webviews are built only once the stage has been measured, so they are born
  // at the right size instead of against a guess.
  useEffect(() => {
    if (!config || booted.current || config.apps.length === 0) return;
    booted.current = true;
    report();
    void api.bootstrap();
  }, [config, report]);


  useEffect(() => {
    if (activeId) void api.setActive(activeId);
  }, [activeId]);

  useEffect(() => {
    const unlisten = [
      // A link in one app that points at another: Rust decided, the shell moves.
      listen<SwitchEvent>("switch-app", async (e) => {
        setActiveId(e.payload.appId);
        await api.navigateApp(e.payload.appId, e.payload.url);
      }),
      // Something was right-clicked away inside a page.
      listen<HiddenEvent>("hidden-added", () => {
        void api.getConfig().then(setConfig);
      }),
      // "Manage hidden elements…" from a page's context menu.
      listen<string>("manage-hidden", (e) => {
        setFocusHidden(e.payload);
        setSettingsOpen(true);
      }),
      // A macOS banner was clicked. Switch to the app that raised it, then let
      // that page's own handler run — it is the only thing that knows which
      // message the notification was about.
      listen<NotificationClick>("notification-clicked", async (e) => {
        await api.focusWindow();
        setSettingsOpen(false);
        setActiveId(e.payload.appId);
        await api.setActive(e.payload.appId);
        await api.notificationClick(e.payload.appId, e.payload.notificationId);
      }),
      // Zoom changed from the menu bar; keep Settings' sliders honest.
      listen<[string, number]>("zoom-changed", () => {
        void api.getConfig().then(setConfig);
      }),
      listen<[string, number][]>("unread-changed", (e) => {
        setUnread(Object.fromEntries(e.payload));
      }),
      // A login was submitted. The password is held in Rust; this only asks.
      listen<PasswordOffer>("password-offer", (e) => setOffer(e.payload)),
    ];
    return () => {
      unlisten.forEach((p) => p.then((f) => f()));
    };
  }, []);

  /* A native view paints over anything the shell draws, so a dialog needs the
     app moved out of the way rather than merely a higher z-index — and once it
     is moved there is nothing left behind the dialog to look at. A still taken
     on the way out, blurred, puts the background back. */
  useEffect(() => {
    if (!config) return;
    let cancelled = false;

    if (settingsOpen) {
      void (async () => {
        const shot = await api.stageSnapshot().catch(() => null);
        if (cancelled) return;
        setBackdrop(shot);
        await api.hideStage();
      })();
    } else {
      setBackdrop(null);
      void api.showStage();
    }
    return () => {
      cancelled = true;
    };
  }, [settingsOpen, config]);

  const toggleCollapse = () => {
    if (!config) return;
    const next = !collapsed;
    setConfig({ ...config, settings: { ...config.settings, navCollapsed: next } });
    void api.setNavCollapsed(next);
  };

  const toggleGroup = (g: Group) => {
    if (!config) return;
    const updated = { ...g, collapsed: !g.collapsed };
    setConfig({ ...config, groups: config.groups.map((x) => (x.id === g.id ? updated : x)) });
    void api.updateGroup(updated);
  };

  const onTheme = (t: Theme) => {
    setTheme(t);
    void api.setTheme(t);
  };

  if (!config) return null;

  return (
    <div className="flex h-screen">
      <Nav
        config={config}
        activeId={activeId}
        collapsed={collapsed}
        onSelect={setActiveId}
        onToggleCollapse={toggleCollapse}
        onOpenSettings={() => { setFocusHidden(null); setSettingsOpen(true); }}
        onToggleGroup={toggleGroup}
        onBack={() => activeId && api.historyGo(activeId, -1)}
        onForward={() => activeId && api.historyGo(activeId, 1)}
        onReload={() => activeId && api.historyGo(activeId, 0)}
        unread={unread}
      />

      {/* The hole an app's native webview is positioned into. It stays empty
          on purpose — anything drawn here would be painted over. */}
      <div
        ref={stageRef}
        className="relative min-w-0 flex-1 overflow-hidden bg-slate-100 dark:bg-slate-950"
      >
        {backdrop && (
          <img
            src={backdrop}
            alt=""
            aria-hidden
            /* Scaled up so the blur has no soft edge to give itself away. */
            className="absolute inset-0 h-full w-full scale-110 object-cover blur-[16px]"
          />
        )}
        {config.apps.length === 0 && (
          <div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
            <p className="text-[13px] text-slate-500 dark:text-slate-400">
              No apps yet. Add the tools you work in and they appear in the nav.
            </p>
            <button onClick={() => setSettingsOpen(true)} className={BTN_PRIMARY}>
              Add your first app
            </button>
          </div>
        )}
      </div>

      {offer && (
        <Dialog
          title="Save this password?"
          onCancel={() => {
            void api.discardPassword();
            setOffer(null);
          }}
          confirmLabel="Save"
          onConfirm={async () => {
            try {
              setSaved(await api.savePassword());
            } catch {
              setSaved(null);
            }
            setOffer(null);
          }}
        >
          {offer.account ? (
            <>
              <span className="font-medium">{offer.account}</span> at{" "}
              <span className="font-mono text-[12px]">{offer.host}</span>.
            </>
          ) : (
            <>
              The login you just entered at{" "}
              <span className="font-mono text-[12px]">{offer.host}</span>.
            </>
          )}{" "}
          It goes into your macOS Keychain — not into this app's settings file, and
          nowhere else on disk.
        </Dialog>
      )}

      {saved && (
        <Dialog title="Saved" onCancel={() => setSaved(null)}>
          The password for <span className="font-mono text-[12px]">{saved}</span> is in
          your Keychain. You can see and remove it in Keychain Access, under{" "}
          <span className="font-mono text-[12px]">Work — {saved}</span>.
        </Dialog>
      )}

      {settingsOpen && (
        <Settings
          config={config}
          theme={theme}
          activeId={activeId}
          focusHiddenFor={focusHidden}
          onConfig={setConfig}
          onTheme={onTheme}
          onClose={() => { setSettingsOpen(false); setFocusHidden(null); }}
        />
      )}
    </div>
  );
}
