/**
 * App — citation-evidence demo shell (CE-WP-0005).
 *
 * Composition:
 *
 *   SessionProvider (cross-session)
 *   └─ AppShell — owns routing + the top bar
 *       ├─ if no active session → CreateFirstSession (empty state)
 *       └─ else
 *           EngineProvider key={sessionId} sessionId={sessionId}
 *           └─ BinderProvider bus={engine.bus}
 *               └─ ReviewLayout | FormsApp (per `mode`)
 *
 * The hash is the single source of truth for `{sessionId, mode}`. The
 * SessionService's active id is kept in sync with the hash via a
 * useEffect inside `AppShell`. Deep links to unknown sessions redirect
 * to the empty state with a toast.
 */

import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { BinderProvider } from "@binder/index";
import {
  EngineProvider,
  SessionProvider,
  useActiveSession,
  useEngine,
  usePdfByteStore,
  useSessionByteStoreRegistry,
  useSessionService,
  useSessionsHydrated,
  useSessionVersion,
  useSessionVersionBumper,
} from "@work/index";

import { FormsApp } from "./forms/FormsApp";
import { ReviewLayout } from "./ReviewLayout";

import {
  CreateFirstSession,
  EMPTY_ROUTE,
  exportSessionZip,
  importSessionZip,
  parseRoute,
  navigateTo,
  SessionMenu,
  sessionZipFilename,
  Toast,
  triggerSessionDownload,
  UploadDropzone,
  useToast,
  type AppMode,
  type AppRoute,
} from "./sessions";

function readRoute(): AppRoute {
  if (typeof window === "undefined") return EMPTY_ROUTE;
  return parseRoute(window.location.hash);
}

function useHashRoute(): AppRoute {
  const [route, setRoute] = useState<AppRoute>(() => readRoute());
  useEffect(() => {
    const handler = () => setRoute(readRoute());
    window.addEventListener("hashchange", handler);
    return () => window.removeEventListener("hashchange", handler);
  }, []);
  return route;
}

function AppShell() {
  const route = useHashRoute();
  const service = useSessionService();
  const hydrated = useSessionsHydrated();
  const toast = useToast();
  // Guards the "unknown session id → toast + redirect" path against an
  // infinite loop: `useToast.show` creates a fresh `toast` object every
  // render, which would otherwise re-fire the effect.
  const lastHandledSessionIdRef = useRef<string | null>(null);

  // Sync hash → SessionService.setActive. Unknown session ids fall back
  // to the empty state with a toast.
  useEffect(() => {
    if (!hydrated) return;
    const key = route.sessionId ?? "";
    if (lastHandledSessionIdRef.current === key) return;
    lastHandledSessionIdRef.current = key;

    if (route.sessionId === null) {
      service.setActive(null);
      return;
    }
    const exists = service.get(route.sessionId);
    if (exists) {
      service.setActive(route.sessionId);
    } else {
      toast.show("Session not found — opened the empty state instead", "error");
      navigateTo(EMPTY_ROUTE);
    }
  }, [route.sessionId, service, hydrated, toast]);

  if (!hydrated) {
    return (
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          height: "100vh",
          fontFamily: "system-ui, sans-serif",
          color: "#888",
        }}
      >
        Loading…
      </div>
    );
  }

  if (route.sessionId === null) {
    return (
      <div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
        <EmptyTopBar />
        <div style={{ flex: 1, minHeight: 0 }}>
          <CreateFirstSession />
        </div>
        <Toast toast={toast.toast} onDismiss={toast.dismiss} />
      </div>
    );
  }

  return <ActiveAppFrame route={route} toast={toast} />;
}

function ActiveAppFrame({
  route,
  toast,
}: {
  route: AppRoute;
  toast: ReturnType<typeof useToast>;
}) {
  // EngineProvider remounts whenever the session id OR the per-session
  // version counter changes. Import-into-active-session bumps the version
  // so the new state from storage is picked up.
  const sessionId = route.sessionId!;
  const version = useSessionVersion(sessionId);
  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100vh", color: "#222" }}>
      <EngineProvider key={`${sessionId}:${version}`} sessionId={sessionId}>
        <ActiveTopBar route={route} showToast={toast.show} />
        <div style={{ flex: 1, minHeight: 0 }}>
          <SessionScopedTree mode={route.mode} />
        </div>
      </EngineProvider>
      <Toast toast={toast.toast} onDismiss={toast.dismiss} />
    </div>
  );
}

function SessionScopedTree({ mode }: { mode: AppMode }) {
  const engine = useEngine();
  return (
    <BinderProvider bus={engine.bus}>
      {mode === "forms" ? <FormsApp /> : <ReviewLayout upload={<UploadDropzone />} />}
    </BinderProvider>
  );
}

function EmptyTopBar() {
  const sessionService = useSessionService();
  const registry = useSessionByteStoreRegistry();
  const bumpVersion = useSessionVersionBumper();
  const toast = useToast(); // local toast — empty state has its own

  const handleImport = useCallback(async (file: File) => {
    try {
      const result = await importSessionZip(file, {
        sessionService,
        getOrCreateByteStore: registry.getOrCreateByteStore,
        bumpSessionVersion: bumpVersion,
      });
      navigateTo({ sessionId: result.sessionId, mode: "review" });
      toast.show(
        result.outcome === "created"
          ? "Imported as a new session"
          : "Merged into existing session",
        "success",
      );
    } catch (err) {
      toast.show(
        err instanceof Error ? `Import failed: ${err.message}` : "Import failed",
        "error",
      );
    }
  }, [sessionService, registry, bumpVersion, toast]);

  return (
    <header
      style={{
        display: "flex",
        gap: 8,
        padding: "6px 12px",
        borderBottom: "1px solid #ddd",
        background: "#fafafa",
        fontFamily: "system-ui, sans-serif",
        alignItems: "center",
      }}
    >
      <strong style={{ fontSize: 13, marginRight: 8 }}>citation-evidence</strong>
      <SessionMenu onImportZip={() => pickAndImport(handleImport)} />
      <Toast toast={toast.toast} onDismiss={toast.dismiss} />
    </header>
  );
}

function pickAndImport(onPicked: (file: File) => void): void {
  if (typeof document === "undefined") return;
  const input = document.createElement("input");
  input.type = "file";
  input.accept = ".zip,application/zip";
  input.onchange = () => {
    const file = input.files?.[0];
    if (file) onPicked(file);
  };
  input.click();
}

function ActiveTopBar({
  route,
  showToast,
}: {
  route: AppRoute;
  showToast: (msg: string, tone?: "success" | "error" | "info") => void;
}) {
  const engine = useEngine();
  const byteStore = usePdfByteStore();
  const session = useActiveSession();
  const sessionService = useSessionService();
  const registry = useSessionByteStoreRegistry();
  const bumpVersion = useSessionVersionBumper();

  const handleModeChange = useCallback(
    (next: AppMode) => {
      if (!route.sessionId) return;
      navigateTo({ sessionId: route.sessionId, mode: next });
    },
    [route.sessionId],
  );

  const handleExport = useCallback(async () => {
    if (!session) return;
    try {
      const blob = await exportSessionZip(engine, byteStore, session);
      triggerSessionDownload(blob, sessionZipFilename(session));
      showToast("Session exported", "success");
    } catch (err) {
      showToast(
        err instanceof Error ? `Export failed: ${err.message}` : "Export failed",
        "error",
      );
    }
  }, [engine, byteStore, session, showToast]);

  const handleImport = useCallback(
    async (file: File) => {
      try {
        const result = await importSessionZip(file, {
          sessionService,
          getOrCreateByteStore: registry.getOrCreateByteStore,
          bumpSessionVersion: bumpVersion,
        });
        navigateTo({ sessionId: result.sessionId, mode: "review" });
        const totals = result.stats;
        const summary =
          result.outcome === "created"
            ? `Imported new session — ${totals.documentsAdded} document${totals.documentsAdded === 1 ? "" : "s"}, ${totals.annotationsAdded} annotation${totals.annotationsAdded === 1 ? "" : "s"}`
            : `Merged into existing — ${totals.documentsAdded} new doc${totals.documentsAdded === 1 ? "" : "s"}, ${totals.documentsDeduped} deduped`;
        showToast(summary, "success");
      } catch (err) {
        showToast(
          err instanceof Error ? `Import failed: ${err.message}` : "Import failed",
          "error",
        );
      }
    },
    [sessionService, registry, bumpVersion, showToast],
  );

  const tabs = useMemo(
    () => [
      { id: "review" as const, label: "Review" },
      { id: "forms" as const, label: "Forms" },
    ],
    [],
  );

  return (
    <header
      style={{
        display: "flex",
        gap: 8,
        padding: "6px 12px",
        borderBottom: "1px solid #ddd",
        background: "#fafafa",
        fontFamily: "system-ui, sans-serif",
        alignItems: "center",
      }}
    >
      <strong style={{ fontSize: 13, marginRight: 8 }}>citation-evidence</strong>
      <SessionMenu
        onExportZip={() => void handleExport()}
        onImportZip={() => pickAndImport((file) => void handleImport(file))}
      />
      <div style={{ display: "flex", gap: 4, marginLeft: 12 }}>
        {tabs.map((t) => (
          <button
            key={t.id}
            onClick={() => handleModeChange(t.id)}
            aria-pressed={route.mode === t.id}
            style={tabStyle(route.mode === t.id)}
          >
            {t.label}
          </button>
        ))}
      </div>
    </header>
  );
}

function tabStyle(active: boolean) {
  return {
    padding: "4px 12px",
    fontSize: 12,
    border: "1px solid #ccc",
    borderBottom: active ? "2px solid #0050b3" : "1px solid #ccc",
    background: active ? "#e8f0ff" : "white",
    cursor: "pointer" as const,
  };
}

export function App() {
  return (
    <SessionProvider>
      <AppShell />
    </SessionProvider>
  );
}
