'use client'

import React, { useCallback, useEffect, useId, useRef, useState } from 'react'

import {
  type MentionUser,
  detectMentionContext,
  insertMentionToken,
  searchMentionUsers,
} from '../lib/mention-picker.js'

/**
 * `<MentionableTextarea />` — drop-in replacement for `<textarea>` /
 * `<input type="text">` that opens a Radix-styled mention picker
 * whenever the user types `@`.
 *
 * Behaviour
 * ─────────
 * - The picker appears anchored to the textarea, below the input.
 *   We intentionally do NOT track the caret pixel position (that
 *   requires a textarea-mirror trick which doesn't add enough UX to
 *   justify the complexity); GitHub, Linear, and Asana all anchor
 *   their pickers this way on simple textareas.
 * - Arrow Up / Arrow Down move the highlight. Enter / Tab commit the
 *   selection. Escape closes the picker without inserting anything.
 *   Mouse click on a row also commits.
 * - The picker only fires server queries while a real mention is
 *   being typed; it short-circuits an empty `query` so the dropdown
 *   shows up immediately after `@` is typed (with an empty state)
 *   and only flips to "no matches" once the user has typed at least
 *   one character.
 * - The textarea value is fully controlled by the parent — we never
 *   keep a separate copy. `onChange` is called with the next value
 *   on every keystroke AND every mention insertion.
 *
 * Dependency-injection seam
 * ─────────────────────────
 * The `search` prop accepts an override so tests can substitute a
 * synchronous fake. Production callers leave it unset and the
 * component uses the real `searchMentionUsers()` REST helper.
 */
export interface MentionableTextareaProps {
  value: string
  onChange: (next: string) => void
  /** Forwarded to the underlying textarea. */
  placeholder?: string
  /** Forwarded to the underlying textarea. */
  rows?: number
  className?: string
  /** Forwarded — useful for keyboard shortcuts the parent owns. */
  onKeyDown?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
  /** Forwarded — useful for click-to-stop-propagation in nested cards. */
  onClick?: (event: React.MouseEvent<HTMLTextAreaElement>) => void
  /** Test hook — required when rendering with React state assertions. */
  'data-testid'?: string
  /**
   * Override the user search. The default uses the REST helper.
   * Tests inject an in-memory fake so dropdown contents are
   * deterministic without HTTP mocking.
   */
  search?: (query: string) => Promise<MentionUser[]>
  /**
   * Optional fixed id for the picker dropdown — pass when the parent
   * wires up `aria-controls`. We generate one with `useId()` when
   * unset.
   */
  pickerId?: string
  /**
   * Format the displayed token. By default we show `@<name>` while
   * the underlying value stores `@<userId>`. A future slice can swap
   * to a rich-text editor where these diverge; the prop lets that
   * land without changing the picker.
   */
  formatLabel?: (user: MentionUser) => string
}

interface PickerState {
  open: boolean
  query: string
  highlight: number
  users: MentionUser[]
}

const initialPicker: PickerState = {
  open: false,
  query: '',
  highlight: 0,
  users: [],
}

export function MentionableTextarea({
  value,
  onChange,
  placeholder,
  rows = 3,
  className,
  onKeyDown,
  onClick,
  'data-testid': testId,
  search = (q) => searchMentionUsers(q).then((r) => (r.ok ? r.result : [])),
  pickerId: pickerIdProp,
  formatLabel = (u) => `@${u.name}`,
}: MentionableTextareaProps): React.JSX.Element {
  const generatedId = useId()
  const pickerId = pickerIdProp ?? `actuate-mention-picker-${generatedId}`
  const textareaRef = useRef<HTMLTextAreaElement | null>(null)
  const [picker, setPicker] = useState<PickerState>(initialPicker)
  // `pickerRef` mirrors `picker` for the keydown handler so the
  // arrow-key / enter listeners don't go stale between renders.
  const pickerRef = useRef(picker)
  useEffect(() => {
    pickerRef.current = picker
  }, [picker])
  const requestSeq = useRef(0)
  const mountedRef = useRef(true)
  useEffect(() => {
    return () => {
      mountedRef.current = false
    }
  }, [])

  const closePicker = useCallback(() => {
    setPicker((p) => (p.open ? initialPicker : p))
  }, [])

  // Recompute the active mention context whenever the value or
  // selection changes. Centralising this in one effect keeps the
  // open/close logic in a single place — every textarea event
  // (input, click, keyup) just calls `recompute()`.
  const recompute = useCallback(
    async (nextValue: string, caret: number) => {
      const ctx = detectMentionContext(nextValue, caret)
      if (!ctx) {
        closePicker()
        return
      }
      const id = ++requestSeq.current
      // Open immediately so the user sees a dropdown the moment they
      // type `@`. Clear the `users` array on every new query so the
      // user can never press Enter / Tab and commit a stale row
      // from the previous query while the new request is in flight
      // (PR #159 Copilot finding — type `@a` then `@bo` quickly and
      // hit Enter before the `@bo` results arrive: without this
      // reset, the picker would commit a row from `@a`).
      setPicker((p) => ({
        open: true,
        query: ctx.query,
        highlight: 0,
        users: p.query === ctx.query ? p.users : [],
      }))
      try {
        const users = await search(ctx.query)
        if (!mountedRef.current) return
        // Drop stale responses — only commit if our request is still
        // the latest one. Otherwise the user has typed more and a
        // newer query is in flight.
        if (id !== requestSeq.current) return
        setPicker((p) =>
          // If the picker was closed (Escape) between request and
          // response, don't reopen it.
          p.open ? { ...p, users, highlight: 0 } : p,
        )
      } catch {
        if (!mountedRef.current) return
        if (id !== requestSeq.current) return
        // Surface no rows on failure — the textarea must keep
        // working when the user is offline. The poll loop / native
        // submit path is unaffected.
        setPicker((p) => (p.open ? { ...p, users: [] } : p))
      }
    },
    [closePicker, search],
  )

  const commitMention = useCallback(
    (user: MentionUser) => {
      const el = textareaRef.current
      if (!el) return
      const caret = el.selectionStart ?? value.length
      const ctx = detectMentionContext(value, caret)
      if (!ctx) {
        closePicker()
        return
      }
      const next = insertMentionToken(value, ctx, user.id)
      onChange(next.value)
      closePicker()
      // Move the caret past the inserted token AFTER React paints
      // the new value, so the selection isn't clobbered by the
      // controlled update.
      requestAnimationFrame(() => {
        const live = textareaRef.current
        if (!live) return
        live.focus()
        live.setSelectionRange(next.caret, next.caret)
      })
    },
    [closePicker, onChange, value],
  )

  const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
    const next = event.target.value
    const caret = event.target.selectionStart ?? next.length
    onChange(next)
    void recompute(next, caret)
  }

  const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
    const p = pickerRef.current
    if (p.open && p.users.length > 0) {
      if (event.key === 'ArrowDown') {
        event.preventDefault()
        setPicker((s) => ({ ...s, highlight: (s.highlight + 1) % s.users.length }))
        return
      }
      if (event.key === 'ArrowUp') {
        event.preventDefault()
        setPicker((s) => ({
          ...s,
          highlight: (s.highlight - 1 + s.users.length) % s.users.length,
        }))
        return
      }
      if (event.key === 'Enter' || event.key === 'Tab') {
        event.preventDefault()
        // Defensive: highlight could theoretically point past the
        // end of `users` if state went stale between renders. Guard
        // it so we never `undefined`-bang.
        const target = p.users[p.highlight]
        if (target) commitMention(target)
        return
      }
    }
    if (event.key === 'Escape' && p.open) {
      event.preventDefault()
      closePicker()
      return
    }
    onKeyDown?.(event)
  }

  const handleClick = (event: React.MouseEvent<HTMLTextAreaElement>) => {
    onClick?.(event)
    const el = textareaRef.current
    if (!el) return
    void recompute(el.value, el.selectionStart ?? el.value.length)
  }

  // Closing on outside click — when the user clicks anywhere outside
  // the textarea or the picker, the dropdown should disappear so it
  // doesn't sit on top of unrelated UI.
  useEffect(() => {
    if (!picker.open) return
    const onDocMouseDown = (ev: MouseEvent) => {
      const t = ev.target as Node | null
      if (!t) return
      const parent = textareaRef.current?.parentElement
      if (parent && parent.contains(t)) return
      closePicker()
    }
    document.addEventListener('mousedown', onDocMouseDown)
    return () => document.removeEventListener('mousedown', onDocMouseDown)
  }, [picker.open, closePicker])

  return (
    <div className="relative">
      <textarea
        ref={textareaRef}
        value={value}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        onClick={handleClick}
        placeholder={placeholder}
        rows={rows}
        className={className}
        aria-autocomplete="list"
        aria-controls={picker.open ? pickerId : undefined}
        aria-expanded={picker.open}
        data-testid={testId}
      />
      {picker.open && (
        <div
          id={pickerId}
          role="listbox"
          aria-label="Mention a teammate"
          className="absolute top-full left-0 z-30 mt-1 max-h-56 w-64 overflow-y-auto rounded border border-gray-200 bg-white shadow-lg"
          data-testid={testId ? `${testId}-picker` : 'mention-picker'}
        >
          {picker.users.length === 0 ? (
            <p className="px-3 py-2 text-xs text-gray-500">
              {picker.query ? 'No matches' : 'Type a name…'}
            </p>
          ) : (
            <ul>
              {picker.users.map((u, idx) => (
                <li key={u.id}>
                  <button
                    type="button"
                    role="option"
                    aria-selected={idx === picker.highlight}
                    onMouseDown={(e) => {
                      // mousedown (not click) — prevents the
                      // textarea from losing focus before we
                      // commit, which would otherwise hide the
                      // selection and feel janky.
                      e.preventDefault()
                      commitMention(u)
                    }}
                    onMouseEnter={() => setPicker((s) => ({ ...s, highlight: idx }))}
                    className={`block w-full px-3 py-1.5 text-left text-xs ${
                      idx === picker.highlight
                        ? 'bg-blue-50 text-blue-900'
                        : 'text-gray-800 hover:bg-gray-50'
                    }`}
                    data-testid={testId ? `${testId}-picker-row-${u.id}` : `mention-row-${u.id}`}
                  >
                    <div className="font-medium">{formatLabel(u)}</div>
                    <div className="text-[10px] text-gray-500">{u.email}</div>
                  </button>
                </li>
              ))}
            </ul>
          )}
        </div>
      )}
    </div>
  )
}
