import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Check, ChevronDown, CircleSlash, Loader2, Pin, X } from 'lucide-react';
import type { Tone } from '@/components/Data';
import { toneText } from '@/components/Data';
import type { ChatMessage } from '../types';
import { useBackgroundTasks, useTaskDetail, type BackgroundTask } from '../useBackgroundTasks';
import { SubagentTrace } from './ToolActivity';
import { CodeSurface } from './CodeSurface';

type BackgroundTaskTrayProps = {
  messages: ChatMessage[];
};

/** Same vocabulary as the transcript's own task rows — a completed task must not be green here and grey there. */
function statusTone(status: BackgroundTask['status']): Tone {
  if (status === 'completed') return 'success';
  if (status === 'failed') return 'danger';
  if (status === 'stopped') return 'neutral';
  return 'warning';
}

/**
 * The background tasks of this conversation, docked above the input.
 *
 * They are also rows in the transcript, and stay there — this is the same information pinned where it
 * doesn't scroll away, because a task you started ten minutes ago is the one you most want to watch.
 * Clicking a chip opens what it is actually doing right now: a subagent's own tool-by-tool trace, or the
 * tail of a backgrounded shell's log, both read from the file Claude Code streams the task into.
 */
export const BackgroundTaskTray = ({ messages }: BackgroundTaskTrayProps) => {
  const { tasks, pinned, unpinned, running, clearable, togglePin, dismiss, dismissFinished } =
    useBackgroundTasks(messages);
  const [openId, setOpenId] = useState<string | null>(null);

  // Derived, not stored: dismissing the chip that is open closes the panel without a second piece of state
  // having to be kept in step with the first.
  const open = tasks.find((t) => t.taskId === openId) ?? null;

  if (tasks.length === 0) return null;

  const chipProps = (task: BackgroundTask) => ({
    task,
    active: task.taskId === openId,
    onClick: () => setOpenId(task.taskId === openId ? null : task.taskId),
    onTogglePin: () => togglePin(task.taskId),
    // Finished only. A running chip is the only handle on work still going on, so closing one would hide
    // something you cannot get back to — the bulk clear draws the same line.
    onDismiss: task.status ? () => dismiss(task.taskId) : undefined,
  });

  return (
    <div className="mb-2 rounded-lg border border-border bg-muted/40">
      {open && <TaskPanel task={open} onClose={() => setOpenId(null)} />}

      {pinned.length > 0 && (
        <TaskStrip label="Pinned" divided={unpinned.length > 0}>
          {pinned.map((task) => (
            <TaskChip key={task.taskId} pinned {...chipProps(task)} />
          ))}
        </TaskStrip>
      )}

      {unpinned.length > 0 && (
        <TaskStrip
          label={running.length > 0 ? `${running.length} running` : 'Background'}
          trailing={
            clearable.length > 0 && (
              <button
                type="button"
                onClick={dismissFinished}
                title="Clear all finished"
                aria-label="Clear all finished tasks"
                className="shrink-0 cursor-pointer rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
              >
                <X className="h-3.5 w-3.5" />
              </button>
            )
          }
        >
          {unpinned.map((task) => (
            <TaskChip key={task.taskId} pinned={false} {...chipProps(task)} />
          ))}
        </TaskStrip>
      )}
    </div>
  );
};

// ── Strip ──

type TaskStripProps = {
  label: string;
  children: ReactNode;
  trailing?: ReactNode;
  divided?: boolean;
};

const TaskStrip = ({ label, children, trailing, divided }: TaskStripProps) => (
  <div className={`flex items-center gap-2 px-2 py-2 ${divided ? 'border-b border-border' : ''}`}>
    <span className="shrink-0 text-xs uppercase tracking-wider text-muted-foreground">{label}</span>
    {/* pb-1.5 keeps the scrollbar clear of the chips; the matching -mb-1.5 hides that pad from layout,
        so it draws into the container's own padding and the label and X still centre on the chips. */}
    <div className="task-strip -mb-1.5 flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto pb-1.5">{children}</div>
    {trailing}
  </div>
);

// ── Chip ──

type TaskChipProps = {
  task: BackgroundTask;
  active: boolean;
  pinned: boolean;
  onClick: () => void;
  onTogglePin: () => void;
  /** Absent for a running task — see the call site. */
  onDismiss?: () => void;
};

/**
 * The pill is a div wrapping several buttons rather than one button with controls inside it: a button
 * nested in a button is invalid, and the browser resolves it by swallowing one of the two clicks.
 */
const TaskChip = ({ task, active, pinned, onClick, onTogglePin, onDismiss }: TaskChipProps) => {
  const { status } = task;
  const tone = toneText[statusTone(status)];
  const label = task.description || 'Background task';

  return (
    <div
      className={`group flex shrink-0 items-center rounded-full border text-xs transition-colors ${
        active
          ? 'border-duck-teal/40 bg-duck-teal/10 text-foreground'
          : 'border-border bg-background/60 text-muted-foreground hover:border-foreground/20 hover:text-foreground'
      }`}
    >
      <button
        type="button"
        onClick={onClick}
        aria-expanded={active}
        className="flex cursor-pointer items-center gap-1.5 py-1 pl-2.5 pr-1"
      >
        {status === 'completed' ? (
          <Check className={`h-3 w-3 ${tone}`} />
        ) : status === 'failed' ? (
          // Was a CircleSlash, the same icon as "stopped" — the two outcomes you most need to tell apart
          // were the one glyph.
          <X className={`h-3 w-3 ${tone}`} />
        ) : status ? (
          <CircleSlash className={`h-3 w-3 ${tone}`} />
        ) : (
          <span className="h-1.5 w-1.5 shrink-0 animate-pulse rounded-full bg-warning" />
        )}
        <span className="max-w-[14rem] truncate">{label}</span>
      </button>
      {/* Always drawn, not revealed on hover: the strip is as much a touch target as a pointer one, and a
          control you can only find by hovering is one half the devices cannot find at all. */}
      <button
        type="button"
        onClick={onTogglePin}
        aria-pressed={pinned}
        title={pinned ? 'Unpin' : 'Pin to the top row'}
        aria-label={`${pinned ? 'Unpin' : 'Pin'} ${label}`}
        className={`cursor-pointer py-1 pl-0.5 transition-colors ${onDismiss ? 'pr-0.5' : 'pr-2'} ${
          pinned ? 'text-duck-teal' : 'text-muted-foreground/50 hover:text-foreground'
        }`}
      >
        <Pin className={`h-3 w-3 ${pinned ? 'fill-current' : ''}`} />
      </button>
      {onDismiss && (
        <button
          type="button"
          onClick={onDismiss}
          title="Dismiss"
          aria-label={`Dismiss ${label}`}
          className="cursor-pointer rounded-full py-1 pl-0.5 pr-2 text-muted-foreground/50 transition-colors hover:text-foreground"
        >
          <X className="h-3 w-3" />
        </button>
      )}
    </div>
  );
};

// ── Panel ──

type TaskPanelProps = {
  task: BackgroundTask;
  onClose: () => void;
};

const TaskPanel = ({ task, onClose }: TaskPanelProps) => {
  const running = !task.status;
  const { data, isLoading } = useTaskDetail(task.taskId, running);
  const bodyRef = useRef<HTMLDivElement | null>(null);
  const pinnedRef = useRef(true);

  // Follow the tail while the owner is at the bottom, and stop the moment they scroll up to read
  // something — a live trace that yanks itself back down is unreadable.
  useEffect(() => {
    const body = bodyRef.current;
    if (body && pinnedRef.current) body.scrollTop = body.scrollHeight;
  }, [data]);

  return (
    <div className="border-b border-border">
      <div className="flex items-center gap-2 px-2 py-1.5">
        <div className="min-w-0 flex-1">
          <div className="truncate text-xs text-foreground/80">{task.description || 'Background task'}</div>
          <div className="truncate text-xs text-muted-foreground">
            {task.taskType ?? 'task'} · {task.status ? (task.summary ?? task.status) : 'running…'}
          </div>
        </div>
        {running && <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-warning" />}
        <button
          type="button"
          onClick={onClose}
          title="Collapse"
          aria-label="Collapse task detail"
          className="shrink-0 cursor-pointer rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
        >
          <ChevronDown className="h-3.5 w-3.5" />
        </button>
      </div>

      <div
        ref={bodyRef}
        onScroll={(ev) => {
          const el = ev.currentTarget;
          pinnedRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
        }}
        // The composer is `shrink-0` and the transcript above it is `flex-1 min-h-0`, so every pixel this
        // panel takes comes out of the transcript — a flat max-h-64 could leave almost no conversation
        // visible in a short panel. Capped against the viewport as well as in absolute terms.
        className="max-h-[min(16rem,25vh)] overflow-y-auto border-t border-border px-2 py-1.5"
      >
        <TaskBody detail={data} isLoading={isLoading} />
      </div>
    </div>
  );
};

type TaskBodyProps = {
  detail: ReturnType<typeof useTaskDetail>['data'];
  isLoading: boolean;
};

const TaskBody = ({ detail, isLoading }: TaskBodyProps) => {
  if (isLoading && !detail) return <Placeholder text="Reading…" />;
  if (!detail || detail.kind === 'pending') return <Placeholder text="Nothing written yet." />;

  if (detail.kind === 'log') {
    if (!detail.text) return <Placeholder text="(no output yet)" />;
    return (
      <>
        {detail.truncated && <div className="mb-1 text-xs text-muted-foreground">… earlier output trimmed</div>}
        {/* The green-on-black terminal costume this used to wear was the last of its kind in the chat, and
            it bought nothing: a shell log is the one thing you most often want to hand to someone else, and
            it was the one block with no copy button. On the shared surface it gets both copy modes.

            Unnumbered on purpose — this is a live tail, and `truncated` means it can start mid-file. A
            gutter counting from 1 would be stating a line number that is not the line's number. */}
        <CodeSurface code={detail.text} numbered={false} />
      </>
    );
  }

  if (detail.messages.length === 0) return <Placeholder text="Nothing written yet." />;
  return <SubagentTrace messages={detail.messages} />;
};

const Placeholder = ({ text }: { text: string }) => (
  <div className="px-1 py-2 text-xs text-muted-foreground">{text}</div>
);
