/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
 */

import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import * as LucideIcons from 'lucide-react';
const { ChevronDown, Lock } = LucideIcons;
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { EnterpriseUpsell } from '@/components/common/EnterpriseUpsell';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useUIStore } from '@/stores/uiStore';
import { useAccountStore } from '@/stores/accountStore';
import { useSchemaStore } from '@/stores/schemaStore';
import {
  visibleLayouts,
  findFirstVisibleLinkInLayout,
  findFirstAccessibleLinkInLayout,
  isLinkEnterprise,
  isLinkVisible,
} from '@/lib/layout';
import { findLastVisitedLinkInLayout, setLastVisitedSection } from '@/lib/lastVisited';
import type { Layout, LayoutItem, LayoutSubItem } from '@/types/schema';

function LucideIcon({ name, className }: { name: string; className?: string }) {
  const formatted = name
    .split('-')
    .map((s) => s[0].toUpperCase() + s.slice(1))
    .join('');
  const IconComp = (LucideIcons as Record<string, unknown>)[formatted] as LucideIcons.LucideIcon | undefined;
  if (!IconComp) return <LucideIcons.Circle className={className} />;
  return <IconComp className={className} />;
}

function resolveViewPath(sectionName: string, viewName: string): string {
  return `/${sectionName}/${viewName}`;
}

function pathMatchesView(currentPath: string, sectionName: string, viewName: string): boolean {
  const base = `/${sectionName}/${viewName}`;
  if (currentPath === base || currentPath.startsWith(`${base}/`)) return true;
  if (viewName === 'CustomComponent/Dashboard') {
    const dashBase = `/${sectionName}/Dashboard/`;
    return currentPath.startsWith(dashBase);
  }
  return false;
}

function subtreeContainsActive(items: LayoutSubItem[], currentPath: string, sectionName: string): boolean {
  for (const item of items) {
    if (item.type === 'link') {
      if (pathMatchesView(currentPath, sectionName, item.viewName)) return true;
    } else if (item.type === 'container') {
      if (subtreeContainsActive(item.items, currentPath, sectionName)) return true;
    }
  }
  return false;
}

function subtreeHasVisibleLink(items: LayoutSubItem[], edition: string): boolean {
  for (const item of items) {
    if (item.type === 'link') {
      if (!checkLinkVisible(item.viewName)) continue;
      const enterprise = checkIsEnterprise(item.viewName);
      if (enterprise && edition === 'oss') continue;
      return true;
    } else if (item.type === 'container') {
      if (subtreeHasVisibleLink(item.items, edition)) return true;
    }
  }
  return false;
}

interface AccordionLevelContextValue {
  openId: string | null;
  setOpenId: (id: string | null) => void;
}

const AccordionLevelContext = createContext<AccordionLevelContextValue | null>(null);

// Sibling collapsibles share a single open id, so expanding one collapses the
// others at the same level (accordion behavior).
function AccordionLevel({ children }: { children: React.ReactNode }) {
  const [openId, setOpenId] = useState<string | null>(null);
  const value = useMemo(() => ({ openId, setOpenId }), [openId]);
  return <AccordionLevelContext.Provider value={value}>{children}</AccordionLevelContext.Provider>;
}

// A collapsible wired to its accordion level. It opens itself whenever the
// active page lands inside it, while still allowing manual toggling.
function AccordionCollapsible({
  id,
  containsActive,
  children,
}: {
  id: string;
  containsActive: boolean;
  children: React.ReactNode;
}) {
  const level = useContext(AccordionLevelContext);
  if (!level) throw new Error('AccordionCollapsible must be used within AccordionLevel');
  const { openId, setOpenId } = level;
  // Layout effect so the branch containing the active page is already open on
  // the first paint after a navigation.
  useLayoutEffect(() => {
    if (containsActive) setOpenId(id);
  }, [containsActive, id, setOpenId]);
  return (
    <Collapsible open={openId === id} onOpenChange={(open) => setOpenId(open ? id : null)}>
      {children}
    </Collapsible>
  );
}

// Softer than the default ghost hover, closer to documentation sidebars:
// muted text that brightens with a faint background instead of a strong fill.
const sidebarItemClass =
  'w-full justify-start gap-2 font-normal text-muted-foreground hover:bg-accent/50 hover:text-foreground';

// In square mode the sidebar follows documentation conventions (better-auth):
// full-bleed rows with a barely-there hover wash instead of inset pills.
const sidebarItemSquareClass =
  "[[data-radius='square']_&]:hover:bg-foreground/[0.03] [[data-radius='square']_&]:hover:text-foreground/90";

function checkLinkVisible(viewName: string): boolean {
  const schema = useSchemaStore.getState().schema;
  if (!schema) return true;

  const accountStore = useAccountStore.getState();
  return isLinkVisible(
    schema,
    viewName,
    accountStore.edition,
    (prefix: string) => accountStore.hasObjectPermission(prefix, 'Get'),
    (perm: string) => accountStore.hasPermission(perm),
  );
}

function checkIsEnterprise(viewName: string): boolean {
  const schema = useSchemaStore.getState().schema;
  if (!schema) return false;
  const edition = useAccountStore.getState().edition;
  return isLinkEnterprise(schema, viewName, edition);
}

interface SidebarSubItemProps {
  item: LayoutSubItem;
  depth: number;
  sectionName: string;
  currentPath: string;
  navigate: ReturnType<typeof useNavigate>;
  edition: string;
  onUpsell: () => void;
}

function SidebarSubItem({ item, depth, sectionName, currentPath, navigate, edition, onUpsell }: SidebarSubItemProps) {
  // Picking a plain link closes any sibling group left open at this same
  // accordion level — it's not part of a collapsible, so nothing should
  // stay expanded on its account once it's the one that's active.
  const level = useContext(AccordionLevelContext);

  if (item.type === 'link') {
    if (!checkLinkVisible(item.viewName)) return null;

    const path = resolveViewPath(sectionName, item.viewName);
    const isActive = pathMatchesView(currentPath, sectionName, item.viewName);
    const enterprise = checkIsEnterprise(item.viewName);
    const isLocked = enterprise && edition === 'community';
    const isHidden = enterprise && edition === 'oss';

    if (isHidden) return null;

    return (
      <Button
        variant="ghost"
        data-sidebar-active={isActive || undefined}
        className={cn(
          sidebarItemClass,
          sidebarItemSquareClass,
          isActive && 'bg-accent text-accent-foreground hover:bg-accent',
          depth > 0 && 'text-sm',
        )}
        style={{ paddingLeft: `${(depth + 1) * 12 + 8}px` }}
        onClick={() => {
          if (isLocked) {
            onUpsell();
          } else {
            level?.setOpenId(null);
            setLastVisitedSection(sectionName, item.viewName);
            navigate(path);
          }
        }}
      >
        <span className="truncate">{item.name || 'Overview'}</span>
        {isLocked && <Lock className="ml-auto h-3 w-3 text-muted-foreground" />}
      </Button>
    );
  }

  if (item.type === 'container') {
    if (!subtreeHasVisibleLink(item.items, edition)) return null;

    const containsActive = subtreeContainsActive(item.items, currentPath, sectionName);
    return (
      <AccordionCollapsible id={item.name} containsActive={containsActive}>
        <CollapsibleTrigger asChild>
          <Button
            variant="ghost"
            className={cn(sidebarItemClass, sidebarItemSquareClass, 'text-sm')}
            style={{ paddingLeft: `${(depth + 1) * 12 + 8}px` }}
          >
            <ChevronDown className="h-3 w-3 shrink-0 transition-transform duration-200 [[data-state=closed]>&]:rotate-[-90deg]" />
            <span className="truncate">{item.name}</span>
          </Button>
        </CollapsibleTrigger>
        <CollapsibleContent>
          <AccordionLevel>
            {item.items.map((sub) => (
              <SidebarSubItem
                key={sub.type === 'link' ? sub.viewName : sub.name}
                item={sub}
                depth={depth + 1}
                sectionName={sectionName}
                currentPath={currentPath}
                navigate={navigate}
                edition={edition}
                onUpsell={onUpsell}
              />
            ))}
          </AccordionLevel>
        </CollapsibleContent>
      </AccordionCollapsible>
    );
  }

  return null;
}

interface SidebarTopItemProps {
  item: LayoutItem;
  sectionName: string;
  currentPath: string;
  navigate: ReturnType<typeof useNavigate>;
  edition: string;
  onUpsell: () => void;
}

function SidebarTopItem({ item, sectionName, currentPath, navigate, edition, onUpsell }: SidebarTopItemProps) {
  // Picking a plain link closes any sibling group left open at this same
  // accordion level — it's not part of a collapsible, so nothing should
  // stay expanded on its account once it's the one that's active.
  const level = useContext(AccordionLevelContext);

  if ('link' in item) {
    const { name, icon, viewName } = item.link;

    if (!checkLinkVisible(viewName)) return null;

    const path = resolveViewPath(sectionName, viewName);
    const isActive = pathMatchesView(currentPath, sectionName, viewName);
    const enterprise = checkIsEnterprise(viewName);
    const isLocked = enterprise && edition === 'community';
    const isHidden = enterprise && edition === 'oss';

    if (isHidden) return null;

    return (
      <Button
        variant="ghost"
        data-sidebar-active={isActive || undefined}
        className={cn(
          sidebarItemClass,
          sidebarItemSquareClass,
          isActive && 'bg-accent text-accent-foreground hover:bg-accent',
        )}
        onClick={() => {
          if (isLocked) {
            onUpsell();
          } else {
            level?.setOpenId(null);
            setLastVisitedSection(sectionName, viewName);
            navigate(path);
          }
        }}
      >
        <LucideIcon name={icon} className="h-4 w-4 shrink-0" />
        <span className="truncate">{name}</span>
        {isLocked && <Lock className="ml-auto h-3 w-3 text-muted-foreground" />}
      </Button>
    );
  }

  if ('container' in item) {
    const { name, icon, items } = item.container;
    if (!subtreeHasVisibleLink(items, edition)) return null;

    const containsActive = subtreeContainsActive(items, currentPath, sectionName);

    return (
      <AccordionCollapsible id={name} containsActive={containsActive}>
        <CollapsibleTrigger asChild>
          <Button variant="ghost" className={cn(sidebarItemClass, sidebarItemSquareClass)}>
            <LucideIcon name={icon} className="h-4 w-4 shrink-0" />
            <span className="truncate">{name}</span>
            <ChevronDown className="ml-auto h-3 w-3 shrink-0 transition-transform duration-200 [[data-state=closed]>&]:rotate-[-90deg]" />
          </Button>
        </CollapsibleTrigger>
        <CollapsibleContent>
          <AccordionLevel>
            {items.map((sub) => (
              <SidebarSubItem
                key={sub.type === 'link' ? sub.viewName : sub.name}
                item={sub}
                depth={1}
                sectionName={sectionName}
                currentPath={currentPath}
                navigate={navigate}
                edition={edition}
                onUpsell={onUpsell}
              />
            ))}
          </AccordionLevel>
        </CollapsibleContent>
      </AccordionCollapsible>
    );
  }

  return null;
}

export function Sidebar() {
  const navigate = useNavigate();
  const location = useLocation();
  const activeSection = useUIStore((s) => s.activeSection);
  const setActiveSection = useUIStore((s) => s.setActiveSection);
  const sidebarOpen = useUIStore((s) => s.sidebarOpen);
  const setSidebarOpen = useUIStore((s) => s.setSidebarOpen);
  const schema = useSchemaStore((s) => s.schema);
  const edition = useAccountStore((s) => s.edition);
  const permissions = useAccountStore((s) => s.permissions);
  const hasPermission = useAccountStore((s) => s.hasPermission);
  const [upsellOpen, setUpsellOpen] = useState(false);
  const navRef = useRef<HTMLElement>(null);
  // Set by handleSectionClick right before navigating: switching sections
  // from the footer should keep the sidebar open so the new section's pages
  // are still browsable, instead of closing right back up like a leaf-page
  // navigation would.
  const skipCloseOnNavigateRef = useRef(false);

  // Build the permission checks from the permissions array itself: the store
  // accessors are stable refs, so depending on them alone would keep a stale
  // layout list after access data finishes loading.
  const layouts = useMemo(() => {
    if (!schema) return [];
    const canGet = (prefix: string) => permissions.includes(`${prefix}Get`);
    return visibleLayouts(schema, edition, canGet, hasPermission);
  }, [schema, edition, permissions, hasPermission]);

  useEffect(() => {
    if (typeof window === 'undefined') return;
    if (skipCloseOnNavigateRef.current) {
      skipCloseOnNavigateRef.current = false;
      return;
    }
    if (window.matchMedia('(max-width: 767px)').matches) {
      setSidebarOpen(false);
    }
  }, [location.pathname, setSidebarOpen]);

  // Keep the active page visible in the sidebar after any navigation
  // (e.g. from the command palette or an external link).
  useEffect(() => {
    const active = navRef.current?.querySelector('[data-sidebar-active="true"]');
    active?.scrollIntoView({ block: 'nearest' });
  }, [location.pathname, activeSection]);

  if (!sidebarOpen || !schema) return null;

  const layout: Layout | undefined = layouts.find((l) => l.name === activeSection);
  if (!layout) return null;

  const handleSectionClick = (target: Layout) => {
    setActiveSection(target.name);
    const canGet = (prefix: string) => permissions.includes(`${prefix}Get`);
    const last = findLastVisitedLinkInLayout(schema, target, edition, canGet, hasPermission);
    const first =
      last ??
      findFirstAccessibleLinkInLayout(schema, target, edition, canGet, hasPermission) ??
      findFirstVisibleLinkInLayout(schema, target, edition, canGet, hasPermission);
    if (first) {
      skipCloseOnNavigateRef.current = true;
      navigate(`/${target.name}/${first}`);
    }
  };

  return (
    <>
      <div
        aria-hidden="true"
        className="fixed inset-0 top-14 z-20 bg-black/40 md:hidden"
        onClick={() => setSidebarOpen(false)}
      />
      <aside className="fixed top-14 left-0 bottom-0 z-30 flex w-64 flex-col border-r bg-background">
        <ScrollArea className="flex-1">
          <nav
            ref={navRef}
            className="flex flex-col gap-0.5 px-2 py-2 [[data-radius='square']_&]:gap-0 [[data-radius='square']_&]:px-0"
          >
            <AccordionLevel>
              {layout.items.map((item) => (
                <SidebarTopItem
                  key={'link' in item ? item.link.viewName : item.container.name}
                  item={item}
                  sectionName={layout.name}
                  currentPath={location.pathname}
                  navigate={navigate}
                  edition={edition}
                  onUpsell={() => setUpsellOpen(true)}
                />
              ))}
            </AccordionLevel>
          </nav>
        </ScrollArea>

        {layouts.length > 1 && (
          <TooltipProvider>
            <div className="flex items-center justify-around border-t bg-background px-2 py-2 [[data-radius='square']_&]:px-0 [[data-radius='square']_&]:py-0">
              {layouts.map((target) => {
                const Icon = (LucideIcons as Record<string, unknown>)[
                  target.icon
                    .split('-')
                    .map((s) => s[0].toUpperCase() + s.slice(1))
                    .join('')
                ] as LucideIcons.LucideIcon | undefined;
                const isActive = target.name === activeSection;
                return (
                  <Tooltip key={target.name}>
                    <TooltipTrigger asChild>
                      <Button
                        type="button"
                        variant="ghost"
                        size="icon"
                        aria-label={target.name}
                        aria-current={isActive ? 'page' : undefined}
                        onClick={() => handleSectionClick(target)}
                        className={cn(
                          'h-9 w-9',
                          "[[data-radius='square']_&]:h-12 [[data-radius='square']_&]:flex-1 [[data-radius='square']_&]:border-r [[data-radius='square']_&]:border-border [[data-radius='square']_&]:last:border-r-0 [[data-radius='square']_&]:hover:bg-foreground/[0.03]",
                          isActive && 'bg-accent text-accent-foreground',
                        )}
                      >
                        {Icon ? <Icon className="h-4 w-4" /> : <LucideIcons.Circle className="h-4 w-4" />}
                      </Button>
                    </TooltipTrigger>
                    <TooltipContent side="top">{target.name}</TooltipContent>
                  </Tooltip>
                );
              })}
            </div>
          </TooltipProvider>
        )}

        <EnterpriseUpsell open={upsellOpen} onClose={() => setUpsellOpen(false)} />
      </aside>
    </>
  );
}
