import { useMemo, useState } from 'react';
import { FileText, Terminal, Pencil, Search, Globe, Wrench, ChevronRight, Bot } from 'lucide-react';
import type { ChatMessage } from '../types';
import type { DiffLine } from './line-diff';
import { diffStat, editDiff } from './line-diff';
import { CodeSurface } from './CodeSurface';
import { CopyButton } from './CopyButton';
import { useToolOpen } from './tool-open';

type ToolMessage = Extract<ChatMessage, { role: 'tool' }>;

type ToolActivityProps = {
  message: ToolMessage;
  /**
   * Whether this row starts expanded — true for the turn you are watching happen, false everywhere else.
   *
   * This used to be a clock: the row stamped its own arrival, held itself open for five seconds and then
   * collapsed on a timer. That made a running turn shift under the reader continuously, each row changing
   * height at a moment nobody chose, and it made the answer depend on when a virtualised row happened to
   * first render rather than on when the call happened. Expansion is now a fact about position in the
   * conversation, so a row only changes height when you click it or when you move on to the next turn.
   */
  defaultOpen?: boolean;
};

/**
 * Expanded output is a glance, not a read: the cap keeps a 400-line file from taking the viewport, and
 * `CodeSurface`'s own "Show more" gives the whole thing in one click when the glance isn't enough.
 */
const MAX_LINES = 10;

const toolIcons: Record<string, typeof FileText> = {
  Read: FileText,
  Edit: Pencil,
  Write: Pencil,
  Bash: Terminal,
  Grep: Search,
  Glob: Search,
  WebFetch: Globe,
  WebSearch: Globe,
  Task: Bot,
};

function getToolSummary(toolName: string, toolInput: Record<string, unknown>): string {
  switch (toolName) {
    case 'Read':
    case 'Edit':
    case 'Write':
      return (toolInput.file_path as string) ?? '';
    case 'Bash':
      return bashOperation((toolInput.command as string) ?? '');
    case 'Grep':
    case 'Glob':
      return (toolInput.pattern as string) ?? '';
    case 'WebFetch':
      return (toolInput.url as string) ?? '';
    case 'WebSearch':
      return (toolInput.query as string) ?? '';
    case 'Task': {
      // `description` is the short label; the prompt is the essay. Showing the essay made every subagent
      // row look identical for its first eighty characters.
      const description = (toolInput.description as string) ?? '';
      const type = (toolInput.subagent_type as string) ?? '';
      return type ? `${description} · ${type}` : description;
    }
    default:
      return (
        Object.values(toolInput)
          .find((v) => typeof v === 'string')
          ?.toString() ?? ''
      );
  }
}

/**
 * The first segment of a compound command is usually scaffolding — a `cd` into the repo, or an `echo`
 * labelling the output for a human — and it is exactly what a head-truncated summary has room to show.
 * A stack of rows then reads `cd /home/…/platform && git status…`, `echo "=== server-side…`, with the
 * grep that actually answered the question off the end of every one of them.
 *
 * Only a leading `cd`/`echo` is skipped, and only up to a following `&&`/`;`, so nothing that does work
 * is ever hidden. This is display-only: expanding the row, and the copy button, still give the real
 * command verbatim.
 */
const PREAMBLE = /^\s*(?:cd\s+[^&;|]+|echo\s+(?:"[^"]*"|'[^']*'|[^&;|]+))\s*(?:&&|;)\s*/;

function bashOperation(command: string): string {
  let rest = command;
  for (let i = 0; i < 3; i++) {
    const next = rest.replace(PREAMBLE, '');
    if (next === rest) break;
    rest = next;
  }
  // A command that is *only* a preamble (`cd somewhere`) still has to say something.
  return rest.trim() || command.trim();
}

/**
 * Split a summary so the browser truncates it in the **middle** instead of at the end.
 *
 * The end is where the identity lives: one filename per row distinguishes ten `Read`s that share a
 * directory, and a command's target is the last thing on the line. Cutting the tail — which both
 * `slice()` and CSS `truncate` do — throws away precisely the part you were scanning for. So the tail
 * is pinned as its own non-shrinking span and the head is left to ellipsise, which lands the cut in the
 * middle at whatever width the panel happens to be.
 */
const HEAD_MAX = 200;

function splitSummary(text: string): { head: string; tail: string } {
  const flat = text.replace(/\s+/g, ' ').trim();
  if (flat.length <= 48) return { head: flat, tail: '' };
  const window = flat.slice(-40);
  const cut = Math.max(window.lastIndexOf('/'), window.lastIndexOf(' '));
  // Prefer a real boundary; a tail cut mid-token still beats no tail at all.
  const tail = cut > 0 && cut < window.length - 1 ? window.slice(cut + 1) : window.slice(-24);
  const head = flat.slice(0, flat.length - tail.length);
  return { head: head.length > HEAD_MAX ? head.slice(0, HEAD_MAX) : head, tail };
}

/**
 * What the collapsed row never said: whether the call found anything.
 *
 * `done` reported only that it ran — the least interesting fact in a trace, in the loudest colour on the
 * row, repeated on every row. Whether a grep matched is the thing you were actually scanning for, and it
 * used to cost an expand to learn.
 */
function outputHint(toolName: string, output: string): string | null {
  const lines = output.split('\n').filter((l) => l.trim()).length;
  const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? '' : 's'}`;
  switch (toolName) {
    case 'Grep':
      return lines ? plural(lines, 'match') : 'no matches';
    case 'Glob':
      return lines ? plural(lines, 'file') : 'no files';
    case 'Read':
    case 'Bash':
      return lines ? plural(lines, 'line') : 'no output';
    default:
      return null;
  }
}

/**
 * Copy gives you the thing you'd want to paste — the command, or the text the file ends up containing —
 * never the `key: value` dump, which is a rendering of the call and not usable as anything.
 */
function copyableInput(toolName: string, toolInput: Record<string, unknown>): string {
  const pick = (key: string): string | null => (typeof toolInput[key] === 'string' ? (toolInput[key] as string) : null);
  if (toolName === 'Bash') return pick('command') ?? JSON.stringify(toolInput, null, 2);
  if (toolName === 'Edit') return pick('new_string') ?? JSON.stringify(toolInput, null, 2);
  if (toolName === 'Write') return pick('content') ?? JSON.stringify(toolInput, null, 2);
  return Object.entries(toolInput)
    .map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
    .join('\n');
}

/**
 * `Read` hands back its own gutter — `␣␣␣123\tcontent` — which is real file line numbers, not a 1-based
 * count, and worth keeping. So the gutter is peeled off the text and fed back to the surface as a starting
 * number, rather than left in place to be highlighted as if it were code and then joined by a second
 * gutter. Anything that isn't overwhelmingly numbered is passed through untouched.
 */
const NUMBERED = /^\s*(\d+)\t/;

function unnumber(output: string): { code: string; startLine: number } {
  const lines = output.split('\n');
  const numbered = lines.filter((line) => NUMBERED.test(line));
  if (!numbered.length || numbered.length < lines.length * 0.8) return { code: output, startLine: 1 };
  return {
    code: lines.map((line) => line.replace(NUMBERED, '')).join('\n'),
    startLine: Number(NUMBERED.exec(numbered[0]!)![1]),
  };
}

// Most extensions are already valid shiki ids; only the aliases it does not carry need naming. An id it
// has no grammar for fails soft inside `highlight`, so a miss here costs colour, never the block.
const EXT_LANG: Record<string, string> = { yml: 'yaml', mjs: 'js', cjs: 'js', h: 'c', hpp: 'cpp', htm: 'html' };

function langForPath(path: unknown): string {
  if (typeof path !== 'string') return '';
  const name = path.split('/').pop() ?? '';
  const ext = name.includes('.') ? name.split('.').pop()!.toLowerCase() : '';
  return EXT_LANG[ext] ?? ext;
}

export const ToolActivity = ({ message, defaultOpen }: ToolActivityProps) => {
  const Icon = toolIcons[message.toolName] ?? Wrench;
  const { head, tail } = splitSummary(getToolSummary(message.toolName, message.toolInput));
  const pending = message.output === undefined;
  const isError = message.isError === true;

  // Your click is remembered above the row and outranks the default for as long as the conversation is
  // open — including across the unmount a virtualised list does whenever this scrolls out of view.
  const [open, toggle] = useToolOpen(message.toolCallId, defaultOpen === true);
  const children = message.children ?? [];
  const hint = pending || isError ? null : outputHint(message.toolName, message.output ?? '');

  // Computed even while collapsed, because the +/− counts are what make the collapsed row worth reading.
  const diff = useMemo(() => editDiff(message.toolName, message.toolInput), [message.toolName, message.toolInput]);
  const stat = diff ? diffStat(diff) : null;

  return (
    <div className="my-1">
      {/* A failure reads as a failure while collapsed. It behaves like every other row — same default, same
          folding — and differs only in colour, so a call that went wrong stays findable by scanning rather
          than by opening things, and stays nameable in conversation without either of us opening it. */}
      <button
        onClick={toggle}
        className={`flex w-full cursor-pointer items-center gap-2 rounded-md px-3 py-1.5 text-left text-sm transition-colors ${
          isError ? 'bg-destructive/5 hover:bg-destructive/10' : 'hover:bg-muted'
        }`}
        aria-expanded={open}
      >
        <ChevronRight className={`h-3 w-3 shrink-0 transition-transform ${open ? 'rotate-90' : ''}`} />
        <Icon className={`h-4 w-4 shrink-0 ${isError ? 'text-destructive' : 'text-duck-teal'}`} />
        <span className={`font-medium ${isError ? 'text-destructive' : 'text-foreground'}`}>{message.toolName}</span>
        <span className="flex min-w-0 flex-1 items-center font-mono text-xs text-muted-foreground">
          <span className="min-w-0 truncate">{head}</span>
          {tail && <span className="shrink-0">{tail}</span>}
        </span>
        <span className="flex shrink-0 items-center gap-2">
          {stat && (stat.added > 0 || stat.removed > 0) && (
            <span className="tabular-nums text-xs">
              {stat.added > 0 && <span className="text-success">+{stat.added}</span>}
              {stat.added > 0 && stat.removed > 0 && ' '}
              {stat.removed > 0 && <span className="text-destructive">−{stat.removed}</span>}
            </span>
          )}
          {children.length > 0 && (
            <span className="text-xs tabular-nums text-muted-foreground">
              {children.length} step{children.length === 1 ? '' : 's'}
            </span>
          )}
          {hint && <span className="text-xs tabular-nums text-muted-foreground">{hint}</span>}
          {pending && <span className="inline-block h-2 w-2 animate-pulse rounded-full bg-warning" />}
          {/* Success is the default case and says nothing worth a colour; only a failure earns one. */}
          {isError && <span className="text-xs text-destructive">error</span>}
        </span>
      </button>

      {open && (
        <div className="ml-7 mt-1 space-y-2 text-xs">
          <div className="rounded-md bg-muted/50 p-2">
            <div className="flex items-center justify-between mb-1">
              <div className="text-xs uppercase tracking-wider text-muted-foreground">{diff ? 'Diff' : 'Input'}</div>
              {/* The diff and the key/value dump keep their own copy control; a `CodeSurface` brings two. */}
              {(diff || message.toolName !== 'Bash') && (
                <CopyButton
                  text={copyableInput(message.toolName, message.toolInput)}
                  className="!opacity-60 hover:!opacity-100"
                />
              )}
            </div>
            {message.toolName === 'Bash' ? (
              <CodeSurface code={(message.toolInput.command as string) ?? ''} lang="bash" />
            ) : diff ? (
              <DiffView lines={diff} />
            ) : (
              <pre className="font-mono whitespace-pre-wrap break-words text-foreground/80">
                {Object.entries(message.toolInput)
                  .map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
                  .join('\n')}
              </pre>
            )}
          </div>

          {children.length > 0 && (
            <div className="rounded-md border-l-2 border-duck-teal/30 pl-2">
              <div className="text-xs uppercase tracking-wider text-muted-foreground mb-1">Subagent</div>
              <SubagentTrace messages={children} />
            </div>
          )}

          {message.output !== undefined && (
            <div className="rounded-md bg-muted/50 p-2">
              <div className="mb-1 text-xs uppercase tracking-wider text-muted-foreground">
                {isError ? <span className="text-destructive">Output — error</span> : 'Output'}
              </div>
              <ToolOutput
                toolName={message.toolName}
                toolInput={message.toolInput}
                output={message.output}
                isError={isError}
                maxLines={MAX_LINES}
              />
            </div>
          )}
        </div>
      )}
    </div>
  );
};

/**
 * An Edit or Write, rendered as a diff instead of two opaque blobs. This is the most-looked-at panel in
 * the app — an agent that mostly edits code spends most of its visible output here — and it used to show
 * `old_string: <400 lines>` / `new_string: <400 lines>` as flat monospace, which meant the one question
 * you actually had (what changed?) was the one thing you had to work out by eye.
 *
 * The tint carries the meaning and the text stays at normal contrast: colouring whole lines success-green
 * makes code markedly harder to read, and the gutter marker is enough to say which side a line is on. That
 * marker is `select-none` so dragging across the diff copies the code and not a column of +/− noise.
 */
const DIFF_MAX_LINES = 40;

const DiffView = ({ lines }: { lines: DiffLine[] }) => {
  const [expanded, setExpanded] = useState(false);
  const hidden = lines.length - DIFF_MAX_LINES;
  const shown = expanded ? lines : lines.slice(0, DIFF_MAX_LINES);

  return (
    <>
      <div className="overflow-x-auto rounded font-mono">
        {/* `min-w-max` sizes this to the longest line so every row is that wide. Without it each row is
            only as wide as its own text, and the add/delete tint stops short of the right edge the moment
            you scroll — the stripe would end mid-diff and read as a change in meaning. */}
        <div className="min-w-max">
          {shown.map((line, i) => (
            <div
              key={i}
              className={`flex gap-2 px-1 ${
                line.kind === 'add' ? 'bg-success/10' : line.kind === 'del' ? 'bg-destructive/10' : ''
              }`}
            >
              <span
                className={`w-3 shrink-0 select-none text-center ${
                  line.kind === 'add'
                    ? 'text-success'
                    : line.kind === 'del'
                      ? 'text-destructive'
                      : 'text-muted-foreground'
                }`}
              >
                {line.kind === 'add' ? '+' : line.kind === 'del' ? '−' : ' '}
              </span>
              {/* Scroll, don't wrap: a wrapped continuation line has no gutter marker, so it reads as
                  context sitting inside an added block. */}
              <span
                className={`whitespace-pre ${line.kind === 'ctx' ? 'text-muted-foreground' : 'text-foreground/90'}`}
              >
                {line.text || ' '}
              </span>
            </div>
          ))}
        </div>
      </div>
      {hidden > 0 && (
        <button
          onClick={() => setExpanded(!expanded)}
          className="mt-1 cursor-pointer text-xs text-duck-teal hover:underline"
        >
          {expanded ? 'Show less' : `Show more (${hidden} more lines)`}
        </button>
      )}
    </>
  );
};

/**
 * What a subagent did, in order — its own tool calls nested one level further, its prose as plain text.
 * Deliberately not markdown-rendered: this is a trace, and it sits inside an already-nested panel.
 */
export const SubagentTrace = ({ messages }: { messages: ChatMessage[] }) => (
  <div className="space-y-1">
    {messages.map((m, i) =>
      m.role === 'tool' ? (
        <ToolActivity key={m.toolCallId || i} message={m} />
      ) : m.role === 'assistant' ? (
        <div key={m.id ?? i} className="text-muted-foreground whitespace-pre-wrap px-3 py-1">
          {m.text}
        </div>
      ) : null,
    )}
  </div>
);

type ToolOutputProps = {
  toolName: string;
  toolInput: Record<string, unknown>;
  output: string;
  isError: boolean;
  maxLines: number;
};

/**
 * Every tool's output on the same surface the chat's own code blocks use.
 *
 * It used to be green-on-black for Bash and whole-text red for a failure, both of which trade legibility
 * for decoration — the row above already says which call this was and whether it failed, so the output
 * itself only has to be readable. A `Read` is the one output we can colour honestly, because the call
 * carries the path that names the language.
 */
const ToolOutput = ({ toolName, toolInput, output, isError, maxLines }: ToolOutputProps) => {
  const isRead = toolName === 'Read' && !isError;
  const { code, startLine } = isRead ? unnumber(output) : { code: output, startLine: 1 };
  const lang = isRead ? langForPath(toolInput.file_path) : '';

  return (
    <div className={isError ? 'border-l-2 border-destructive pl-2' : undefined}>
      <CodeSurface code={code} lang={lang} startLine={startLine} maxLines={maxLines} />
    </div>
  );
};
