'use client';

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

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

import { ChatFAB, type ChatFABPosition, type ChatFABProps } from './ChatFAB';
import { ChatDock, type ChatDockProps } from './ChatDock';
import { ChatGreeting, type ChatGreetingProps } from './ChatGreeting';
import { ChatHeaderAudioToggle } from './ChatHeaderAudioToggle';
import { ChatUnreadPreview, type ChatUnreadPreviewProps } from './ChatUnreadPreview';
import type { ChatMessage } 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 {
  /** Dock contents — typically a `<Chat>` instance. */
  children: ReactNode;
  /** FAB customization (icon, position, label, pulse, badge, tooltip, variant, size). */
  fab?: Omit<ChatFABProps, 'onClick'>;
  /** Dock customization (size, title, position, transition, mobileFullscreen). */
  dock?: Omit<ChatDockProps, 'open' | 'onClose' | 'children'>;
  /**
   * Proactive greeting bubble shown next to the FAB before the user opens the chat.
   * Set to a string or full config object. Omit to disable.
   */
  greeting?: string | ChatLauncherGreeting;
  /** Open/close via a keyboard shortcut. */
  hotkey?: ChatLauncherHotkey;
  /** Initial open state for uncontrolled mode. @default false */
  defaultOpen?: boolean;
  /** Controlled open state (pair with `onOpenChange`). */
  open?: boolean;
  /** Controlled open state setter. */
  onOpenChange?: (open: boolean) => void;
  /**
   * Focus the composer textarea when the dock opens. Saves a click for
   * every "FAB → start typing" interaction. @default true
   */
  autoFocusComposerOnOpen?: boolean;
  /**
   * Close the dock on `Escape`. Mirrors standard popover / drawer UX.
   * Set to `false` to disable (e.g. if you want Escape to do something
   * else inside the chat). @default true
   */
  closeOnEscape?: boolean;
  /**
   * Last inbound message (admin reply / system notice / agent push) the
   * user hasn't seen yet. Drives the `<ChatUnreadPreview>` bubble next
   * to the FAB and (by default) the FAB badge.
   *
   * Source it from `useChatUnread()` inside your `<ChatProvider>`.
   */
  unreadMessage?: ChatMessage | null;
  /**
   * Called when the user opens the chat via FAB/preview/hotkey or
   * dismisses the preview with ×. Wire to `useChatUnread().markRead`.
   */
  onMarkRead?: () => void;
  /**
   * Customize the unread bubble (`truncate`, `dismissLabel`, …).
   * `open`/`message`/`onClick`/`onDismiss`/`position`/`fabOffset` are
   * wired automatically.
   */
  unreadPreview?: Omit<
    ChatUnreadPreviewProps,
    'open' | 'message' | 'onClick' | 'onDismiss' | 'position' | 'fabOffset'
  >;
  /**
   * Auto-inject a mute / unmute button into the header. Pass the
   * `useChatAudio()` (or any compatible `{ muted, toggleMute }`)
   * instance — the launcher renders `<ChatHeaderAudioToggle>` in the
   * header's actions slot when audio is actually configured (not silent).
   *
   * Hosts that manage their own header can ignore this prop and render
   * `<ChatHeaderAudioToggle>` directly.
   */
  audio?: { muted: boolean; toggleMute: () => void; isSilent?: boolean } | null;
  /**
   * Suppress the auto-injected audio toggle even when `audio` is passed.
   * @default false
   */
  hideAudioToggle?: boolean;
}

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 = FAB + Dock + presence + optional greeting + hotkey.
 *
 * 99% of hosts use this directly. For non-FAB triggers (e.g. an inline
 * link in the page) compose `<ChatDock>` with your own button.
 */
export function ChatLauncher({
  children,
  fab,
  dock,
  greeting,
  hotkey,
  defaultOpen = false,
  open: controlledOpen,
  onOpenChange,
  autoFocusComposerOnOpen = true,
  closeOnEscape = true,
  unreadMessage,
  onMarkRead,
  unreadPreview,
  audio,
  hideAudioToggle = false,
}: 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.
  // We probe the dock subtree for the first textarea/input after the
  // enter-transition settles. Keeps the hook self-contained — no need to
  // bridge into the ChatProvider context which lives inside `children`.
  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]);

  // Two-step Escape (ChatGPT / Slack behaviour):
  //   - When focus is inside a textarea / input / contenteditable, first Esc
  //     just blurs — drafts survive accidental presses.
  //   - When focus is elsewhere (the user already left the composer or never
  //     focused it), Esc closes the dock.
  // Disabled when chat is shut so we don't intercept page-level Esc bindings.
  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]);

  // Greeting visibility: respect dismissal, hideOnOpen, and the actual open state.
  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);
    // Tap-to-open also clears the proactive bubble — pushing it again on
    // the same visit would feel spammy. Persisted dismissal honours the
    // storage key so it doesn't reappear after navigation either.
    setDismissed(true);
    writeDismissed(greetingConfig?.dismissStorageKey);
  };

  // Mark-as-read also fires when the chat opens through any path (FAB,
  // hotkey, controlled state) — symmetric with click-to-open via the
  // preview itself.
  useEffect(() => {
    if (open && unreadMessage) onMarkRead?.();
  }, [open, unreadMessage, onMarkRead]);

  // Unread preview replaces the greeting when there's a real inbound
  // message to surface — same anchor, more relevant content.
  const unreadOpen = !open && !!unreadMessage;
  const handleUnreadClick = () => {
    setOpen(true);
    onMarkRead?.();
  };
  const handleUnreadDismiss = () => {
    onMarkRead?.();
  };

  // Auto-derive a "1" badge from unread when the host didn't set one.
  const resolvedFab = unreadMessage && fab?.badge === undefined
    ? { ...fab, badge: 1 }
    : fab;

  return (
    <>
      <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}
        open={open}
        onClose={() => setOpen(false)}
        headerActions={
          (audio && !audio.isSilent && !hideAudioToggle) || dock?.headerActions ? (
            <>
              {dock?.headerActions}
              {audio && !audio.isSilent && !hideAudioToggle ? (
                <ChatHeaderAudioToggle muted={audio.muted} onToggle={audio.toggleMute} />
              ) : null}
            </>
          ) : undefined
        }
      >
        <div ref={dockContentRef} className="flex h-full min-h-0 min-w-0 flex-col">
          {children}
        </div>
      </ChatDock>
    </>
  );
}
