'use client';

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { ReactNode } from 'react';

import { useHotkey } from '@djangocfg/ui-core/hooks';

import { ChatProvider, useChatContextOptional } from '../context';
import type { ChatAudioConfig } from '../core/audio/types';
import { useChatDockPrefs } from '../hooks/useChatDockPrefs';
import type {
  ChatConfig,
  ChatMessage,
  ChatTransport,
} from '../types';

import { ChatFAB, type ChatFABPosition, type ChatFABProps } from './ChatFAB';
import { ChatDock, type ChatDockProps } from './ChatDock';
import { ChatGreeting, type ChatGreetingProps } from './ChatGreeting';
import { ChatUnreadPreview, type ChatUnreadPreviewProps } from './ChatUnreadPreview';
import { HeaderSlotsRenderer } from './header';
import { resolveHeaderSlots, type ChatHeaderSlots } from './types';

export interface ChatLauncherHotkey {
  /** Key (case-sensitive single char or named like 'Escape'). */
  key: string;
  /** Require Cmd (mac) or Ctrl (other). */
  meta?: boolean;
  /** Require Shift. */
  shift?: boolean;
  /** Require Alt. */
  alt?: boolean;
}

export interface ChatLauncherGreeting
  extends Omit<ChatGreetingProps, 'open' | 'onClick' | 'onDismiss' | 'position' | 'fabOffset' | 'children'> {
  /** Greeting body — string for the default style, or any ReactNode. */
  content: ReactNode;
  /** Persistence key for "user dismissed this greeting" in `localStorage`. Pass `null` to disable persistence. @default null */
  dismissStorageKey?: string | null;
  /** Hide the greeting once the user opens the chat. @default true */
  hideOnOpen?: boolean;
}

export interface ChatLauncherProps {
  // ---- chat-provider wiring (mounts <ChatProvider> internally) -----------
  /**
   * Transport. Required unless the launcher is mounted inside an
   * existing `<ChatProvider>` (in which case the ambient provider is
   * reused and `transport` is ignored).
   */
  transport?: ChatTransport;
  /** Optional chat config (labels, prefs, persona, etc.). */
  config?: ChatConfig;
  /** Pre-existing session to attach to. */
  initialSessionId?: string;
  /** Create a new backend session automatically when none is provided. */
  autoCreateSession?: boolean;
  /** Enable streaming. Defaults to transport's preference. */
  streaming?: boolean;
  /**
   * Audio-trigger configuration (sounds map). The launcher owns the
   * `useChatAudio()` hook internally; consumers no longer construct it
   * themselves.
   */
  audio?: ChatAudioConfig;
  /** Verbose dev logging via consola. */
  debug?: boolean;
  /** Rewrite outgoing content before transport. */
  onBeforeSend?: (content: string) => string | Promise<string>;

  // ---- visual chrome ------------------------------------------------------
  /** Dock contents — typically a `<ChatRoot>` or custom chat shell. */
  children: ReactNode;
  /** FAB customization. */
  fab?: Omit<ChatFABProps, 'onClick'>;
  /** Dock customization. `headerActions` is computed from `headerSlots`. */
  dock?: Omit<ChatDockProps, 'open' | 'onClose' | 'children' | 'headerActions'>;
  /**
   * Declarative header buttons rendered INSIDE the launcher's
   * `<ChatProvider>`. See `ChatHeaderSlots` for the available knobs.
   */
  headerSlots?: ChatHeaderSlots;
  /**
   * Proactive greeting bubble shown next to the FAB before the user
   * opens the chat.
   */
  greeting?: string | ChatLauncherGreeting;
  /** Open/close via a keyboard shortcut. */
  hotkey?: ChatLauncherHotkey;
  /** Initial open state for uncontrolled mode. @default false */
  defaultOpen?: boolean;
  /** Controlled open state. */
  open?: boolean;
  /** Controlled open state setter. */
  onOpenChange?: (open: boolean) => void;
  /** Focus the composer when the dock opens. @default true */
  autoFocusComposerOnOpen?: boolean;
  /** Close the dock on Escape. @default true */
  closeOnEscape?: boolean;
  /**
   * Last unread inbound message — drives `<ChatUnreadPreview>` and the
   * FAB badge.
   */
  unreadMessage?: ChatMessage | null;
  /** Called when the chat is opened or the preview dismissed. */
  onMarkRead?: () => void;
  /** Customize the unread bubble. */
  unreadPreview?: Omit<
    ChatUnreadPreviewProps,
    'open' | 'message' | 'onClick' | 'onDismiss' | 'position' | 'fabOffset'
  >;
}

function readDismissed(storageKey: string | null | undefined): boolean {
  if (!storageKey) return false;
  if (typeof window === 'undefined') return false;
  try {
    return window.localStorage.getItem(storageKey) === '1';
  } catch {
    return false;
  }
}

function writeDismissed(storageKey: string | null | undefined): void {
  if (!storageKey) return;
  if (typeof window === 'undefined') return;
  try {
    window.localStorage.setItem(storageKey, '1');
  } catch {
    // private mode / storage full — silently ignore
  }
}

/**
 * Floating chat launcher = `<ChatProvider>` + FAB + Dock + presence
 * + optional greeting + hotkey.
 *
 * The provider lives at this level so:
 *   - declarative `headerSlots` (e.g. `reset`) can read `sessionId` /
 *     call `clearMessages()` via `useChatContext()` while rendering in
 *     the dock header, which is a sibling of `children` (not a child).
 *   - descendant `<ChatRoot>` instances detect the ambient provider and
 *     skip wrapping in a second one.
 */
export function ChatLauncher({
  // provider wiring
  transport,
  config,
  initialSessionId,
  autoCreateSession,
  streaming,
  audio,
  debug,
  onBeforeSend,
  // visual chrome
  children,
  fab,
  dock,
  headerSlots,
  greeting,
  hotkey,
  defaultOpen = false,
  open: controlledOpen,
  onOpenChange,
  autoFocusComposerOnOpen = true,
  closeOnEscape = true,
  unreadMessage,
  onMarkRead,
  unreadPreview,
}: ChatLauncherProps) {
  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
  const isControlled = controlledOpen !== undefined;
  const open = isControlled ? controlledOpen : uncontrolledOpen;
  const dockContentRef = useRef<HTMLDivElement>(null);

  // Auto-focus the composer when the dock opens.
  useEffect(() => {
    if (!autoFocusComposerOnOpen || !open) return;
    const t = setTimeout(() => {
      const root = dockContentRef.current;
      if (!root) return;
      const target = root.querySelector<HTMLElement>(
        'textarea:not([disabled]):not([readonly]), input[type="text"]:not([disabled]):not([readonly])',
      );
      target?.focus();
    }, 120);
    return () => clearTimeout(t);
  }, [open, autoFocusComposerOnOpen]);

  const setOpen = useCallback(
    (next: boolean) => {
      if (!isControlled) setUncontrolledOpen(next);
      onOpenChange?.(next);
    },
    [isControlled, onOpenChange],
  );
  const toggleOpen = useCallback(() => setOpen(!open), [open, setOpen]);

  useHotkey(
    'escape',
    (e) => {
      const target = (e?.target as HTMLElement | null) ?? null;
      const inEditable =
        !!target &&
        (target.matches?.('input, textarea, [contenteditable="true"]') ?? false);
      if (inEditable) {
        target.blur();
        return;
      }
      setOpen(false);
    },
    { enabled: closeOnEscape && open },
  );

  // Normalize greeting prop.
  const greetingConfig: ChatLauncherGreeting | null =
    greeting === undefined
      ? null
      : typeof greeting === 'string'
        ? { content: greeting }
        : greeting;

  const [dismissed, setDismissed] = useState(() =>
    readDismissed(greetingConfig?.dismissStorageKey),
  );

  // Hotkey.
  useEffect(() => {
    if (!hotkey) return;
    const handler = (e: KeyboardEvent) => {
      const metaOk = hotkey.meta ? e.metaKey || e.ctrlKey : !e.metaKey && !e.ctrlKey;
      const shiftOk = hotkey.shift ? e.shiftKey : !e.shiftKey;
      const altOk = hotkey.alt ? e.altKey : !e.altKey;
      if (!metaOk || !shiftOk || !altOk) return;
      if (e.key !== hotkey.key) return;
      e.preventDefault();
      setOpen(!open);
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [hotkey?.key, hotkey?.meta, hotkey?.shift, hotkey?.alt, open, setOpen, hotkey]);

  const greetingOpen = !!greetingConfig
    && !dismissed
    && (greetingConfig.hideOnOpen === false || !open);

  const fabPosition: ChatFABPosition = fab?.position ?? 'bottom-right';
  const fabOffset = fab?.offset ?? 24;

  const handleGreetingDismiss = () => {
    setDismissed(true);
    writeDismissed(greetingConfig?.dismissStorageKey);
  };

  const handleGreetingClick = () => {
    setOpen(true);
    setDismissed(true);
    writeDismissed(greetingConfig?.dismissStorageKey);
  };

  useEffect(() => {
    if (open && unreadMessage) onMarkRead?.();
  }, [open, unreadMessage, onMarkRead]);

  const unreadOpen = !open && !!unreadMessage;
  const handleUnreadClick = () => {
    setOpen(true);
    onMarkRead?.();
  };
  const handleUnreadDismiss = () => {
    onMarkRead?.();
  };

  const resolvedFab = unreadMessage && fab?.badge === undefined
    ? { ...fab, badge: 1 }
    : fab;

  // Reused below for the ambient-provider branch; read up here so
  // `audioConfigured` can fall back to the ambient provider's audio.
  const ambient = useChatContextOptional();

  // Whether audio wires up any actual sound. Used as the default for
  // `headerSlots.audio` — no point auto-injecting the toggle when
  // there's nothing to mute.
  //
  // When the launcher is rendered inside an existing `<ChatProvider>`
  // (the host owns the provider), audio was configured there, not via
  // this component's `audio` prop — so honor the ambient provider's
  // `hasAudio` too. Without this the dock-header mute toggle silently
  // never appears for host-owned-provider setups.
  const audioConfigured = useMemo<boolean>(() => {
    if (ambient?.hasAudio) return true;
    if (!audio) return false;
    if (audio.silenced) return false;
    const sounds = audio.sounds;
    // `useChatAudio` falls back to DEFAULT_CHAT_SOUNDS when `sounds` is
    // undefined and `silenced` is false. Treat that as "configured" too.
    if (sounds === undefined) return true;
    return Object.values(sounds).some(
      (v) => typeof v === 'string' && v.length > 0,
    );
  }, [audio, ambient?.hasAudio]);

  const resolvedSlots = useMemo(
    () => resolveHeaderSlots(headerSlots, audioConfigured),
    [headerSlots, audioConfigured],
  );

  // Single source of truth for dock layout prefs. The mode-toggle slot
  // and `<ChatDock>` live in separate subtrees — if each called
  // `useChatDockPrefs` independently they'd hold isolated `useState`
  // (no same-tab cross-instance sync), so the toggle would flip
  // localStorage but never re-render the dock. Owning the hook here and
  // threading both the values and the toggle down keeps them in sync.
  const modeToggleSlot = resolvedSlots.modeToggle;
  const dockPrefs = useChatDockPrefs({
    storageKey: modeToggleSlot?.persistAs,
    defaults: modeToggleSlot?.defaults,
  });

  const hasAnySlot =
    resolvedSlots.audio ||
    resolvedSlots.modeToggle !== null ||
    resolvedSlots.languagePicker !== null ||
    resolvedSlots.reset !== null ||
    resolvedSlots.custom !== null;

  const body = (
    <>
      <ChatFAB {...resolvedFab} onClick={toggleOpen} />
      {unreadMessage ? (
        <ChatUnreadPreview
          {...unreadPreview}
          open={unreadOpen}
          message={unreadMessage}
          onClick={handleUnreadClick}
          onDismiss={handleUnreadDismiss}
          position={fabPosition}
          fabOffset={fabOffset}
        />
      ) : greetingConfig ? (
        <ChatGreeting
          {...greetingConfig}
          open={greetingOpen}
          onClick={handleGreetingClick}
          onDismiss={handleGreetingDismiss}
          position={fabPosition}
          fabOffset={fabOffset}
        >
          {greetingConfig.content}
        </ChatGreeting>
      ) : null}
      <ChatDock
        {...dock}
        // When the mode-toggle slot is active, dock prefs are the source
        // of truth so the header toggle actually re-docks the panel.
        // Otherwise fall back to the static `dock` props. `sideWidth`
        // only governs side mode — leave popover width to `dock.width`.
        mode={modeToggleSlot ? dockPrefs.mode : dock?.mode}
        side={modeToggleSlot ? dockPrefs.side : dock?.side}
        width={
          modeToggleSlot && dockPrefs.mode === 'side'
            ? dockPrefs.sideWidth
            : dock?.width
        }
        open={open}
        onClose={() => setOpen(false)}
        headerActions={
          hasAnySlot ? (
            <HeaderSlotsRenderer slots={resolvedSlots} dockPrefs={dockPrefs} />
          ) : undefined
        }
      >
        <div ref={dockContentRef} className="flex h-full min-h-0 min-w-0 flex-col">
          {children}
        </div>
      </ChatDock>
    </>
  );

  if (ambient) {
    // Already inside a ChatProvider — reuse it. Provider-level props
    // (transport / config / audio / debug / onBeforeSend) are ignored
    // because they belong to the upstream provider.
    return body;
  }

  if (!transport) {
    // No ambient provider and no transport — programmer error.
    throw new Error(
      '<ChatLauncher> requires `transport` when mounted outside a <ChatProvider>.',
    );
  }

  return (
    <ChatProvider
      transport={transport}
      config={config}
      initialSessionId={initialSessionId}
      autoCreateSession={autoCreateSession}
      streaming={streaming}
      audio={audio}
      debug={debug}
      onBeforeSend={onBeforeSend}
    >
      {body}
    </ChatProvider>
  );
}
