import { useState } from 'react';
import type { ReactNode } from 'react';
import { Check, ChevronRight, Loader2, X } from 'lucide-react';
import type { ToolCall, ToolResult } from '@/api/types';
import { sessionEvents } from '@/hooks/sessionEvents';

// v1.8.2: cap on the inline arg-summary length. Expanded view shows full
// args + full result, so this is purely a single-line render budget.
const ARG_SUMMARY_MAX = 60;

export interface ToolRun {
  call: ToolCall;
  // null while the call is in flight or the matching tool result hasn't
  // arrived yet on the WS stream.
  result: ToolResult | null;
}

function truncate(s: string, n: number): string {
  return s.length > n ? s.slice(0, n - 1) + '…' : s;
}

// Per-tool argument summary mapping from the v1.8.2 spec. Goal is a single
// scannable line that surfaces the *what* (path / pattern) without
// overwhelming the chat with full JSON.
export function formatToolArgs(name: string, args: Record<string, unknown>): string {
  if (name === 'view_file') {
    const path = String(args.path ?? '');
    const start = args.start_line;
    const end = args.end_line;
    if (typeof start === 'number' && typeof end === 'number') {
      return truncate(`${path}:${start}-${end}`, ARG_SUMMARY_MAX);
    }
    if (typeof start === 'number') {
      return truncate(`${path}:${start}`, ARG_SUMMARY_MAX);
    }
    return truncate(path, ARG_SUMMARY_MAX);
  }
  if (name === 'list_dir') {
    return truncate(String(args.path ?? '.'), ARG_SUMMARY_MAX);
  }
  if (name === 'grep') {
    const pattern = String(args.pattern ?? '');
    const path = args.path ? ` ${String(args.path)}` : '';
    return truncate(`"${pattern}"${path}`, ARG_SUMMARY_MAX);
  }
  if (name === 'find_files') {
    return truncate(String(args.pattern ?? ''), ARG_SUMMARY_MAX);
  }
  if (name === 'git_status') {
    return '';
  }
  if (name === 'skill_use') {
    // Schema (apps/server/src/services/tools.ts SkillUseInput) uses `name`;
    // fall back to `skill_name` defensively in case a model emits that key.
    return truncate(
      String(args.name ?? (args as { skill_name?: unknown }).skill_name ?? '<unknown>'),
      ARG_SUMMARY_MAX,
    );
  }
  // v1.12 Track B.2: codecontext tool pills. Format is "most-identifying-arg",
  // matching view_file/grep precedent — surface the path/symbol/query that
  // makes the call meaningful at a glance.
  if (name === 'get_codebase_overview') {
    return '';
  }
  if (name === 'get_file_analysis') {
    return truncate(String(args.file_path ?? ''), ARG_SUMMARY_MAX);
  }
  if (name === 'get_symbol_info') {
    return truncate(String(args.symbol_name ?? ''), ARG_SUMMARY_MAX);
  }
  if (name === 'search_symbols') {
    return truncate(`"${String(args.query ?? '')}"`, ARG_SUMMARY_MAX);
  }
  if (name === 'get_dependencies') {
    return truncate(String(args.file_path ?? '(project-wide)'), ARG_SUMMARY_MAX);
  }
  if (name === 'watch_changes') {
    return args.enable ? 'enable' : 'disable';
  }
  if (name === 'get_semantic_neighborhoods') {
    return truncate(String(args.file_path ?? '(project-wide)'), ARG_SUMMARY_MAX);
  }
  if (name === 'get_framework_analysis') {
    return truncate(String(args.framework ?? '(auto-detect)'), ARG_SUMMARY_MAX);
  }
  // Unknown tool — surface first arg value or the literal {} so the user can
  // see something happened. Forward-compatible with future tools.
  const keys = Object.keys(args);
  if (keys.length === 0) return '{}';
  const first = keys[0]!;
  return truncate(`${first}: ${String(args[first])}`, ARG_SUMMARY_MAX);
}

export function runStatus(run: ToolRun): 'pending' | 'success' | 'error' {
  if (run.result === null) return 'pending';
  if (run.result.error) return 'error';
  return 'success';
}

// Path-shaped paths in tool output text get a click handler so users can
// jump to the file. Same heuristic as MessageBubble.linkifyPaths.
const PATH_REGEX = /([a-zA-Z0-9._/-]+\.[a-zA-Z0-9]+)/g;
function linkifyOutput(text: string): ReactNode[] {
  const out: ReactNode[] = [];
  let lastIdx = 0;
  let idx = 0;
  for (const match of text.matchAll(PATH_REGEX)) {
    const matchedText = match[0];
    const start = match.index ?? 0;
    if (!matchedText.includes('/')) continue;
    if (start > lastIdx) out.push(text.slice(lastIdx, start));
    out.push(
      <button
        key={idx}
        type="button"
        onClick={() =>
          sessionEvents.emit({ type: 'open_file_in_browser', path: matchedText })
        }
        className="text-primary underline cursor-pointer hover:text-primary/80"
      >
        {matchedText}
      </button>
    );
    lastIdx = start + matchedText.length;
    idx += 1;
  }
  if (lastIdx < text.length) out.push(text.slice(lastIdx));
  return out.length > 0 ? out : [text];
}

interface Props {
  run: ToolRun;
  // When rendered inside a ToolCallGroup the line is already nested under a
  // shared header, so the leading arrow is dropped to avoid double indent.
  insideGroup?: boolean;
}

export function ToolCallLine({ run, insideGroup }: Props) {
  const [open, setOpen] = useState(false);
  const status = runStatus(run);
  const args = run.call.args ?? {};
  const summary = formatToolArgs(run.call.name, args);

  return (
    <div className="text-xs">
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className="flex items-center gap-1.5 w-full text-left hover:bg-muted/40 rounded px-1 py-0.5 -mx-1"
      >
        {!insideGroup && (
          <span className="text-muted-foreground/60 select-none shrink-0">↳</span>
        )}
        <ChevronRight
          className={`size-3 text-muted-foreground/60 shrink-0 transition-transform ${open ? 'rotate-90' : ''}`}
        />
        <span className="font-mono text-foreground/90 shrink-0">{run.call.name}</span>
        {summary && (
          <span className="font-mono text-muted-foreground truncate min-w-0 flex-1">
            {summary}
          </span>
        )}
        {!summary && <span className="flex-1" />}
        <span className="shrink-0 ml-1">
          {status === 'pending' && (
            <Loader2 className="size-3 text-muted-foreground animate-spin" aria-label="running" />
          )}
          {status === 'success' && (
            <Check className="size-3 text-emerald-500" aria-label="success" />
          )}
          {status === 'error' && (
            <X className="size-3 text-destructive" aria-label="error" />
          )}
        </span>
      </button>
      {open && (
        <div className="ml-5 mt-1 mb-1 space-y-1">
          <pre className="text-[10px] text-muted-foreground font-mono whitespace-pre-wrap break-all bg-muted/30 rounded px-2 py-1">
            {JSON.stringify(args, null, 2)}
          </pre>
          {run.result && (
            <pre className="text-[11px] font-mono whitespace-pre-wrap bg-muted/30 rounded px-2 py-1 max-h-72 overflow-y-auto">
              {run.result.error ? (
                <span className="text-destructive">{run.result.error}</span>
              ) : (
                linkifyOutput(
                  typeof run.result.output === 'string'
                    ? run.result.output
                    : JSON.stringify(run.result.output, null, 2)
                )
              )}
              {run.result.truncated && (
                <div className="text-muted-foreground/60 mt-1">— output truncated —</div>
              )}
            </pre>
          )}
        </div>
      )}
    </div>
  );
}
