'use client';

import { memo, type ReactNode, useEffect, useRef, useState } from 'react';
import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react';

import { cn } from '@djangocfg/ui-core/lib';

import type { ChatToolCall } from '../types';
import { ToolPayloadValue, type ToolPayloadKind } from './ToolPayloadValue';

// Re-exported for back-compat: `ToolPayloadKind` is now OWNED by
// ToolPayloadValue (breaking the old ToolCalls↔ToolPayloadValue import cycle).
export type { ToolPayloadKind };

export interface ToolCallsProps {
  calls: ChatToolCall[];
  /** Open every panel up-front. Default: false (panels are closed). */
  defaultExpanded?: boolean;
  /** Auto-open while a tool is running, then auto-close on completion.
   *  User toggles after that are remembered. Default: true. */
  expandWhileStreaming?: boolean;
  /** Override how the tool input payload is rendered. Receives the raw value. */
  renderInput?: (input: unknown, call: ChatToolCall) => ReactNode;
  /** Override how the tool output payload is rendered. */
  renderOutput?: (output: unknown, call: ChatToolCall) => ReactNode;
  /** Override how the live `streamingText` is rendered. */
  renderStreaming?: (text: string, call: ChatToolCall) => ReactNode;
  /** Single override for all three; specific renderers above take precedence. */
  renderPayload?: (value: unknown, kind: ToolPayloadKind, call: ChatToolCall) => ReactNode;
  /**
   * Rendered once **after** all tool-call panels — always visible, outside
   * the collapsible panels. Use for rich UI derived from tool outputs (e.g.
   * vehicle cards, map pins, tax breakdowns). Receives the full calls array
   * so the renderer can aggregate across multiple tool calls.
   */
  renderAfterCalls?: (calls: ChatToolCall[]) => ReactNode;
  /**
   * Custom renderer for each individual tool-call panel. When provided,
   * replaces the default collapsible `<ToolCallItem>`. Return `null` to
   * suppress a specific call while still letting others render.
   */
  renderToolCall?: (call: ChatToolCall) => ReactNode;
  /**
   * When `true`, the collapsible tool-call panels are not rendered at all.
   * `renderAfterCalls` still runs — use together to show only rich UI with
   * no raw accordion panels visible.
   */
  hideToolCalls?: boolean;
  className?: string;
}

export function ToolCalls({
  calls,
  defaultExpanded = false,
  expandWhileStreaming = true,
  renderInput,
  renderOutput,
  renderStreaming,
  renderPayload,
  renderAfterCalls,
  renderToolCall,
  hideToolCalls = false,
  className,
}: ToolCallsProps) {
  if (!calls?.length) return null;
  return (
    <div className={cn('mt-2 space-y-1.5', className)}>
      {!hideToolCalls && calls.map((call) =>
        renderToolCall
          ? <div key={call.id}>{renderToolCall(call)}</div>
          : (
              <ToolCallItem
                key={call.id}
                call={call}
                defaultExpanded={defaultExpanded}
                expandWhileStreaming={expandWhileStreaming}
                renderInput={renderInput}
                renderOutput={renderOutput}
                renderStreaming={renderStreaming}
                renderPayload={renderPayload}
              />
            ),
      )}
      {renderAfterCalls ? renderAfterCalls(calls) : null}
    </div>
  );
}

interface ItemProps {
  call: ChatToolCall;
  defaultExpanded: boolean;
  expandWhileStreaming: boolean;
  renderInput?: ToolCallsProps['renderInput'];
  renderOutput?: ToolCallsProps['renderOutput'];
  renderStreaming?: ToolCallsProps['renderStreaming'];
  renderPayload?: ToolCallsProps['renderPayload'];
}

const ToolCallItem = memo(function ToolCallItem({
  call,
  defaultExpanded,
  expandWhileStreaming,
  renderInput,
  renderOutput,
  renderStreaming,
  renderPayload,
}: ItemProps) {
  const isRunning = call.status === 'running';
  const initialOpen = defaultExpanded || (expandWhileStreaming && isRunning);
  const [open, setOpen] = useState(initialOpen);
  // Remember manual interaction so completion doesn't override it.
  const userToggledRef = useRef(false);
  const wasRunningRef = useRef(isRunning);

  // Auto-collapse on running → completed transition, unless user has interacted.
  useEffect(() => {
    if (wasRunningRef.current && !isRunning) {
      if (!userToggledRef.current && !defaultExpanded) {
        setOpen(false);
      }
    }
    wasRunningRef.current = isRunning;
  }, [isRunning, defaultExpanded]);

  const handleToggle = () => {
    userToggledRef.current = true;
    setOpen((v) => !v);
  };

  const Icon = open ? ChevronDown : ChevronRight;
  // Status maps to a small dot color (success/error/cancelled/running). The
  // word itself is shown only for non-success states to keep the row quiet
  // — a green dot already reads as "ok".
  const statusDot =
    call.status === 'success'
      ? 'bg-success'
      : call.status === 'error'
        ? 'bg-destructive'
        : call.status === 'cancelled'
          ? 'bg-muted-foreground'
          : 'bg-warning';
  const statusText =
    call.status === 'error'
      ? 'text-destructive'
      : call.status === 'cancelled'
        ? 'text-muted-foreground'
        : 'text-muted-foreground';

  const renderValue = (value: unknown, kind: ToolPayloadKind): ReactNode => {
    if (kind === 'input' && renderInput) return renderInput(value, call);
    if (kind === 'output' && renderOutput) return renderOutput(value, call);
    if (kind === 'streaming' && renderStreaming)
      return renderStreaming(typeof value === 'string' ? value : String(value), call);
    if (renderPayload) return renderPayload(value, kind, call);
    return <DefaultPayload value={value} kind={kind} />;
  };

  // A non-success terminal status still shows its word (errors must read);
  // success/running stay quiet (dot only).
  const showStatusWord = call.status === 'error' || call.status === 'cancelled';

  return (
    <div className="overflow-hidden rounded-xl border border-border/60 bg-muted/20">
      <button
        type="button"
        onClick={handleToggle}
        aria-expanded={open}
        className="flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-xs text-muted-foreground transition-colors hover:bg-muted/40"
      >
        {isRunning ? (
          <Loader2 aria-hidden className="size-3 shrink-0 animate-spin text-warning" />
        ) : (
          <span className={cn('size-1.5 shrink-0 rounded-full', statusDot)} />
        )}
        <span className="truncate font-mono text-foreground/90">{call.name}</span>
        {showStatusWord ? (
          <span className={cn('shrink-0', statusText)}>{call.status}</span>
        ) : null}
        <Icon aria-hidden className="ml-auto size-3 shrink-0 opacity-60" />
      </button>
      {open ? (
        <div className="space-y-1 border-t border-border/60 px-2.5 py-1.5 text-[11px]">
          {call.input != null ? renderValue(call.input, 'input') : null}
          {call.streamingText != null
            ? renderValue(call.streamingText, 'streaming')
            : call.output !== undefined
              ? renderValue(call.output, 'output')
              : null}
        </div>
      ) : null}
    </div>
  );
}, (prev, next) => {
  // Re-render only when the call's observable surface actually changed.
  // Render-prop callbacks are accepted as referentially stable by callers —
  // they live in toolCallsProps which itself rarely changes.
  const a = prev.call;
  const b = next.call;
  return (
    a.id === b.id &&
    a.status === b.status &&
    a.output === b.output &&
    a.streamingText === b.streamingText &&
    prev.defaultExpanded === next.defaultExpanded &&
    prev.expandWhileStreaming === next.expandWhileStreaming &&
    prev.renderInput === next.renderInput &&
    prev.renderOutput === next.renderOutput &&
    prev.renderStreaming === next.renderStreaming &&
    prev.renderPayload === next.renderPayload
  );
});

// The default payload renderer delegates to the shared <ToolPayloadValue>
// (object → JsonTree, string/streaming → <pre>) so a host that owns its own
// tool-call card via `renderToolCall` can render the exact same payload with
// no duplicated routing. See ToolPayloadValue.tsx.
function DefaultPayload({ value, kind }: { value: unknown; kind: ToolPayloadKind }) {
  return <ToolPayloadValue value={value} kind={kind} />;
}
