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

import * as api from "./api";
import Nav from "./components/Nav";
import Settings from "./components/Settings";
import { BTN_PRIMARY } from "./components/ui";
import { useAppearance, type Theme } from "./hooks/useAppearance";
import type { Config, Group, HiddenEvent, NotificationClick, 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 [fullscreen, setFullscreen] = useState(false);
  const [backdrop, setBackdrop] = 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;

  /* The window has no frame of its own, so in a normal window the shell keeps
     a margin around itself: somewhere to grab that is not a web page, and the
     only way to move or place the window by hand. Full screen has no use for
     it and gives the room back. */
  useEffect(() => {
    const w = getCurrentWindow();
    const check = () => void w.isFullscreen().then(setFullscreen);
    check();
    const un = w.onResized(check);
    return () => {
      void un.then((f) => f());
    };
  }, []);

  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();
    void api.setStage(r.x, r.y, r.width, r.height);
  }, []);

  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));
      }),
    ];
    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;

  const frame = fullscreen ? 0 : 6;

  return (
    <div
      data-tauri-drag-region
      className="flex h-screen bg-slate-200 dark:bg-black"
      style={{ padding: frame }}
    >
      <div
        className={
          "flex min-w-0 flex-1 overflow-hidden " +
          (fullscreen ? "" : "rounded-xl border border-slate-300 dark:border-slate-800")
        }
      >
      <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>

      </div>

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