/*
 *  InputBar.tsx
 *  Zyquo Cloud Web
 *
 *  Author: Simon-Pierre Boucher
 *  Mail: contact@spboucher.ai
 *
 *  Floating input card: multiline auto-grow editor, image attach/paste for
 *  vision models, text-file attach, drag-and-drop, params popover trigger,
 *  circular accent send (⌘/Ctrl+Enter) that becomes Stop while streaming,
 *  token/cost HUD with context-window usage bar.
 */

import { useEffect, useMemo, useRef, useState } from 'react'
import { estimateTokens, findModel } from '../features/catalogHelpers'
import { useStore } from '../state/store'
import type { Attachment, Conversation } from '../types'
import { uid } from '../state/store'
import SlashMenu from './SlashMenu'
import { IconMic, IconPaperclip, IconSend, IconSliders, IconStop, IconX } from './icons'

interface SpeechRecognitionLike {
  lang: string
  interimResults: boolean
  continuous: boolean
  onresult: ((event: { results: ArrayLike<ArrayLike<{ transcript: string }>> }) => void) | null
  onend: (() => void) | null
  onerror: (() => void) | null
  start: () => void
  stop: () => void
}

function speechRecognition(): SpeechRecognitionLike | null {
  const w = window as unknown as Record<string, unknown>
  const Ctor = (w['SpeechRecognition'] ?? w['webkitSpeechRecognition']) as
    | (new () => SpeechRecognitionLike)
    | undefined
  return Ctor ? new Ctor() : null
}

const IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']
const MAX_TEXT_FILE = 512 * 1024

export default function InputBar({
  conversation,
  draft,
  setDraft,
  onOpenParams,
}: {
  conversation: Conversation
  draft: string
  setDraft: (text: string) => void
  onOpenParams: () => void
}) {
  const send = useStore((s) => s.send)
  const stop = useStore((s) => s.stop)
  const streaming = useStore((s) => s.streaming[conversation.id])
  const toast = useStore((s) => s.toast)
  const [attachments, setAttachments] = useState<Attachment[]>([])
  const [dragging, setDragging] = useState(false)
  const [dictating, setDictating] = useState(false)
  const editorRef = useRef<HTMLTextAreaElement>(null)
  const fileRef = useRef<HTMLInputElement>(null)
  const recognitionRef = useRef<SpeechRecognitionLike | null>(null)

  // Slash command: draft starts with "/" → menu with query (word after slash)
  // and input (rest of the draft after the first whitespace).
  const slashMatch = draft.startsWith('/') ? /^\/(\S*)\s?([\s\S]*)$/.exec(draft) : null

  const toggleDictation = () => {
    if (dictating) {
      recognitionRef.current?.stop()
      return
    }
    const recognition = speechRecognition()
    if (!recognition) {
      toast('Speech recognition not available in this browser', 'error')
      return
    }
    recognition.lang = navigator.language || 'en-US'
    recognition.interimResults = false
    recognition.continuous = true
    recognition.onresult = (event) => {
      const last = event.results[event.results.length - 1]
      const transcript = last?.[0]?.transcript ?? ''
      if (transcript !== '') setDraft(`${draft}${draft === '' ? '' : ' '}${transcript}`)
    }
    recognition.onend = () => setDictating(false)
    recognition.onerror = () => setDictating(false)
    recognitionRef.current = recognition
    setDictating(true)
    recognition.start()
  }

  const model = findModel(conversation.provider, conversation.modelID)
  const vision = model?.capabilities.vision ?? false

  useEffect(() => {
    const editor = editorRef.current
    if (!editor) return
    editor.style.height = 'auto'
    editor.style.height = `${Math.min(editor.scrollHeight, 200)}px`
  }, [draft])

  const contextUsage = useMemo(() => {
    if (!model) return 0
    let chars = (conversation.systemPrompt ?? '').length + draft.length
    for (const message of conversation.messages) chars += message.text.length
    return Math.min(1, estimateTokens('x'.repeat(chars)) / model.contextWindow)
  }, [conversation, draft, model])

  const addFiles = async (files: FileList | File[]) => {
    for (const file of files) {
      if (IMAGE_TYPES.includes(file.type)) {
        if (!vision) {
          toast(`${model?.displayName ?? 'This model'} doesn't support images`, 'error')
          continue
        }
        const data = await fileToBase64(file)
        setAttachments((prev) => [
          ...prev,
          { id: uid(), kind: 'image', fileName: file.name, mimeType: file.type, data },
        ])
      } else if (file.size <= MAX_TEXT_FILE) {
        const text = await file.text()
        setAttachments((prev) => [
          ...prev,
          {
            id: uid(),
            kind: 'textFile',
            fileName: file.name,
            mimeType: file.type || 'text/plain',
            data: text,
          },
        ])
      } else {
        toast(`${file.name} is too large (max 512 KB for text files)`, 'error')
      }
    }
  }

  const doSend = () => {
    const text = draft.trim()
    if (text === '' && attachments.length === 0) return
    setDraft('')
    const toSend = attachments
    setAttachments([])
    void send(conversation.id, text, toSend)
  }

  const onKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
      e.preventDefault()
      doSend()
    }
  }

  const onPaste = (e: React.ClipboardEvent) => {
    const files = [...e.clipboardData.files]
    if (files.length > 0) {
      e.preventDefault()
      void addFiles(files)
    }
  }

  const totalTokens = useMemo(() => {
    let tokens = 0
    let cost = 0
    for (const message of conversation.messages) {
      tokens += (message.usage?.inputTokens ?? 0) + (message.usage?.outputTokens ?? 0)
      cost += message.estimatedCost ?? 0
    }
    return { tokens, cost }
  }, [conversation])

  return (
    <div className="input-dock">
      <div
        className={`input-bar${dragging ? ' drag-target' : ''}`}
        onDragOver={(e) => {
          e.preventDefault()
          setDragging(true)
        }}
        onDragLeave={() => setDragging(false)}
        onDrop={(e) => {
          e.preventDefault()
          setDragging(false)
          void addFiles(e.dataTransfer.files)
        }}
      >
        {attachments.length > 0 && (
          <div className="attachment-strip">
            {attachments.map((attachment) => (
              <div key={attachment.id} className="attachment-thumb">
                {attachment.kind === 'image' ? (
                  <img
                    src={`data:${attachment.mimeType};base64,${attachment.data}`}
                    alt={attachment.fileName}
                  />
                ) : (
                  <span>{attachment.fileName}</span>
                )}
                <button
                  className="attachment-remove"
                  onClick={() =>
                    setAttachments((prev) => prev.filter((a) => a.id !== attachment.id))
                  }
                >
                  <IconX size={8} />
                </button>
              </div>
            ))}
          </div>
        )}
        <div className="input-row" style={{ position: 'relative' }}>
          {slashMatch && (
            <SlashMenu
              query={slashMatch[1] ?? ''}
              input={slashMatch[2] ?? ''}
              conversationID={conversation.id}
              onApply={(text) => {
                setDraft(text)
                editorRef.current?.focus()
              }}
              onClose={() => setDraft(draft.replace(/^\//, ''))}
            />
          )}
          <button
            className="icon-button"
            title={vision ? 'Attach image or file' : 'Attach text file'}
            onClick={() => fileRef.current?.click()}
          >
            <IconPaperclip />
          </button>
          <input
            ref={fileRef}
            type="file"
            multiple
            hidden
            onChange={(e) => {
              if (e.target.files) void addFiles(e.target.files)
              e.target.value = ''
            }}
          />
          <textarea
            ref={editorRef}
            className="input-editor"
            placeholder="Message…"
            rows={1}
            value={draft}
            data-input-editor
            onChange={(e) => setDraft(e.target.value)}
            onKeyDown={onKeyDown}
            onPaste={onPaste}
          />
          <button
            className="icon-button"
            title={dictating ? 'Stop dictation' : 'Dictate'}
            style={dictating ? { color: 'var(--z-danger)' } : {}}
            onClick={toggleDictation}
          >
            <IconMic />
          </button>
          <button className="icon-button" title="Parameters" onClick={onOpenParams}>
            <IconSliders />
          </button>
          {streaming ? (
            <button
              className="send-button stop"
              title="Stop (⌘.)"
              onClick={() => stop(conversation.id)}
            >
              <IconStop />
            </button>
          ) : (
            <button
              className="send-button"
              title="Send (⌘↩)"
              disabled={draft.trim() === '' && attachments.length === 0}
              onClick={doSend}
            >
              <IconSend />
            </button>
          )}
        </div>
        <div className="input-hud">
          {model && (
            <>
              <div className="context-bar" title="Context window usage">
                <div
                  className={`context-bar-fill${contextUsage > 0.8 ? ' warn' : ''}`}
                  style={{ width: `${Math.max(2, contextUsage * 100)}%` }}
                />
              </div>
              <span>{Math.round(contextUsage * 100)}% ctx</span>
            </>
          )}
          {totalTokens.tokens > 0 && (
            <span>
              {totalTokens.tokens.toLocaleString()} tok
              {totalTokens.cost > 0 ? ` · ~$${totalTokens.cost.toFixed(4)}` : ''}
            </span>
          )}
        </div>
      </div>
    </div>
  )
}

function fileToBase64(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = () => {
      const result = reader.result as string
      resolve(result.slice(result.indexOf(',') + 1))
    }
    reader.onerror = () => reject(reader.error ?? new Error('read failed'))
    reader.readAsDataURL(file)
  })
}
