'use client';

import { useEffect, useState, type ComponentProps, type ReactNode } from 'react';
import { usePathname, useRouter } from 'next/navigation';

import { cn } from '@/lib/utils';
import { usePermissions } from '@/hooks/use-permissions';
import { Sidebar } from '@/components/layout/sidebar';
import { Topbar } from '@/components/layout/topbar';
import { NavigationHistoryTracker } from '@/components/layout/navigation-history-tracker';
import { MobileLayoutProvider } from '@/components/layout/mobile/mobile-layout-provider';
import { MobileTopbar } from '@/components/layout/mobile/mobile-topbar';
import { MobileBottomTabs } from '@/components/layout/mobile/mobile-bottom-tabs';
import { MoreSheet } from '@/components/layout/mobile/more-sheet';
import { MobileSearchOverlay } from '@/components/search/mobile-search-overlay';
import { Sheet, SheetContent } from '@/components/ui/sheet';
import { useUIStore } from '@/stores/ui-store';

type SidebarProps = ComponentProps<typeof Sidebar>;
type TopbarProps = ComponentProps<typeof Topbar>;

interface AppShellProps {
  portRoles: SidebarProps['portRoles'];
  isSuperAdmin: boolean;
  user: NonNullable<SidebarProps['user']>;
  ports: TopbarProps['ports'];
  /** Per-port logo URLs resolved server-side. Sidebar picks the entry
   *  matching the currently-active port from the UI store. */
  portLogoUrls: Record<string, string | null>;
  /** Per-port `tenancies_module_enabled` resolution. Gates the Tenancies
   *  sidebar entry SSR-side so the nav doesn't flicker in/out. */
  tenanciesModuleByPort: Record<string, boolean>;
  /** Per-port `expenses_module_enabled` resolution. Gates the Expenses
   *  + How-to-upload-receipts sidebar entries SSR-side. Defaults to
   *  true so existing ports keep the feature. */
  expensesModuleByPort: Record<string, boolean>;
  /** Per-port `residential_module_enabled` resolution. Gates the
   *  "Residential" sidebar section + mobile entry SSR-side. Defaults to
   *  true so existing ports keep the feature. */
  residentialModuleByPort: Record<string, boolean>;
  /**
   * Server-rendered form-factor hint (from the request User-Agent). The
   * shell mounts the matching tree on first render so we never paint the
   * wrong shell, and only switches if the runtime viewport matchMedia
   * disagrees (e.g. desktop browser resized below lg).
   */
  initialFormFactor: 'mobile' | 'desktop';
  children: ReactNode;
}

// Three tiers, aligned with Tailwind and the `useViewportTier` hook:
//   mobile  : < 768px   (sm and below) → mobile shell (bottom tabs, slide-over more sheet)
//   tablet  : 768-1023  (md)            → desktop shell, sidebar wrapped in Sheet, logo trigger in topbar
//   desktop : >= 1024   (lg and up)     → desktop shell, sidebar always visible
//
// Previously the app used a binary `(max-width: 1023.98px)` split, which
// rendered the mobile shell on iPad portrait AND on a half-screen 13"
// browser. Neither is really "mobile"; the tablet tier fills that gap.
const MOBILE_QUERY = '(max-width: 767.98px)';
const TABLET_QUERY = '(min-width: 768px) and (max-width: 1023.98px)';

type Tier = 'mobile' | 'tablet' | 'desktop';

function computeInitialTier(initialFormFactor: 'mobile' | 'desktop'): Tier {
  // Server UA classification is binary; we can only distinguish "looks
  // like a phone/tablet UA" from "looks like a desktop UA." Tablets get
  // classified as 'mobile' by the UA token check (iPad/Android), so
  // initialFormFactor=mobile on first paint covers both mobile + tablet.
  // The matchMedia subscription below corrects to the precise tier on
  // hydration without a flash because useSyncExternalStore is mismatch-safe.
  return initialFormFactor === 'mobile' ? 'mobile' : 'desktop';
}

/**
 * #26 + H-09: single-tree responsive shell with stable children subtree.
 *
 * The shell renders ONE `<main>` and ONE `<MobileLayoutProvider>` at all
 * viewports - only the chrome (sidebar+topbar vs mobile-topbar+bottom-tabs
 * vs tablet's hidden-sidebar-via-Sheet) conditionally renders. Three payoffs:
 *
 *   - #26 / first ship: no double-mount of chrome subtrees.
 *   - H-09: `{children}` stays mounted across viewport flips. A rep
 *     editing an inline field on desktop who resizes through the mobile
 *     breakpoint no longer loses the draft mid-edit.
 *   - Tier-aware sidebar: tablet width gets the desktop shell with
 *     sidebar hidden behind a Sheet (slide-over from left, opened by
 *     the topbar logo button) instead of falling all the way back to
 *     the mobile shell. Closes the half-screen-on-13"-Mac usability gap.
 *
 * The mobile-only floating panels (MoreSheet, MobileSearchOverlay) only
 * mount in the mobile branch - they have no desktop counterpart.
 *
 * SSR safety: the server passes its UA-classified hint via `initialFormFactor`;
 * the first client render uses the same value so hydration matches. After
 * mount, a matchMedia subscription overrides to the precise tier.
 */
export function AppShell({
  portRoles,
  isSuperAdmin,
  user,
  ports,
  portLogoUrls,
  tenanciesModuleByPort,
  expensesModuleByPort,
  residentialModuleByPort,
  initialFormFactor,
  children,
}: AppShellProps) {
  const [tier, setTier] = useState<Tier>(computeInitialTier(initialFormFactor));
  const [moreOpen, setMoreOpen] = useState(false);
  const [searchOpen, setSearchOpen] = useState(false);
  const [tabletSidebarOpen, setTabletSidebarOpen] = useState(false);
  const currentPortSlug = useUIStore((s) => s.currentPortSlug);
  const currentPortId = useUIStore((s) => s.currentPortId);
  const logoUrl = currentPortSlug ? portLogoUrls[currentPortSlug] : null;

  // Residential lockdown: a residential-only user (residential access, no
  // marina `clients.view`) must never see marina pages — including the marina
  // dashboard. The API already 403s their data; this guard blocks the *routes*,
  // redirecting any non-residential path to their residential home. Personal
  // surfaces (settings, inbox) stay reachable.
  const pathname = usePathname();
  const router = useRouter();
  const { can } = usePermissions();
  const residentialOnly =
    !isSuperAdmin && can('residential_clients', 'view') && !can('clients', 'view');
  useEffect(() => {
    if (!residentialOnly || !pathname) return;
    const [portSeg, ...rest] = pathname.split('/').filter(Boolean);
    const sub = rest.join('/');
    const allowed =
      sub === '' ||
      sub.startsWith('residential') ||
      sub.startsWith('settings') ||
      sub.startsWith('inbox');
    if (!allowed && portSeg) {
      router.replace(`/${portSeg}/residential/clients`);
    }
  }, [residentialOnly, pathname, router]);

  useEffect(() => {
    const mqMobile = window.matchMedia(MOBILE_QUERY);
    const mqTablet = window.matchMedia(TABLET_QUERY);
    const update = () => {
      const next: Tier = mqMobile.matches ? 'mobile' : mqTablet.matches ? 'tablet' : 'desktop';
      setTier(next);
      // Persist for the next SSR pass so the server renders the
      // matching shell on first paint - eliminates the chrome flicker
      // on refresh when UA-based classification disagrees with the
      // actual viewport (most common on macOS Safari at sub-1024
      // widths). 1-year expiry; SameSite=Lax is fine since the cookie
      // is read by our own server only.
      document.cookie = `pn-crm.viewport-tier=${next}; path=/; max-age=31536000; SameSite=Lax`;
    };
    update();
    mqMobile.addEventListener('change', update);
    mqTablet.addEventListener('change', update);
    return () => {
      mqMobile.removeEventListener('change', update);
      mqTablet.removeEventListener('change', update);
    };
  }, []);

  const isMobile = tier === 'mobile';
  const isTablet = tier === 'tablet';

  // Close the tablet sheet when crossing breakpoints so it doesn't stay
  // "open" after a resize back to desktop (Sheet keeps its open prop).
  useEffect(() => {
    if (!isTablet && tabletSidebarOpen) {
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setTabletSidebarOpen(false);
    }
  }, [isTablet, tabletSidebarOpen]);

  const sidebarProps: SidebarProps = {
    portRoles,
    isSuperAdmin,
    user,
    ports,
    portLogoUrls,
    tenanciesModuleByPort,
    expensesModuleByPort,
    residentialModuleByPort,
  };

  // Resolve the current port's residential flag for the mobile More sheet
  // (the sidebar resolves its own copy internally from the by-port map).
  const residentialModuleEnabled = currentPortId
    ? (residentialModuleByPort[currentPortId] ?? true)
    : true;

  // Chrome subtree per tier.
  let chrome: ReactNode = null;
  if (isMobile) {
    chrome = <MobileTopbar />;
  } else if (isTablet) {
    // Tablet: sidebar lives inside a left-side Sheet, opened by the
    // topbar's leading logo button. SheetContent has its own width;
    // override to match the inline Sidebar's width so the layout
    // reads consistent when the sheet is open.
    chrome = (
      <Sheet open={tabletSidebarOpen} onOpenChange={setTabletSidebarOpen}>
        <SheetContent side="left" className="p-0 w-[var(--width-sidebar)]">
          <Sidebar {...sidebarProps} />
        </SheetContent>
      </Sheet>
    );
  } else {
    chrome = <Sidebar {...sidebarProps} />;
  }

  const footer = isMobile ? (
    <>
      <MobileBottomTabs
        onMoreClick={() => setMoreOpen(true)}
        onSearchClick={() => setSearchOpen(true)}
      />
      <MoreSheet
        open={moreOpen}
        onOpenChange={setMoreOpen}
        residentialModuleEnabled={residentialModuleEnabled}
      />
      <MobileSearchOverlay open={searchOpen} onOpenChange={setSearchOpen} />
    </>
  ) : null;

  // Desktop topbar; on tablet it gains a leading logo button that
  // opens the sidebar Sheet.
  const desktopTopbar = !isMobile ? (
    <Topbar
      ports={ports}
      user={user}
      leadingSlot={
        isTablet ? (
          <button
            type="button"
            aria-label="Open menu"
            onClick={() => setTabletSidebarOpen(true)}
            className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md hover:bg-accent transition-colors"
          >
            {logoUrl ? (
              <img src={logoUrl} alt="" className="h-6 w-6 object-contain" />
            ) : (
              // Neutral fallback when the port has no branding logo yet -
              // a three-bar menu icon keeps the affordance discoverable.
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth="2"
                strokeLinecap="round"
                strokeLinejoin="round"
                className="h-4 w-4"
                aria-hidden
              >
                <line x1="3" y1="6" x2="21" y2="6" />
                <line x1="3" y1="12" x2="21" y2="12" />
                <line x1="3" y1="18" x2="21" y2="18" />
              </svg>
            )}
          </button>
        ) : null
      }
    />
  ) : null;

  return (
    <MobileLayoutProvider>
      {/* Records every in-app navigation so useSmartBack can return the
          rep to the page they were actually on (e.g. Sarah Doe -> Yacht
          -> Back -> Sarah) instead of always falling back to the
          logical URL parent. Renders nothing. */}
      <NavigationHistoryTracker />
      <div
        className={cn(
          'bg-background',
          isMobile ? 'min-h-[100dvh]' : 'flex h-screen overflow-hidden',
        )}
      >
        {chrome}
        <div className={cn(isMobile ? 'contents' : 'flex-1 flex flex-col overflow-hidden min-w-0')}>
          {desktopTopbar}
          <main
            className={cn(
              isMobile
                ? 'px-4 min-h-[100dvh] pt-[calc(56px+env(safe-area-inset-top)+1rem)] pb-[calc(56px+env(safe-area-inset-bottom)+2rem)]'
                : 'flex-1 overflow-y-auto bg-background px-6 pt-3 pb-6',
            )}
          >
            {children}
          </main>
        </div>
        {footer}
      </div>
    </MobileLayoutProvider>
  );
}
