"use client";

import { useEffect, useRef, useState, type ReactNode } from "react";
import { usePathname, useRouter } from "next/navigation";
import Link from "next/link";
import { getApiVersion, logout, me, updateUiScale, type ServiceVersion } from "@/lib/api";
import { webBuildInfo } from "@/lib/build-info";
import { AuthContext, can } from "@/lib/abilities";
import { ROLE_LABEL } from "@/lib/labels";
import {
  DEFAULT_UI_SCALE,
  applyUiScale,
  normalizeUiScale,
  readUiScale,
  saveUiScale,
} from "@/lib/ui-scale";
import { FontScaleControl } from "./FontScaleControl";
import type { AuthUser, Ability } from "@/lib/types";

/**
 * Authenticated shell: gates on /auth/me, redirects to /login when the
 * session is missing, renders the brand header + logout, and wraps page
 * content. Provides the AuthContext so any page can read the user's
 * abilities. Used by every authenticated page.
 */

type NavLink = {
  href: string;
  label: string;
  ability?: Ability;
  /** Shown when the user holds *any* of these — for a screen that merges two
   *  separately-gated jobs (Notificaciones: servicios + pólizas). */
  anyAbility?: Ability[];
  exact?: boolean;
  /** Extra path prefixes that belong to this entry (e.g. a second route into
   *  the same screen), so they highlight it instead of nothing. */
  aliases?: string[];
};
type NavEntry =
  | ({ kind: "link" } & NavLink)
  | { kind: "group"; label: string; items: NavLink[] };

/**
 * Top nav. Daily screens stay one click away; the movement screens and the
 * admin screens are grouped behind menus so the bar doesn't saturate as the
 * app grows. A group disappears entirely when the user can't see any of its
 * items (gating here is cosmetic — the API enforces every write).
 */
const NAV: NavEntry[] = [
  { kind: "link", href: "/inicio", label: "Inicio", exact: true },
  { kind: "link", href: "/clientes", label: "Clientes" },
  { kind: "link", href: "/polizas", label: "Pólizas" },
  { kind: "link", href: "/servicios", label: "Propiedades" },
  {
    kind: "group",
    label: "Cobranza",
    items: [
      // Daily data-entry screen (the legacy "Editor"). Hidden from VIEWER, who
      // can't capture anyway — the page itself also refuses. Both capture modes
      // live behind this one entry: keying receipts by hand, and scanning a
      // stack of bills for OCR (the `/recibos` route opens the same screen on
      // its automatic tab).
      {
        href: "/estado-cuenta/lote",
        label: "Captura",
        ability: "ledger:create",
        aliases: ["/recibos"],
      },
      { href: "/estado-cuenta", label: "Estado de cuenta" },
      { href: "/banco", label: "Chequera" },
    ],
  },
  { kind: "link", href: "/reportes", label: "Reportes" },
  {
    kind: "group",
    label: "Admin",
    items: [
      { href: "/catalogos", label: "Catálogos", ability: "lookup:manage" },
      {
        href: "/banco/cuentas",
        label: "Cuentas de chequera",
        ability: "bank:manage-accounts",
      },
      // Mass email (servicios) and renewal notices (pólizas) are two tabs of
      // one screen; `/renovaciones` opens the same page on its pólizas tab.
      {
        href: "/notificaciones",
        label: "Notificaciones",
        anyAbility: ["notification:send", "renewal:send"],
        aliases: ["/renovaciones"],
      },
      { href: "/usuarios", label: "Usuarios", ability: "user:manage" },
      { href: "/operaciones", label: "Operaciones", ability: "db:manage" },
    ],
  },
];

/** Every nav destination, flattened out of the groups. */
const NAV_LINKS: NavLink[] = NAV.flatMap((entry) =>
  entry.kind === "link" ? [entry] : entry.items,
);

/** The nav the given user may see, with empty groups dropped. */
function visibleNav(user: AuthUser | null): NavEntry[] {
  const allowed = (item: NavLink) =>
    (!item.ability || can(user, item.ability)) &&
    (!item.anyAbility || item.anyAbility.some((a) => can(user, a)));
  const out: NavEntry[] = [];
  for (const entry of NAV) {
    if (entry.kind === "link") {
      if (allowed(entry)) out.push(entry);
      continue;
    }
    const items = entry.items.filter(allowed);
    if (items.length > 0) out.push({ ...entry, items });
  }
  return out;
}

/**
 * Which nav entry is highlighted for a path. Longest matching href wins, so a
 * nested route (`/estado-cuenta/lote`) highlights its own entry instead of also
 * lighting up its parent (`/estado-cuenta`) — while `/estado-cuenta/<id>`, which
 * has no entry of its own, still correctly highlights the parent.
 */
function activeHref(pathname: string | null): string | null {
  if (!pathname) return null;
  let best: string | null = null;
  for (const item of NAV_LINKS) {
    const under = (href: string) =>
      pathname === href || pathname.startsWith(`${href}/`);
    const match = item.exact
      ? pathname === item.href
      : under(item.href) || (item.aliases?.some(under) ?? false);
    if (match && (best === null || item.href.length > best.length)) {
      best = item.href;
    }
  }
  return best;
}

/**
 * One collapsible group in the desktop bar. Opens on click, closes on outside
 * click, Escape, or navigation. The trigger stays highlighted while any of its
 * children is the current page.
 */
function NavMenu({
  label,
  items,
  current,
  pathname,
}: {
  label: string;
  items: NavLink[];
  current: string | null;
  pathname: string | null;
}) {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);
  const holdsCurrent = items.some((item) => item.href === current);

  useEffect(() => {
    setOpen(false);
  }, [pathname]);

  useEffect(() => {
    if (!open) return;
    function onPointerDown(event: MouseEvent) {
      if (ref.current && !ref.current.contains(event.target as Node)) {
        setOpen(false);
      }
    }
    function onKeyDown(event: KeyboardEvent) {
      if (event.key === "Escape") setOpen(false);
    }
    document.addEventListener("mousedown", onPointerDown);
    document.addEventListener("keydown", onKeyDown);
    return () => {
      document.removeEventListener("mousedown", onPointerDown);
      document.removeEventListener("keydown", onKeyDown);
    };
  }, [open]);

  return (
    <div className="appbar-menu" ref={ref}>
      <button
        type="button"
        className={`appbar-link appbar-menu-trigger${holdsCurrent ? " active" : ""}`}
        aria-expanded={open}
        aria-haspopup="true"
        onClick={() => setOpen((v) => !v)}
      >
        {label}
        <span className="appbar-caret" aria-hidden="true" />
      </button>
      {open && (
        <div className="appbar-dropdown" role="menu">
          {items.map((item) => (
            <Link
              key={item.href}
              href={item.href}
              role="menuitem"
              className={`appbar-dropdown-link${current === item.href ? " active" : ""}`}
              aria-current={current === item.href ? "page" : undefined}
              onClick={() => setOpen(false)}
            >
              {item.label}
            </Link>
          ))}
        </div>
      )}
    </div>
  );
}

/**
 * What is deployed, from both halves. build.yml builds api + web in one matrix
 * run, so their versions cannot drift at build time — but they can at DEPLOY
 * time, if a stack is applied with only one image's tag moved. Showing both and
 * flagging a mismatch is the cheap check that catches a half-applied release.
 */
function BuildFooter() {
  const web = webBuildInfo();
  const [api, setApi] = useState<ServiceVersion | null>(null);

  useEffect(() => {
    let alive = true;
    getApiVersion()
      .then((v) => {
        if (alive) setApi(v);
      })
      .catch(() => {
        // The shell already redirects to /login when the API is unreachable;
        // a missing version line is not worth a second error surface.
      });
    return () => {
      alive = false;
    };
  }, []);

  // Compare the COMMIT, not the version string. On a branch build both tiers
  // report APP_VERSION "master", so comparing versions cannot see drift — which
  // is exactly how a stale web image once sat next to a current API with this
  // footer showing nothing wrong. The sha is the only field that actually
  // differs between two builds of the same branch.
  const mismatch = api !== null && api.gitSha !== web.gitSha;

  return (
    <footer className="shell-footer">
      <span>Jorge Cuadros &amp; Asociados</span>
      {/* The FULL 40-char commit, not an abbreviation: this line exists to be
          pasted into `git show` or compared against a registry tag, and a
          7-char prefix makes both a manual step. It is what GIT_SHA already
          carries — build.yml bakes in `github.sha` whole. */}
      <span
        className="shell-footer-build"
        title={`web ${web.version} (${web.gitSha}) — ${web.buildDate}`}
      >
        v{web.version} · {web.gitSha}
        {mismatch && api ? ` · API ${api.gitSha}` : ""}
      </span>
      {mismatch && (
        <span className="shell-footer-warn" role="status">
          versiones desincronizadas
        </span>
      )}
    </footer>
  );
}

export function AppShell({ children }: { children: ReactNode }) {
  const router = useRouter();
  const pathname = usePathname();
  const [user, setUser] = useState<AuthUser | null>(null);
  const [checking, setChecking] = useState(true);
  const [loggingOut, setLoggingOut] = useState(false);
  const [drawerOpen, setDrawerOpen] = useState(false);
  const [uiScale, setUiScale] = useState(DEFAULT_UI_SCALE);
  const current = activeHref(pathname);
  const nav = visibleNav(user);

  useEffect(() => {
    let alive = true;
    me()
      .then((u) => {
        if (!alive) return;
        setUser(u);
        setChecking(false);
        // The account wins over the localStorage copy the pre-hydration script
        // painted with: that copy is this browser's, while the account follows
        // the person between machines. Re-save so the next cold paint here is
        // already correct.
        const accountScale = normalizeUiScale(u.uiScale ?? DEFAULT_UI_SCALE);
        setUiScale(accountScale);
        applyUiScale(accountScale);
        saveUiScale(accountScale);
      })
      .catch(() => {
        router.replace("/login");
      });
    return () => {
      alive = false;
    };
  }, [router]);

  // Before /auth/me answers, show whatever the pre-hydration script applied so
  // the control isn't briefly out of step with the page.
  useEffect(() => {
    setUiScale(readUiScale());
  }, []);

  function changeUiScale(next: number) {
    setUiScale(next);
    applyUiScale(next);
    saveUiScale(next);
    setUser((prev) => (prev ? { ...prev, uiScale: next } : prev));
    // Fire and forget: the change is already applied and cached locally, so a
    // failed write only means it won't follow the user to another machine.
    updateUiScale(next).catch(() => {
      /* ignore */
    });
  }

  // Navigating away closes the mobile drawer — the route change is the only
  // "done" signal we get from a <Link>.
  useEffect(() => {
    setDrawerOpen(false);
  }, [pathname]);

  useEffect(() => {
    if (!drawerOpen) return;
    function onKeyDown(event: KeyboardEvent) {
      if (event.key === "Escape") setDrawerOpen(false);
    }
    document.addEventListener("keydown", onKeyDown);
    return () => document.removeEventListener("keydown", onKeyDown);
  }, [drawerOpen]);

  async function handleLogout() {
    setLoggingOut(true);
    try {
      await logout();
    } catch {
      /* ignore — we redirect regardless */
    }
    router.replace("/login");
  }

  if (checking) {
    return (
      <div
        style={{
          minHeight: "100vh",
          display: "grid",
          placeItems: "center",
          color: "var(--brand-700)",
        }}
      >
        <span className="spinner" aria-label="Cargando" />
      </div>
    );
  }

  return (
    <AuthContext.Provider value={user}>
      <header className="appbar">
        <div className="appbar-inner">
          <Link href="/inicio" className="brand">
            <img
              src="/images/company_logo.png"
              alt=""
              className="brand-mark"
            />
            <span className="brand-text">
              <span className="brand-name">Jorge Cuadros</span>
              <span className="brand-sub">& Asociados</span>
            </span>
          </Link>
          <nav className="appbar-nav" aria-label="Principal">
            {nav.map((entry) =>
              entry.kind === "link" ? (
                <Link
                  key={entry.href}
                  href={entry.href}
                  className={`appbar-link${current === entry.href ? " active" : ""}`}
                  aria-current={current === entry.href ? "page" : undefined}
                >
                  {entry.label}
                </Link>
              ) : (
                <NavMenu
                  key={entry.label}
                  label={entry.label}
                  items={entry.items}
                  current={current}
                  pathname={pathname}
                />
              ),
            )}
          </nav>
          <span className="appbar-spacer" />
          <div className="appbar-user">
            <FontScaleControl value={uiScale} onChange={changeUiScale} />
            {user && (
              <span className="appbar-user-name">
                {user.name}
                <span className="appbar-user-role">{ROLE_LABEL[user.role]}</span>
              </span>
            )}
            <button
              type="button"
              className="btn btn-ghost"
              onClick={handleLogout}
              disabled={loggingOut}
            >
              {loggingOut ? "Saliendo…" : "Cerrar sesión"}
            </button>
          </div>
          <button
            type="button"
            className="appbar-burger"
            aria-label={drawerOpen ? "Cerrar menú" : "Abrir menú"}
            aria-expanded={drawerOpen}
            onClick={() => setDrawerOpen((v) => !v)}
          >
            <span className={`burger-icon${drawerOpen ? " open" : ""}`} aria-hidden="true" />
          </button>
        </div>
        {drawerOpen && (
          <nav className="appbar-drawer" aria-label="Principal (móvil)">
            {nav.map((entry) =>
              entry.kind === "link" ? (
                <Link
                  key={entry.href}
                  href={entry.href}
                  className={`appbar-drawer-link${current === entry.href ? " active" : ""}`}
                  aria-current={current === entry.href ? "page" : undefined}
                >
                  {entry.label}
                </Link>
              ) : (
                <div key={entry.label} className="appbar-drawer-group">
                  <span className="appbar-drawer-heading">{entry.label}</span>
                  {entry.items.map((item) => (
                    <Link
                      key={item.href}
                      href={item.href}
                      className={`appbar-drawer-link${current === item.href ? " active" : ""}`}
                      aria-current={current === item.href ? "page" : undefined}
                    >
                      {item.label}
                    </Link>
                  ))}
                </div>
              ),
            )}
            <FontScaleControl
              value={uiScale}
              onChange={changeUiScale}
              variant="inline"
            />
            {user && (
              <div className="appbar-drawer-user">
                {user.name} · {ROLE_LABEL[user.role]}
              </div>
            )}
          </nav>
        )}
      </header>
      <main className="shell-main">{children}</main>
      <BuildFooter />
    </AuthContext.Provider>
  );
}
