'use client'

import * as React from 'react'
import { Paperclip, RotateCw, SendHorizonal, X } from 'lucide-react'
import { cn } from '../../lib/utils'
import type { PendingAttachment } from './types'
import { formatBytes, isImageName } from './attachment-utils'
import {
  useComposerUploads,
  type AttachmentUploader,
  type UploadedAttachmentRef,
} from './useComposerUploads'

export interface ChatComposerProps {
  /** Controlled value. Omit for uncontrolled usage. */
  value?: string
  onChange?: (value: string) => void
  /** Called with the trimmed message text on submit. */
  onSubmit: (text: string) => void
  placeholder?: string
  /** Disables input entirely. */
  disabled?: boolean
  /** Submission in flight — input stays editable but submit is blocked. */
  busy?: boolean
  submitLabel?: string
  /** Max rows the textarea grows to before scrolling. Default 10. */
  maxRows?: number
  /**
   * Controlled list of pending attachments rendered as chips above the textarea.
   * The APP owns upload state; the composer is presentation-only here.
   */
  attachments?: PendingAttachment[]
  /**
   * Fired with the selected files from the attach button, a drag-and-drop onto
   * the composer, or a clipboard paste of image/file data. Presence of this
   * handler enables the attach affordances.
   */
  onAttachFiles?: (files: File[]) => void
  /** Remove a pending attachment chip by id. */
  onRemoveAttachment?: (id: string) => void
  /** Retry a failed attachment chip by id (shows a retry control on error chips). */
  onRetryAttachment?: (id: string) => void
  /**
   * SELF-MANAGED attachment mode. When provided, the composer drives the entire
   * upload lifecycle itself (queued/uploading/ready/error chips, downscale,
   * retry, remove + server-delete, new-deck queue-then-flush-on-`documentId`) by
   * routing the attach button / drag-drop / paste files through this uploader.
   * The app's `@startsimpli/api` `AttachmentsApi` structurally satisfies it.
   *
   * In this mode the controlled props (`attachments`/`onAttachFiles`/
   * `onRemoveAttachment`/`onRetryAttachment`) are ignored — the composer owns
   * the chip state. `onSubmit` still carries text only.
   */
  uploader?: AttachmentUploader
  /**
   * Document the uploads land on (self-managed mode). May be absent/'new' for a
   * brand-new document: files are held client-side and auto-upload the moment a
   * real id arrives.
   */
  documentId?: string
  /** Upload purpose tag for self-managed uploads. Default 'reference'. */
  attachmentPurpose?: string
  /** Self-managed mode: fired with the uploaded attachment refs when they change. */
  onAttachmentsChange?: (refs: UploadedAttachmentRef[]) => void
  className?: string
}

/**
 * Chat input with auto-growing textarea. Enter submits, Shift+Enter inserts a
 * newline, and large pastes are accepted as-is. Controlled or uncontrolled.
 *
 * Reference attachments are supported two ways (submit always carries text only):
 *
 *  - SELF-MANAGED (recommended, one line): pass an `uploader` (+ `documentId`).
 *    The composer drives the full upload lifecycle internally via
 *    `useComposerUploads` — downscale, progress, retry, remove + server-delete,
 *    and new-deck queue-then-flush — routing attach button / drag-drop / paste
 *    files through the uploader. The app wires nothing else.
 *
 *  - CONTROLLED (legacy): the app owns upload state and feeds `attachments`
 *    down; the composer renders chips and surfaces new files via `onAttachFiles`
 *    (`onRemoveAttachment`/`onRetryAttachment` for chip actions).
 *
 * Providing `uploader` switches to self-managed and ignores the controlled props.
 */
export function ChatComposer({
  value,
  onChange,
  onSubmit,
  placeholder = 'Message…',
  disabled = false,
  busy = false,
  submitLabel = 'Send',
  maxRows = 10,
  attachments,
  onAttachFiles,
  onRemoveAttachment,
  onRetryAttachment,
  uploader,
  documentId,
  attachmentPurpose,
  onAttachmentsChange,
  className,
}: ChatComposerProps) {
  const isControlled = value !== undefined
  const [internal, setInternal] = React.useState('')
  const text = isControlled ? value! : internal
  const textareaRef = React.useRef<HTMLTextAreaElement>(null)
  const fileInputRef = React.useRef<HTMLInputElement>(null)
  const [dragOver, setDragOver] = React.useState(false)

  // SELF-MANAGED mode: when an `uploader` is supplied, the composer owns the
  // upload state and feeds its own chips/handlers in place of the controlled
  // props. The hook runs unconditionally (Rules of Hooks); a no-op uploader
  // keeps it inert when in controlled mode.
  const selfManaged = !!uploader
  const noopUploader = React.useRef<AttachmentUploader>({
    async uploadAttachment() {
      return { id: '', fileUrl: null }
    },
    async deleteAttachment() {
      /* noop */
    },
  })
  const uploads = useComposerUploads(uploader ?? noopUploader.current, documentId, {
    purpose: attachmentPurpose,
    onAttachmentsChange,
  })

  // Resolve effective chips/handlers from whichever mode is active.
  const effectiveAttachments = selfManaged ? uploads.attachments : attachments
  const effectiveAttachFiles = selfManaged ? uploads.attachFiles : onAttachFiles
  const effectiveRemove = selfManaged ? uploads.removeAttachment : onRemoveAttachment
  const effectiveRetry = selfManaged ? uploads.retryAttachment : onRetryAttachment

  const attachEnabled = !!effectiveAttachFiles && !disabled
  const chips = effectiveAttachments ?? []

  const setText = (next: string) => {
    if (!isControlled) setInternal(next)
    onChange?.(next)
  }

  // auto-grow
  React.useEffect(() => {
    const el = textareaRef.current
    if (!el) return
    el.style.height = 'auto'
    const lineHeight = 24
    el.style.height = `${Math.min(el.scrollHeight, lineHeight * maxRows)}px`
  }, [text, maxRows])

  const canSubmit = !disabled && !busy && text.trim().length > 0

  const submit = () => {
    if (!canSubmit) return
    onSubmit(text.trim())
    if (!isControlled) setInternal('')
    // Self-managed chips have been (or are being) uploaded to the document; the
    // consumer reads them off the server on its turn, so clear them from the
    // composer once the message goes out. (Controlled mode leaves chip lifecycle
    // entirely to the app.)
    if (selfManaged) uploads.clear()
  }

  const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key !== 'Enter') return
    // Cmd/Ctrl+Enter always submits (handy for multi-line content).
    if (e.metaKey || e.ctrlKey) {
      e.preventDefault()
      submit()
      return
    }
    if (e.shiftKey) return // explicit newline
    // Bare Enter submits ONLY single-line content. When the value already
    // spans multiple lines (e.g. a pasted outline) Enter inserts a newline so
    // the multi-line block stays intact — submit via the button or Cmd+Enter.
    if (text.includes('\n')) return
    e.preventDefault()
    submit()
  }

  const emitFiles = (list: FileList | null | undefined) => {
    if (!attachEnabled || !list || list.length === 0) return
    effectiveAttachFiles!(Array.from(list))
  }

  const onPickClick = () => fileInputRef.current?.click()

  const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    emitFiles(e.target.files)
    // reset so picking the same file again re-fires change
    e.target.value = ''
  }

  const onDrop = (e: React.DragEvent) => {
    if (!attachEnabled) return
    const files = e.dataTransfer?.files
    if (files && files.length > 0) {
      e.preventDefault()
      setDragOver(false)
      emitFiles(files)
    }
  }

  const onDragOver = (e: React.DragEvent) => {
    if (!attachEnabled) return
    // Only treat as a file drag (not text selection drags).
    if (Array.from(e.dataTransfer?.types ?? []).includes('Files')) {
      e.preventDefault()
      setDragOver(true)
    }
  }

  const onDragLeave = () => setDragOver(false)

  const onPaste = (e: React.ClipboardEvent) => {
    if (!attachEnabled) return
    const files = e.clipboardData?.files
    if (files && files.length > 0) {
      // Pasted image/file data — capture it as an attachment, not text.
      e.preventDefault()
      emitFiles(files)
    }
  }

  return (
    <div
      className={cn(
        'flex flex-col gap-2 border-t border-border bg-background p-3',
        dragOver && 'ring-2 ring-inset ring-ring',
        className
      )}
      onDrop={onDrop}
      onDragOver={onDragOver}
      onDragLeave={onDragLeave}
    >
      {chips.length > 0 && (
        <ul className="flex flex-wrap gap-2" data-testid="chat-attachment-chips">
          {chips.map((a) => (
            <AttachmentChip
              key={a.id}
              attachment={a}
              onRemove={effectiveRemove}
              onRetry={effectiveRetry}
            />
          ))}
        </ul>
      )}
      <div className="flex items-end gap-2">
        {attachEnabled && (
          <>
            <input
              ref={fileInputRef}
              type="file"
              multiple
              className="sr-only"
              data-testid="chat-attach-input"
              onChange={onInputChange}
            />
            <button
              type="button"
              onClick={onPickClick}
              aria-label="Attach files"
              className={cn(
                'inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-input bg-background text-muted-foreground shadow-sm transition-colors',
                'hover:bg-accent hover:text-foreground'
              )}
            >
              <Paperclip className="h-4 w-4" aria-hidden="true" />
            </button>
          </>
        )}
        <textarea
          ref={textareaRef}
          value={text}
          onChange={(e) => setText(e.target.value)}
          onKeyDown={onKeyDown}
          onPaste={attachEnabled ? onPaste : undefined}
          placeholder={placeholder}
          disabled={disabled}
          rows={1}
          className={cn(
            'flex-1 resize-none rounded-lg border border-input bg-background px-3 py-2 text-sm shadow-sm',
            'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
            'disabled:cursor-not-allowed disabled:opacity-50'
          )}
        />
        <button
          type="button"
          onClick={submit}
          disabled={!canSubmit}
          aria-label={submitLabel}
          className={cn(
            'inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm transition-opacity',
            'hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40'
          )}
        >
          <SendHorizonal className="h-4 w-4" aria-hidden="true" />
        </button>
      </div>
    </div>
  )
}

function AttachmentChip({
  attachment,
  onRemove,
  onRetry,
}: {
  attachment: PendingAttachment
  onRemove?: (id: string) => void
  onRetry?: (id: string) => void
}) {
  const { id, name, status, progress, thumbnailUrl, sizeBytes, error } = attachment
  const isImage = !!thumbnailUrl && isImageName(name)
  const pct = Math.round((progress ?? 0) * 100)

  return (
    <li
      data-testid={`chat-attachment-${id}`}
      data-status={status}
      className={cn(
        'group relative flex items-center gap-2 rounded-md border border-border bg-muted/40 py-1 pl-1 pr-2 text-xs',
        status === 'error' && 'border-red-300 bg-red-50 text-red-700'
      )}
    >
      {isImage ? (
        // eslint-disable-next-line @next/next/no-img-element
        <img
          src={thumbnailUrl}
          alt={name}
          className="h-8 w-8 shrink-0 rounded object-cover"
        />
      ) : (
        <span
          className="flex h-8 w-8 shrink-0 items-center justify-center rounded bg-muted text-muted-foreground"
          aria-hidden="true"
        >
          <Paperclip className="h-4 w-4" />
        </span>
      )}
      <span className="flex min-w-0 flex-col">
        <span className="max-w-[10rem] truncate font-medium" title={name}>
          {name}
        </span>
        <span className="text-[10px] text-muted-foreground">
          {status === 'queued'
            ? 'Waiting for deck…'
            : status === 'uploading'
              ? `Uploading… ${pct}%`
              : status === 'error'
                ? (error || 'Upload failed')
                : sizeBytes != null
                  ? formatBytes(sizeBytes)
                  : null}
        </span>
      </span>
      {status === 'error' && onRetry && (
        <button
          type="button"
          onClick={() => onRetry(id)}
          aria-label={`Retry ${name}`}
          className="ml-1 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-red-600 hover:bg-red-100"
        >
          <RotateCw className="h-3 w-3" aria-hidden="true" />
        </button>
      )}
      {status === 'uploading' && (
        <span
          role="progressbar"
          aria-valuemin={0}
          aria-valuemax={100}
          aria-valuenow={pct}
          className="absolute inset-x-1 bottom-0 h-1 overflow-hidden rounded-full bg-muted"
        >
          <span
            className="block h-full bg-primary transition-[width]"
            style={{ width: `${pct}%` }}
          />
        </span>
      )}
      {onRemove && (
        <button
          type="button"
          onClick={() => onRemove(id)}
          aria-label={`Remove ${name}`}
          className="ml-1 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-background hover:text-foreground"
        >
          <X className="h-3 w-3" aria-hidden="true" />
        </button>
      )}
    </li>
  )
}
