import type { DragEvent } from 'react';
import { useRef, useState } from 'react';
import { Loader2, Mic, Send, Square, ListPlus, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import type { QueuedPrompt, UseEmbeddableChatType } from '../EmbeddableChat/useEmbeddableChat';
import { useAudioRecording } from '../useAudioRecording';
import { ModelSelector } from './ModelSelector';
import { AttachmentList } from './AttachmentList';
import { AttachButton } from './AttachButton';
import { WebpageDialog } from './WebpageDialog';
import { BackgroundTaskTray } from './BackgroundTaskTray';

type InputAreaProps = {
  manager: UseEmbeddableChatType;
};

export const InputArea = ({ manager }: InputAreaProps) => {
  const {
    input,
    setInput,
    handleSend,
    handleKeyDown,
    stopGeneration,
    isGenerating,
    isConnected,
    commandFeedback,
    textareaRef,
    messages,
    availableModels,
    selectedModel,
    setSelectedModel,
    model,
    hasStarted,
    attachments,
    attachWebpage,
    attachImage,
    removeAttachment,
    appendToInput,
    thinkingLevel,
    setThinkingLevel,
    queued,
    sendQueuedNow,
    unqueue,
  } = manager;

  const { recording, transcribing, toggleRecording } = useAudioRecording(appendToInput);
  const [urlDialogOpen, setUrlDialogOpen] = useState(false);

  // Does the model actually accept images?
  //
  // `list-models.ts` has always published this per model and NOTHING read it, so every image affordance
  // was offered on every harness. On OpenCode the result was a lie you could watch happen: drop a
  // screenshot, see it render in your own bubble, and have it discarded before the model ever saw it —
  // `handleOpenCodeChat`'s message type has no `images` field.
  //
  // `selectedModel || model` mirrors ModelSelector's `displayModel`, so the gate and the visible model
  // name can never disagree. Unknown model → allow: a missing capability should not silently remove a
  // control that works, and the flag is only false where we know it is false.
  const effectiveModelId = selectedModel || model;
  const supportsImages = availableModels.find((m) => m.id === effectiveModelId)?.images !== false;

  const drop = useImageDrop(attachImage, supportsImages);

  return (
    <div
      className="relative shrink-0 border-t border-border bg-background/60 p-2 md:p-3"
      onDragEnter={drop.onDragEnter}
      onDragOver={drop.onDragOver}
      onDragLeave={drop.onDragLeave}
      onDrop={drop.onDrop}
    >
      {drop.isTarget && (
        <div className="pointer-events-none absolute inset-1 z-10 flex items-center justify-center rounded-lg border-2 border-dashed border-duck-teal bg-background/80">
          <span className="text-xs font-medium text-duck-teal">Drop images to attach</span>
        </div>
      )}
      {commandFeedback && (
        <div className="mb-2 px-3 py-1.5 text-xs text-duck-teal bg-duck-teal/10 rounded-md">{commandFeedback}</div>
      )}

      <BackgroundTaskTray messages={messages} />

      <QueuedList queued={queued} onRemove={unqueue} onSendNow={sendQueuedNow} />

      <AttachmentList attachments={attachments} onRemove={removeAttachment} />

      <div className="flex items-end gap-1 md:gap-2">
        <AttachButton
          onAttachImage={supportsImages ? attachImage : undefined}
          onAttachWebpage={() => setUrlDialogOpen(true)}
          onAttachText={appendToInput}
        />
        <MicButton recording={recording} transcribing={transcribing} onToggle={toggleRecording} />

        <textarea
          ref={textareaRef}
          value={input}
          onChange={(ev) => setInput(ev.target.value)}
          onKeyDown={handleKeyDown}
          onPaste={(ev) => {
            // Let an image paste fall through to the browser's default (nothing) rather than attaching
            // something this model will discard.
            if (!supportsImages) return;
            const items = ev.clipboardData?.items;
            if (!items) return;
            for (const item of Array.from(items)) {
              if (item.type.startsWith('image/')) {
                ev.preventDefault();
                const file = item.getAsFile();
                if (file) attachImage(file);
                return;
              }
            }
          }}
          placeholder="Type a message..."
          rows={1}
          className="min-w-0 flex-1 resize-none rounded-lg border border-input bg-background/80 px-2 py-1.5 md:px-3 md:py-2 text-base md:text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
        />
        {/* Stop stays put while a turn runs, and the send button sits beside it rather than replacing it:
            both are things you might want mid-turn, and swapping one for the other would mean typing a
            follow-up cost you the ability to interrupt. Amber and a different icon say the press will not
            go anywhere yet — the same amber the queued tray above uses. */}
        {isGenerating && (
          <Button
            onClick={stopGeneration}
            variant="destructive"
            size="icon"
            title="Stop"
            className="shrink-0 h-7 w-7 md:h-9 md:w-9 cursor-pointer"
          >
            <Square className="h-4 w-4" />
          </Button>
        )}
        <Button
          onClick={handleSend}
          disabled={!input.trim() || !isConnected}
          size="icon"
          title={isGenerating ? 'Queue — sends when the current turn finishes' : 'Send'}
          className={`shrink-0 h-7 w-7 md:h-9 md:w-9 cursor-pointer disabled:opacity-40 ${
            isGenerating ? 'bg-amber-500 hover:bg-amber-500/90 text-amber-950' : 'bg-duck-teal hover:bg-duck-teal/90'
          }`}
        >
          {isGenerating ? <ListPlus className="h-4 w-4" /> : <Send className="h-4 w-4" />}
        </Button>
      </div>
      <ModelSelector
        messages={messages}
        availableModels={availableModels}
        selectedModel={selectedModel}
        onModelChange={setSelectedModel}
        model={model}
        isConnected={isConnected}
        isGenerating={isGenerating}
        hasStarted={hasStarted}
        thinkingLevel={thinkingLevel}
        onThinkingChange={setThinkingLevel}
      />

      <WebpageDialog open={urlDialogOpen} onOpenChange={setUrlDialogOpen} onSubmit={attachWebpage} />
    </div>
  );
};

// ── Helpers ──

/**
 * Drop images anywhere on the composer, not just on the textarea — the target is the whole bar, because
 * a screenshot dragged out of the macOS corner thumbnail is a small thing to aim with.
 *
 * Three things this has to get right, each of which silently breaks the drop if missed:
 *
 *  - **`preventDefault` on dragover.** Without it the browser refuses the drop and never fires `onDrop`;
 *    it just navigates to the file instead, throwing away whatever was typed.
 *  - **A depth counter, not a boolean.** `dragenter`/`dragleave` fire for every child crossed, so moving
 *    over the textarea or a button reads as leaving the bar and the highlight strobes.
 *  - **Only claim drags that carry files.** Dragging selected text across the composer would otherwise
 *    light it up and then swallow the drop, which is how you lose a text-drag into the input.
 */
function useImageDrop(attachImage: (file: File) => void, enabled = true) {
  const [isTarget, setIsTarget] = useState(false);
  const depth = useRef(0);

  // When the model takes no images, do not claim the drag at all: no highlight, no preventDefault, and
  // the drop is left to the browser. Claiming it and then dropping the file on the floor is the exact
  // behaviour this gate exists to remove.
  const carriesFiles = (ev: DragEvent<HTMLElement>) => enabled && Array.from(ev.dataTransfer.types).includes('Files');

  const reset = () => {
    depth.current = 0;
    setIsTarget(false);
  };

  return {
    isTarget,
    onDragEnter: (ev: DragEvent<HTMLElement>) => {
      if (!carriesFiles(ev)) return;
      ev.preventDefault();
      depth.current += 1;
      setIsTarget(true);
    },
    onDragOver: (ev: DragEvent<HTMLElement>) => {
      if (!carriesFiles(ev)) return;
      ev.preventDefault();
      ev.dataTransfer.dropEffect = 'copy';
    },
    onDragLeave: (ev: DragEvent<HTMLElement>) => {
      if (!carriesFiles(ev)) return;
      depth.current -= 1;
      if (depth.current <= 0) reset();
    },
    onDrop: (ev: DragEvent<HTMLElement>) => {
      if (!carriesFiles(ev)) return;
      ev.preventDefault();
      reset();
      // Images only, and quietly: a drag can carry several files, and refusing the PDF among them with a
      // toast would be noise when the three screenshots you meant went in fine.
      for (const file of Array.from(ev.dataTransfer.files)) {
        if (file.type.startsWith('image/')) attachImage(file);
      }
    },
  };
}

/**
 * What is waiting to be sent. Without this a queued prompt is invisible until its turn comes — the
 * composer empties and nothing else changes, which reads exactly like the message having been lost.
 * Each is removable right up until it goes.
 */
const QueuedList = ({
  queued,
  onRemove,
  onSendNow,
}: {
  queued: QueuedPrompt[];
  onRemove: (id: string) => void;
  onSendNow: () => void;
}) => {
  if (queued.length === 0) return null;

  return (
    <div className="mb-2 flex flex-col gap-1">
      {/* The keystroke is otherwise undiscoverable — Enter on an empty composer has never done anything,
          so nobody would try it. Stated once above the queue rather than on every row. */}
      <button
        type="button"
        onClick={onSendNow}
        title="Send these into the turn that is already running"
        className="cursor-pointer self-start rounded px-1 text-[11px] text-amber-700/80 transition-colors hover:bg-amber-500/10 hover:text-amber-700"
      >
        Send now — press Enter on an empty box to add {queued.length === 1 ? 'this' : 'these'} to the running turn
      </button>
      {queued.map((item) => (
        <div
          key={item.id}
          className="flex items-center gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-2 py-1"
        >
          <ListPlus className="h-3 w-3 shrink-0 text-amber-600" />
          <span className="min-w-0 flex-1 truncate text-xs text-foreground/80">{item.raw.trim()}</span>
          <button
            type="button"
            onClick={() => onRemove(item.id)}
            title="Remove from queue"
            className="shrink-0 cursor-pointer rounded p-0.5 text-muted-foreground transition-colors hover:bg-black/10 hover:text-foreground"
          >
            <X className="h-3 w-3" />
          </button>
        </div>
      ))}
    </div>
  );
};

type MicButtonProps = {
  recording: boolean;
  transcribing: boolean;
  onToggle: () => void;
};

const MicButton = ({ recording, transcribing, onToggle }: MicButtonProps) => (
  <button
    type="button"
    disabled={transcribing}
    onClick={onToggle}
    className="relative shrink-0 h-7 w-7 md:h-9 md:w-9 flex items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
  >
    {transcribing ? (
      <Loader2 className="h-4 w-4 animate-spin" />
    ) : recording ? (
      <>
        <span className="absolute inset-0 rounded-lg animate-ping bg-destructive/30" />
        <Square className="h-3.5 w-3.5 text-destructive" />
      </>
    ) : (
      <Mic className="h-4 w-4" />
    )}
  </button>
);
