// OrchestratorPane — run view for a flow run (Phase 8).
//
// Subscribes to the coder user channel (via useCoderUserEvents → sessionEvents)
// for run-level frames (flow_run_started / flow_run_step_updated). Per-step
// content streams ride the existing coder per-session WS frames (delta /
// tool_call / message_complete), connected on demand when a step is expanded.
//
// Layout per D-7:
//   - Run header (flow name + band + status + stop button)
//   - Final report at top when completed
//   - Collapsed agent roster (one row per step, status dot + label)
//   - Expand-one-at-a-time detail well (step's live stream via CoderMessageList)
//   - Mobile: single column, inline expand

import { useCallback, useEffect, useRef, useState } from 'react';
import { ChevronDown, ChevronRight, MoreHorizontal, Square, Workflow, X } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api/client';
import type { FlowRunRow, FlowStepRow, OrchestratorState } from '@/api/types';
import { sessionEvents } from '@/hooks/sessionEvents';
import { chatInputsRegistry, sendToChat } from '@/lib/events';
import { CoderMessageList, type CoderTimelineWire } from "@/components/panes/CoderMessageList";
import { mergeWireToolCall } from '@/lib/coder-tools';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/lib/utils';

// ---- step status dot (same visual language as AgentStatusDot in AgentComposerBar) --

function FlowStepStatusDot({ status }: { status: FlowStepRow['status'] }) {
  if (status === 'running') {
    return (
      <span
        aria-label="running"
        className="inline-block w-3 h-3 rounded-full border-2 border-emerald-500 border-t-transparent animate-spin shrink-0"
      />
    );
  }
  const cls =
    status === 'completed'
      ? 'bg-emerald-500'
      : status === 'failed'
        ? 'bg-destructive'
        : status === 'cancelled'
          ? 'bg-muted-foreground/20'
          : 'bg-muted-foreground/40'; // pending / skipped
  return <span aria-label={status} className={cn('inline-block w-1.5 h-1.5 rounded-full shrink-0', cls)} />;
}

type RawFrame = Record<string, unknown>;

function useStepStream(sessionId: string | null, chatId: string | null): CoderTimelineWire[] {
  const [messages, setMessages] = useState<CoderTimelineWire[]>([]);
  const chatIdRef = useRef(chatId);
  chatIdRef.current = chatId;

  useEffect(() => {
    if (!sessionId || !chatId) {
      setMessages([]);
      return;
    }
    setMessages([]);

    // Initial REST fetch for any already-persisted messages.
    api.coder.listMessages(sessionId, chatId).then((rows) => setMessages(rows)).catch(() => {});

    // Live stream from the step's synthetic session.
    const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    const ws = new WebSocket(`${proto}//${window.location.host}/api/coder/ws/sessions/${sessionId}`);

    ws.onmessage = (ev) => {
      try {
        const frame = JSON.parse(ev.data as string) as RawFrame;
        const scopedChatId = chatIdRef.current;

        // Drop frames for other chats (except the snapshot which we filter below).
        if (scopedChatId && frame.chat_id && frame.chat_id !== scopedChatId && frame.type !== 'snapshot') return;

        if (frame.type === 'snapshot' && Array.isArray(frame.messages)) {
          const rows = (frame.messages as Array<Record<string, unknown>>).filter(
            (m) => !scopedChatId || m.chat_id === scopedChatId,
          );
          setMessages(rows as unknown as CoderTimelineWire[]);
        } else if (frame.type === 'message_started') {
          const role = (frame.role ?? 'assistant') as string;
          setMessages((prev) => {
            if (prev.some((m) => m.id === frame.message_id)) return prev;
            return [
              ...prev,
              { id: frame.message_id as string, role, content: '', status: 'streaming' } as CoderTimelineWire,
            ];
          });
        } else if (frame.type === 'delta') {
          setMessages((prev) =>
            prev.map((m) => {
              if (m.id !== frame.message_id || m.role === 'tool') return m;
              const msg = m as { content: string };
              return { ...m, content: msg.content + ((frame.content as string) ?? '') };
            }),
          );
        } else if (frame.type === 'reasoning_delta') {
          setMessages((prev) =>
            prev.map((m) => {
              if (m.id !== frame.message_id || m.role === 'tool') return m;
              const msg = m as { reasoning_text?: string };
              return { ...m, reasoning_text: (msg.reasoning_text ?? '') + ((frame.content as string) ?? '') };
            }),
          );
        } else if (frame.type === 'message_complete') {
          setMessages((prev) =>
            prev.map((m) =>
              m.id === frame.message_id && m.role !== 'tool'
                ? {
                    ...m,
                    status: ((frame.status as string) ?? 'complete') as 'complete' | 'failed' | 'cancelled',
                    model: (frame.model as string | null | undefined) ?? null,
                  }
                : m,
            ),
          );
        } else if (frame.type === 'tool_call' && frame.tool_call) {
          const tc = frame.tool_call as { id: string; name: string; args?: Record<string, unknown> };
          if (tc.id) {
            setMessages((prev) =>
              prev.map((m) => {
                if (m.role !== 'assistant' || m.id !== frame.message_id) return m;
                const msg = m as { tool_calls?: import('@/lib/coder-tools').CoderToolCallWire[] };
                return { ...m, tool_calls: mergeWireToolCall(msg.tool_calls, { ...tc, args: tc.args ?? {} }) };
              }),
            );
          }
        } else if (frame.type === 'tool_result') {
          const result = {
            tool_call_id: frame.tool_call_id as string,
            output: frame.output,
            truncated: (frame.truncated as boolean) ?? false,
            ...((frame.error as string | undefined) ? { error: frame.error as string } : {}),
          };
          setMessages((prev) => {
            const exists = prev.some((m) => m.id === frame.tool_message_id);
            if (exists) {
              return prev.map((m) =>
                m.role === 'tool' && m.id === frame.tool_message_id ? { ...m, tool_results: result } : m,
              );
            }
            return [
              ...prev,
              { id: frame.tool_message_id as string, role: 'tool' as const, tool_results: result },
            ];
          });
        }
      } catch {
        // bad frame — ignore
      }
    };

    return () => {
      try { ws.close(); } catch {}
    };
  }, [sessionId, chatId]);

  return messages;
}

function humanize(slug: string): string {
  return slug.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}

function StepRow({
  step,
  isExpanded,
  onToggle,
}: {
  step: FlowStepRow;
  isExpanded: boolean;
  onToggle: () => void;
}) {
  // Only connect when expanded; null inputs cause the hook to return [] immediately.
  const streamMessages = useStepStream(
    isExpanded ? step.session_id : null,
    isExpanded ? step.chat_id : null,
  );

  return (
    <div>
      <button
        type="button"
        onClick={onToggle}
        className="w-full flex items-center gap-2.5 px-3 py-2.5 text-left hover:bg-muted/30 transition-colors"
      >
        <FlowStepStatusDot status={step.status} />
        <span className="text-sm flex-1 truncate">{humanize(step.step_id)}</span>
        {step.agent && (
          <span className="text-xs text-muted-foreground shrink-0 hidden sm:block">{step.agent}</span>
        )}
        {isExpanded
          ? <ChevronDown size={12} className="shrink-0 text-muted-foreground" />
          : <ChevronRight size={12} className="shrink-0 text-muted-foreground" />}
      </button>

      {isExpanded && (
        <div className="border-t border-border/50 bg-muted/10 max-h-[55vh] flex flex-col overflow-hidden">
          {streamMessages.length === 0 ? (
            <div className="flex items-center justify-center py-6 text-sm text-muted-foreground">
              {step.status === 'pending' ? 'Waiting to start…'
                : step.status === 'completed' || step.status === 'failed' ? 'Loading output…'
                : 'Connecting…'}
            </div>
          ) : (
            <CoderMessageList messages={streamMessages} chatId={step.chat_id ?? undefined} />
          )}
        </div>
      )}
    </div>
  );
}

interface Props {
  state: OrchestratorState;
  onClose: () => void;
}

export function OrchestratorPane({ state, onClose }: Props) {
  const [run, setRun] = useState<FlowRunRow | null>(null);
  const [steps, setSteps] = useState<FlowStepRow[]>([]);
  const [expandedStepId, setExpandedStepId] = useState<string | null>(null);
  const [stopping, setStopping] = useState(false);

  // Fetch current run state on mount (handles both new runs and reopen).
  useEffect(() => {
    setRun(null);
    setSteps([]);
    setExpandedStepId(null);
    api.runs.get(state.run_id)
      .then(({ run: r, steps: s }) => {
        setRun(r);
        setSteps(s);
        // Auto-expand first running step.
        const firstRunning = s.find((step) => step.status === 'running');
        if (firstRunning) setExpandedStepId(firstRunning.step_id);
      })
      .catch(() => {});
  }, [state.run_id]);

  // Subscribe to live run-level frames from the coder user channel
  // (forwarded by useCoderUserEvents → sessionEvents).
  // Idempotent: flow_run_started only seeds the roster when empty (the API
  // fetch above is authoritative; the frame is a fallback for the race where
  // the pane opens before the GET resolves).
  useEffect(() => {
    return sessionEvents.subscribe((ev) => {
      if (ev.type === 'flow_run_started' && ev.run_id === state.run_id) {
        setSteps((prev) => {
          if (prev.length > 0) return prev;
          return ev.steps.map((s) => ({
            id: s.step_id,
            run_id: state.run_id,
            step_id: s.step_id,
            kind: s.kind,
            agent: s.agent,
            status: 'pending' as const,
            task_id: null,
            chat_id: s.chat_id,
            session_id: null,
            input: null,
            output: null,
            error: null,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
          }));
        });
      } else if (ev.type === 'flow_run_step_updated' && ev.run_id === state.run_id) {
        // Idempotent status update — no double-emit risk (useCoderUserEvents
        // owns the WS → sessionEvents bridge; we don't also emit locally).
        setSteps((prev) =>
          prev.map((s) => (s.step_id === ev.step_id ? { ...s, status: ev.status } : s)),
        );
        if (ev.run_status) {
          setRun((prev) =>
            prev ? { ...prev, status: ev.run_status!, report: ev.report ?? prev.report } : prev,
          );
        }
        // Auto-expand the step that just went running.
        if (ev.status === 'running') setExpandedStepId(ev.step_id);
      }
    });
  }, [state.run_id]);

  const toggleExpand = useCallback((stepId: string) => {
    setExpandedStepId((prev) => (prev === stepId ? null : stepId));
  }, []);

  const handleStop = useCallback(async () => {
    if (stopping) return;
    setStopping(true);
    try {
      await api.runs.cancel(state.run_id);
    } catch {
      // non-fatal
    } finally {
      setStopping(false);
    }
  }, [state.run_id, stopping]);

  const runStatus = run?.status ?? 'running';
  const isRunning = runStatus === 'running';
  const agentSteps = steps.filter((s) => s.kind === 'agent');
  const hasReport = runStatus === 'completed' && !!run?.report;

  function handleCopyReport() {
    if (!run?.report) return;
    navigator.clipboard.writeText(run.report).catch(() => toast.error('Clipboard write failed'));
  }

  function handleSaveReport() {
    if (!run?.report) return;
    const blob = new Blob([run.report], { type: 'text/markdown' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${run.flow_name}-report.md`;
    a.click();
    URL.revokeObjectURL(url);
  }

  function handleSendToChat() {
    if (!run?.report) return;
    const chats = chatInputsRegistry.list();
    const first = chats[0];
    if (!first) {
      toast.error('No open chat to send to');
      return;
    }
    sendToChat.emit({ chat_id: first.chatId, text: run.report });
  }

  return (
    <div className="flex flex-col h-full min-h-0 overflow-hidden">
      {/* Header */}
      <div className="flex items-center gap-2 border-b border-border bg-muted/20 px-3 py-2 shrink-0">
        <Workflow size={14} className="text-muted-foreground shrink-0" />
        <span className="text-sm font-medium truncate">{humanize(state.flow_name)}</span>
        <span className="text-xs text-muted-foreground shrink-0 capitalize">{state.band}</span>
        <div className="ml-auto flex items-center gap-1.5 shrink-0">
          {isRunning ? (
            <button
              type="button"
              onClick={() => void handleStop()}
              disabled={stopping}
              className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-50"
              title="Stop run"
            >
              <Square size={10} />
              Stop
            </button>
          ) : (
            <span
              className={cn(
                'text-xs px-1.5 py-0.5 rounded',
                runStatus === 'completed'
                  ? 'text-emerald-600 bg-emerald-500/10'
                  : runStatus === 'failed'
                    ? 'text-destructive bg-destructive/10'
                    : 'text-muted-foreground bg-muted/40',
              )}
            >
              {runStatus}
            </span>
          )}
          {hasReport && (
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <button
                  type="button"
                  className="inline-flex items-center justify-center p-1 rounded text-muted-foreground hover:bg-muted hover:text-foreground"
                  aria-label="Export options"
                  title="Export report"
                >
                  <MoreHorizontal size={14} />
                </button>
              </DropdownMenuTrigger>
              <DropdownMenuContent align="end">
                <DropdownMenuItem onSelect={handleCopyReport}>
                  Copy report
                </DropdownMenuItem>
                <DropdownMenuItem onSelect={handleSaveReport}>
                  Save to file
                </DropdownMenuItem>
                <DropdownMenuItem onSelect={handleSendToChat}>
                  Send to chat
                </DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          )}
          <button
            type="button"
            onClick={onClose}
            className="inline-flex items-center justify-center p-1 rounded text-muted-foreground hover:bg-muted hover:text-foreground"
            aria-label="Close pane"
            title="Close pane"
          >
            <X size={12} />
          </button>
        </div>
      </div>

      {/* Body */}
      <div className="flex-1 min-h-0 overflow-y-auto">
        {/* Final report — at the top when completed */}
        {run?.report && (
          <div className="border-b border-border p-4">
            <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2 pb-1 border-b border-border/50">
              Report
            </div>
            <div className="text-sm text-foreground whitespace-pre-wrap leading-relaxed">
              {run.report}
            </div>
          </div>
        )}

        {/* Empty state */}
        {agentSteps.length === 0 && !run?.report && (
          <div className="flex items-center justify-center h-24 text-sm text-muted-foreground">
            Starting run…
          </div>
        )}

        {/* Agent roster — collapsed by default, expand one at a time */}
        <div className="divide-y divide-border">
          {agentSteps.map((step) => (
            <StepRow
              key={step.step_id}
              step={step}
              isExpanded={expandedStepId === step.step_id}
              onToggle={() => toggleExpand(step.step_id)}
            />
          ))}
        </div>
      </div>
    </div>
  );
}
