"use client"

import * as React from "react"
import { useLocation } from "react-router"

import { Kbd, KbdGroup } from "@/components/ui/kbd"
import { useAltKeyLabel, useModKeyLabel } from "@/hooks/use-mod-key-label"
import { usePersistedState } from "@exxatdesignux/ui/lib/persisted-state"
import {
  focusAskLeoLauncher,
  focusAskLeoSurface,
  isFocusInsideAskLeo,
} from "@/lib/ask-leo-focus"
import { ASK_LEO_DOCK_KEY, type AskLeoDock } from "@/lib/ask-leo-view"
import { isEditableTarget } from "@/lib/editable-target"
import { isExamLockPath } from "@/lib/exam-lock-shell"
import { productSlug } from "@/stores/app-store"
import { useProduct } from "@/contexts/product-context"

/**
 * Page context that pages register with `useAskLeoPageContext` so Leo knows
 * what the user is currently looking at.
 */
export interface AskLeoPageContext {
  title: string
  description?: string
  suggestions?: string[]
  data?: Record<string, unknown>
}

interface AskLeoContextValue {
  open: boolean
  setOpen: (open: boolean) => void
  toggle: () => void
  openWithPrompt: (prompt: string) => void
  consumePendingComposerPrompt: () => string | null
  pageContext: AskLeoPageContext | null
  setPageContext: (ctx: AskLeoPageContext | null) => void
  /**
   * Leo is composing an answer. The thread state itself stays inside the panel
   * that owns it; this is the one bit of it that entry points outside the panel
   * need, so they can show the working state instead of guessing.
   */
  busy: boolean
  setBusy: (busy: boolean) => void
  /**
   * The open shell has at least one turn in it. Same reasoning as `busy`: the
   * shell owns the thread, but its header — which lives outside the body — has
   * to know whether "New chat" would do anything, and a control that does
   * nothing is worse than no control.
   */
  threadActive: boolean
  setThreadActive: (active: boolean) => void
  /**
   * Which shell renders the conversation — the docked rail or the floating
   * window. A stored preference, so it survives a reload; full screen is not
   * here because it is a route, not a preference.
   */
  dock: AskLeoDock
  setDock: (dock: AskLeoDock) => void
  /**
   * Window mode only: parked to the launcher. The window stays mounted while
   * minimised (hidden, so nothing inside it is focusable) — unmounting it would
   * throw away the conversation the user was in the middle of, which is the one
   * thing minimise must not do.
   */
  minimized: boolean
  setMinimized: (minimized: boolean) => void
}

const AskLeoContext = React.createContext<AskLeoContextValue>({
  open: false,
  setOpen: () => {},
  toggle: () => {},
  openWithPrompt: () => {},
  consumePendingComposerPrompt: () => null,
  pageContext: null,
  setPageContext: () => {},
  busy: false,
  setBusy: () => {},
  threadActive: false,
  setThreadActive: () => {},
  dock: "panel",
  setDock: () => {},
  minimized: false,
  setMinimized: () => {},
})

function isLeoLandingPath(pathname: string, leoHref: string) {
  return pathname === leoHref || pathname.startsWith(`${leoHref}/`)
}

export function useAskLeo() {
  return React.useContext(AskLeoContext)
}

export function useAskLeoPageContext(ctx: AskLeoPageContext | null) {
  const { setPageContext } = React.useContext(AskLeoContext)
  React.useEffect(() => {
    setPageContext(ctx)
    return () => setPageContext(null)
  }, [ctx, setPageContext])
}

export function AskLeoProvider({ children }: { children: React.ReactNode }) {
  const { pathname } = useLocation()
  const examLock = isExamLockPath(pathname)
  const [open, setOpen] = React.useState(false)
  const [busy, setBusy] = React.useState(false)
  const [threadActive, setThreadActive] = React.useState(false)
  const [pageContext, setPageContext] = React.useState<AskLeoPageContext | null>(null)
  const [dock, setDock] = usePersistedState<AskLeoDock>(ASK_LEO_DOCK_KEY, "panel", {
    debounceMs: 0,
  })
  // Not persisted: minimised is a "park this for a minute" state, and coming
  // back tomorrow to an invisible parked window with no conversation in it
  // would just look like Leo was broken.
  const [minimized, setMinimized] = React.useState(false)
  const toggle = React.useCallback(() => setOpen(v => !v), [])
  const pendingComposerPromptRef = React.useRef<string | null>(null)

  // Opening Leo always means showing Leo. Without this, a minimised window
  // would swallow the ⌘⌥K toggle and the utility-bar button.
  React.useEffect(() => {
    if (open) setMinimized(false)
  }, [open])

  const openWithPrompt = React.useCallback((prompt: string) => {
    pendingComposerPromptRef.current = prompt
    setOpen(true)
  }, [])

  const consumePendingComposerPrompt = React.useCallback(() => {
    const prompt = pendingComposerPromptRef.current
    pendingComposerPromptRef.current = null
    return prompt
  }, [])

  const value = React.useMemo(
    () => ({
      open,
      setOpen,
      toggle,
      openWithPrompt,
      consumePendingComposerPrompt,
      pageContext,
      setPageContext,
      busy,
      setBusy,
      threadActive,
      setThreadActive,
      dock,
      setDock,
      minimized,
      setMinimized,
    }),
    [
      open,
      toggle,
      openWithPrompt,
      consumePendingComposerPrompt,
      pageContext,
      busy,
      threadActive,
      dock,
      setDock,
      minimized,
    ],
  )

  const { product } = useProduct()
  const leoHref = `/${productSlug(product)}/leo`

  React.useEffect(() => {
    if (isLeoLandingPath(pathname, leoHref)) {
      setOpen(false)
    }
  }, [pathname, leoHref])

  const toggleRef = React.useRef(toggle)
  React.useEffect(() => {
    toggleRef.current = toggle
  })

  const openRef = React.useRef(open)
  React.useEffect(() => {
    openRef.current = open
  })

  const minimizedRef = React.useRef(minimized)
  React.useEffect(() => {
    minimizedRef.current = minimized
  })

  React.useEffect(() => {
    if (examLock) {
      setOpen(false)
      return
    }
    function onGlobalKeyDown(event: KeyboardEvent) {
      if (!event.altKey || (!event.metaKey && !event.ctrlKey)) return
      if (event.key.toLowerCase() !== "k") return
      const insideLeo = isFocusInsideAskLeo()
      // Typing in a page field means the chord belongs to that field, not to
      // us. Typing in Leo's own composer does not: the shortcut has to keep
      // working from there, or opening Leo would disarm the key that closes him.
      if (!insideLeo && isEditableTarget(event.target)) return
      event.preventDefault()
      if (openRef.current) {
        // Parked in the corner. Bring the conversation back rather than
        // toggling `open` off, which would discard the thread the user
        // minimised precisely to keep. The window moves focus on restore.
        if (minimizedRef.current) {
          setMinimized(false)
          return
        }
        // Open, but the user is working somewhere else on the page: they want
        // to get back to Leo, not dismiss him. Both shells sit at the end of
        // the tab order, so tabbing there is not a real option.
        if (!insideLeo && focusAskLeoSurface()) return
        // Focus is inside, so this press means close, and focus has to land
        // somewhere deliberate instead of collapsing to the document.
        toggleRef.current()
        focusAskLeoLauncher()
        return
      }
      toggleRef.current()
    }
    document.addEventListener("keydown", onGlobalKeyDown)
    return () => document.removeEventListener("keydown", onGlobalKeyDown)
  }, [examLock])

  return (
    <AskLeoContext.Provider value={value}>
      {children}
    </AskLeoContext.Provider>
  )
}

export function AskLeoShortcutKbds({
  className,
  variant = "tile",
}: {
  className?: string
  variant?: "tile" | "bare"
}) {
  const mod = useModKeyLabel()
  const alt = useAltKeyLabel()
  if (variant === "bare") {
    return (
      <KbdGroup className={className}>
        <Kbd variant="bare">{mod}{alt}K</Kbd>
      </KbdGroup>
    )
  }
  return (
    <KbdGroup className={className}>
      <Kbd>{mod}</Kbd>
      <Kbd>{alt}</Kbd>
      <Kbd>K</Kbd>
    </KbdGroup>
  )
}
