/**
 * Accessible tabs component implementing the WAI-ARIA Tabs pattern.
 *
 * Reference: https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
 *
 * Built as a custom component instead of using Base UI because Astro islands
 * cannot share React Context across separate component instances. We use
 * nanostores for cross-island state management instead.
 *
 * Oh, and by the way. The first key in the titles object OR the first panel
 * will be used as the default active tab. Ensure that TabsPanel children
 * are provided in the same order for predictable behavior.
 */

import type { KeyboardEvent, ReactNode } from 'react';

import { useStore } from '@nanostores/react';
import clsx from 'clsx';
import { Check, Copy } from 'lucide-react';

import { twMerge } from 'tailwind-merge';
import CopyButton from '@/components/CopyButton';
import { $tabs } from '@/stores/tabs';
import useIsHydrated from '@/utils/useIsHydrated';

interface TabsRootProps {
  /** Unique ID for this tabs instance. Required for both Astro and React usage. Generated by Rehype in MDX */
  id: string;
  /** Accessible label for the tablist */
  'aria-label': string;
  /** Additional CSS classes */
  className?: string;
  /**
   * TabsPanel children.
   * Order of children should match the order of keys in titles object.
   * The first key in titles or the first panel will be the default active tab.
   */
  children: ReactNode;
  /**
   * Map of tab values to their display labels.
   * Order of children should match the order of keys in this object.
   * The first key in titles or the first panel will be the default active tab.
   */
  titles: Record<string, string>;

  maxWidth?: boolean;
}

export function TabsRoot({
  id,
  'aria-label': ariaLabel,
  titles,
  className,
  children,
  maxWidth = true,
}: TabsRootProps) {
  const isHydrated = useIsHydrated();

  // Derive default from first item in titles
  // This assumes titles keys and TabsPanel children are in the same order...
  // Which is... unfortunate. But. Astro's gonna astro. It's hard to communicate between components.
  const defaultValue = Object.keys(titles)[0];
  if (!defaultValue) {
    throw new Error('TabsRoot requires at least one item in titles.');
  }

  const currentState = $tabs.get();
  if (currentState[id] === undefined) {
    $tabs.setKey(id, defaultValue);
  }

  // Subscribe to the tabs store
  const allTabsState = useStore($tabs);

  // Get current active value, or use first item as fallback
  const activeValue = allTabsState[id] ?? defaultValue;

  const handleTabClick = (value: string) => {
    $tabs.setKey(id, value);
  };

  const values = Object.keys(titles);

  const handleKeyDown = (e: KeyboardEvent, currentIndex: number) => {
    let newIndex: number | null = null;

    switch (e.key) {
      case 'ArrowLeft':
        newIndex = currentIndex - 1;
        if (newIndex < 0) newIndex = values.length - 1; // Circular
        break;
      case 'ArrowRight':
        newIndex = currentIndex + 1;
        if (newIndex >= values.length) newIndex = 0; // Circular
        break;
      case 'Home':
        newIndex = 0;
        break;
      case 'End':
        newIndex = values.length - 1;
        break;
    }

    if (newIndex !== null) {
      e.preventDefault();
      const newValue = values[newIndex];
      $tabs.setKey(id, newValue);
      // Focus the new tab
      const newTabElement = document.getElementById(`${id}-tab-${newValue}`);
      newTabElement?.focus();
    }
  };

  return (
    <div
      className={
        twMerge(
          clsx(
            'rounded-lg overflow-hidden border border-light-40 dark:border-dark-80',
            'bg-light-100 dark:bg-dark-110 flex flex-col',
            'my-6',
            maxWidth && 'w-full max-w-3xl mx-auto',
            className,
          ),
        )
      }
      id={id}
    >
      <div className="w-full border-b border-light-40 dark:border-dark-80 flex bg-light-80 dark:bg-dark-100 overflow-x-scroll not-content">
        <ul role="tablist" aria-label={ariaLabel} className="flex list-none p-0 m-0">
          {values.map((value, index) => {
            const isActive = value === activeValue;
            const isLoading = !isHydrated && !isActive;

            return (
              <li key={value} role="presentation" className="flex">
                <button
                  type="button"
                  role="tab"
                  id={`${id}-tab-${value}`}
                  aria-selected={isActive}
                  aria-controls={`${id}-panel-${value}`}
                  tabIndex={isActive ? 0 : -1}
                  onClick={() => handleTabClick(value)}
                  onKeyDown={e => handleKeyDown(e, index)}
                  className={clsx(
                    'flex items-center h-9 px-4 py-2 text-sm',
                    'border-x border-light-40 dark:border-dark-80',
                    'first:-ml-px last:-mr-px -mx-[0.5px] no-underline',
                    isActive
                      ? 'bg-light-100 dark:bg-dark-110'
                      : 'bg-light-80 dark:bg-dark-100',
                    isLoading ? 'cursor-wait' : 'cursor-pointer intent:bg-light-100 dark:intent:bg-dark-110',
                  )}
                >
                  {titles[value]}
                </button>
              </li>
            );
          })}
        </ul>
        <CopyButton
          copyFrom={{
            container: `#${id}`,
            target: '[role="tabpanel"]:not([hidden])',
          }}
          className="ml-auto sticky right-0 border-l border-light-40 dark:border-dark-80 h-9 w-9 flex items-center justify-center not-disabled:intent:bg-light-100 dark:not-disabled:intent:bg-dark-110 cursor-pointer disabled:cursor-wait"
          copied={<Check size={16} />}
        >
          <Copy size={16} />
        </CopyButton>
      </div>
      {children}
    </div>
  );
}

interface TabsPanelProps {
  /** Unique ID matching the parent TabsRoot. Required for both Astro and React usage.  Generated by Rehype in MDX */
  tabsId: string;
  /** The value this panel corresponds to */
  value: string;
  /** Additional CSS classes */
  className?: string;
  /** Panel content */
  children: ReactNode;
}

export function TabsPanel({ tabsId, value, className, children }: TabsPanelProps) {
  // Initialize tabs store if necessary
  // Astro SOMETIMES renders TabsPanel before TabsRoot, unfortunately
  // so, in this case, let's assume this is the first TabsPanel in order
  // and set ourselves as the active one
  // I hate this, but it seems to work.
  const currentState = $tabs.get();
  if (currentState[tabsId] === undefined) {
    $tabs.setKey(tabsId, value);
  }

  // Subscribe to the tabs store
  const allTabsState = useStore($tabs);
  const activeValue = allTabsState[tabsId];
  const isActive = activeValue === value; ;

  return (
    <div
      role="tabpanel"
      id={`${tabsId}-panel-${value}`}
      aria-labelledby={`${tabsId}-tab-${value}`}
      hidden={!isActive}
      className={clsx('overflow-scroll p-6 max-h-96 flex-1', className)}
      tabIndex={0}
    >
      {children}
    </div>
  );
}
