"use client";

import { useState, useEffect, useCallback, useRef } from "react";
import { Bot, ChevronDown, Clock, Loader2, Square } from "lucide-react";
import { api } from "@multica/core/api";
import { useWSEvent, useWSReconnect } from "@multica/core/realtime";
import type { TaskMessagePayload } from "@multica/core/types/events";
import type { AgentTask } from "@multica/core/types/agent";
import { toast } from "sonner";
import { ActorAvatar } from "../../common/actor-avatar";
import { useActorName } from "@multica/core/workspace/hooks";
import {
  TranscriptButton,
  buildTimeline,
  type TimelineItem,
} from "../../common/task-transcript";
import { useT } from "../../i18n";
import { TerminateTaskConfirmDialog } from "./terminate-task-confirm-dialog";
import { AgentAvatarStack } from "../../agents/components/agent-avatar-stack";

// AgentLiveCard renders a sticky banner at the top of the issue's main
// column for every active task. Each banner shows "agent X is working",
// elapsed time, tool count, and Cancel/Transcript actions.
//
// The full timeline (live execution log) used to live inside an
// expandable area on this card. It now lives in the right panel via
// ExecutionLogSection — this card is just a header-style anchor that
// answers "is anyone working on this issue right now?" at a glance.
//
// We still maintain per-task raw message state here so the live
// TranscriptButton on the sticky banner can open the dialog with live
// items already attached (the dialog stays in sync via WS as messages
// arrive). The right-panel rows use the lazy mode of TranscriptButton
// instead — a one-shot fetch when opened. Both modes coexist.

function formatElapsed(startedAt: string): string {
  const elapsed = Date.now() - new Date(startedAt).getTime();
  const seconds = Math.floor(elapsed / 1000);
  if (seconds < 60) return `${seconds}s`;
  const minutes = Math.floor(seconds / 60);
  const secs = seconds % 60;
  return `${minutes}m ${secs}s`;
}

interface TaskState {
  task: AgentTask;
  messages: TaskMessagePayload[];
}

interface AgentLiveCardProps {
  issueId: string;
}

export function AgentLiveCard({ issueId }: AgentLiveCardProps) {
  const { t } = useT("issues");
  const { getActorName } = useActorName();
  const [taskStates, setTaskStates] = useState<Map<string, TaskState>>(new Map());
  // Cancel confirmation is hoisted here (not per-card) so a single dialog
  // serves both the inline single banner and the multi-agent popover. A
  // confirm dialog living inside the popover would be torn down the moment
  // the popover closed on the dialog's outside-press; lifting it keeps the
  // confirm flow alive regardless of where Stop was clicked.
  const [cancelTarget, setCancelTarget] = useState<AgentTask | null>(null);
  const [cancellingIds, setCancellingIds] = useState<ReadonlySet<string>>(
    () => new Set(),
  );
  // Multi-agent accordion: collapsed by default to a one-line summary,
  // expands inline within the same full-width content column as the single
  // banner so all states read at one consistent width. A popover would
  // force the list into a narrow floating card ≠ the full-width banner.
  const [expanded, setExpanded] = useState(false);
  const seenSeqs = useRef(new Set<string>());
  const hydratedTaskIds = useRef(new Set<string>());
  const mountedRef = useRef(true);
  // Monotonic counter — each reconcile() call captures its issued seq and
  // only applies its response if it's still the latest issued. This stops
  // a slow getActiveTasksForIssue response from clobbering newer truth
  // (e.g. a stale "task is active" payload re-adding a banner that a
  // newer "tasks: []" response just cleared).
  const reconcileSeq = useRef(0);
  useEffect(() => {
    mountedRef.current = true;
    return () => { mountedRef.current = false; };
  }, []);

  // Reconcile local state to server truth. Replaces taskStates with the
  // server's active set: tasks no longer active are dropped (this is what
  // self-heals a stale "is working" banner when a task:completed/failed/
  // cancelled event was lost during a WS reconnect window), and tasks
  // still active keep their accumulated TimelineItems so the live
  // TranscriptButton doesn't lose history. New tasks get a one-shot
  // listTaskMessages hydration to backfill any messages that landed
  // before the WS subscription saw them.
  const reconcile = useCallback(() => {
    const mySeq = ++reconcileSeq.current;
    api.getActiveTasksForIssue(issueId).then(({ tasks }) => {
      if (!mountedRef.current) return;
      // A newer reconcile was issued after this one — drop this response
      // unconditionally and let the latest request win, regardless of
      // resolution order. Without this guard, a slow A then a fast B can
      // resolve in B-then-A order and A re-adds tasks B already cleared.
      if (mySeq !== reconcileSeq.current) return;
      const activeIds = new Set(tasks.map((t) => t.id));

      setTaskStates((prev) => {
        const next = new Map<string, TaskState>();
        for (const task of tasks) {
          const existing = prev.get(task.id);
          next.set(task.id, existing
            ? { task, messages: existing.messages }
            : { task, messages: [] });
        }
        return next;
      });

      // Drop bookkeeping for tasks that vanished, so a future re-dispatch
      // of the same id (very rare, but possible) re-hydrates cleanly.
      for (const key of Array.from(seenSeqs.current)) {
        const taskId = key.slice(0, key.indexOf(":"));
        if (!activeIds.has(taskId)) seenSeqs.current.delete(key);
      }
      for (const id of Array.from(hydratedTaskIds.current)) {
        if (!activeIds.has(id)) hydratedTaskIds.current.delete(id);
      }

      // Hydrate messages for tasks we haven't fetched yet. Per-task guard
      // prevents duplicate fetches when reconcile fires repeatedly (mount
      // + reconnect + queued/dispatch can stack within a single tick).
      for (const task of tasks) {
        if (hydratedTaskIds.current.has(task.id)) continue;
        hydratedTaskIds.current.add(task.id);
        api.listTaskMessages(task.id).then((msgs) => {
          if (!mountedRef.current) return;
          for (const m of msgs) seenSeqs.current.add(`${m.task_id}:${m.seq}`);
          setTaskStates((prev) => {
            const next = new Map(prev);
            const existing = next.get(task.id);
            if (!existing) return prev;
            const loadedSeqs = new Set(msgs.map((i) => i.seq));
            const wsOnly = existing.messages.filter((i) => !loadedSeqs.has(i.seq));
            const messages = [...msgs, ...wsOnly].sort((a, b) => a.seq - b.seq);
            next.set(task.id, { task: existing.task, messages });
            return next;
          });
        }).catch((e) => {
          hydratedTaskIds.current.delete(task.id);
          console.error(e);
        });
      }
    }).catch(console.error);
  }, [issueId]);

  // Initial fetch on mount / issueId change.
  useEffect(() => {
    reconcile();
  }, [reconcile]);

  // WS reconnect — anything that happened while we were offline (most
  // notably task:completed / task:failed / task:cancelled) won't replay,
  // so re-pull the truth and let reconcile drop any stale banners.
  useWSReconnect(reconcile);

  // Real-time messages — route by task_id and dedupe by seq.
  useWSEvent(
    "task:message",
    useCallback((payload: unknown) => {
      const msg = payload as TaskMessagePayload;
      if (msg.issue_id !== issueId) return;
      const key = `${msg.task_id}:${msg.seq}`;
      if (seenSeqs.current.has(key)) return;
      seenSeqs.current.add(key);

      setTaskStates((prev) => {
        const next = new Map(prev);
        const existing = next.get(msg.task_id);
        if (existing) {
          const messages = [...existing.messages, msg].sort((a, b) => a.seq - b.seq);
          next.set(msg.task_id, { ...existing, messages });
        }
        return next;
      });
    }, [issueId]),
  );

  // Task end — optimistically drop the banner for snappy UX, then
  // reconcile to also clean up sibling tasks whose own end events may
  // have been missed (e.g. a sequence of tasks all ending during a WS
  // reconnect window will only replay this one event when we resubscribe).
  const handleTaskEnd = useCallback((payload: unknown) => {
    const p = payload as { task_id: string; issue_id: string };
    if (p.issue_id !== issueId) return;
    setTaskStates((prev) => {
      if (!prev.has(p.task_id)) return prev;
      const next = new Map(prev);
      next.delete(p.task_id);
      return next;
    });
    reconcile();
  }, [issueId, reconcile]);

  useWSEvent("task:completed", handleTaskEnd);
  useWSEvent("task:failed", handleTaskEnd);
  useWSEvent("task:cancelled", handleTaskEnd);

  // Newly active tasks — both queued and dispatched land here. Subscribing
  // to both events matters because retry creates a queued child without
  // emitting task:dispatch (only the daemon's claim does), so listening
  // to dispatch alone leaves the banner stale during the queued window.
  // reconcile is idempotent (per-task hydration guard) and also drops
  // stale tasks, so it's safe to fire once per event.
  const handleTaskActive = useCallback((payload: unknown) => {
    const p = payload as { issue_id?: string };
    if (p.issue_id && p.issue_id !== issueId) return;
    reconcile();
  }, [issueId, reconcile]);

  useWSEvent("task:queued", handleTaskActive);
  useWSEvent("task:dispatch", handleTaskActive);
  // The daemon publishes these two transitions while a task moves through
  // the dispatch → waiting_local_directory → running sequence on a busy
  // local_directory path. Without subscribing here, the sticky banner
  // would stay stuck on the optimistic dispatch state instead of flipping
  // to "waiting" then resuming "is working".
  useWSEvent("task:waiting_local_directory", handleTaskActive);
  useWSEvent("task:running", handleTaskActive);

  // Fire the actual cancel once the user confirms. The banner is dropped
  // optimistically by handleTaskEnd / reconcile when the task:cancelled
  // event lands, so `cancellingIds` only needs to gate the button between
  // confirm and that event.
  const handleConfirmCancel = useCallback(async () => {
    const task = cancelTarget;
    if (!task) return;
    setCancelTarget(null);
    setCancellingIds((prev) => new Set(prev).add(task.id));
    try {
      await api.cancelTask(issueId, task.id);
    } catch (e) {
      toast.error(
        e instanceof Error ? e.message : t(($) => $.agent_live.cancel_failed),
      );
      setCancellingIds((prev) => {
        const next = new Set(prev);
        next.delete(task.id);
        return next;
      });
    }
  }, [cancelTarget, issueId, t]);

  if (taskStates.size === 0) return null;

  // Order: running → dispatched → waiting → queued. The most-active task
  // takes the sticky slot; the parked / queued tasks sit below so the
  // "is working" banner isn't pushed off by a freshly-enqueued or
  // path-parked sibling. ListActiveTasksByIssue's server-side ORDER BY is
  // created_at DESC, which doesn't reflect lifecycle priority, so we
  // re-sort on the client.
  const statusRank: Record<AgentTask["status"], number> = {
    running: 0,
    dispatched: 1,
    waiting_local_directory: 2,
    queued: 3,
    completed: 4,
    failed: 4,
    cancelled: 4,
  };
  const entries = Array.from(taskStates.values()).sort(
    (a, b) => statusRank[a.task.status] - statusRank[b.task.status],
  );
  const firstEntry = entries[0];
  if (!firstEntry) return null;

  const resolveName = (agentId: string | null) =>
    agentId ? getActorName("agent", agentId) : t(($) => $.agent_live.fallback_name);

  // One active task → it fills the card as a single row. Multiple → the
  // card shows a collapsed summary header that expands the rows inline
  // (one bordered container with divided rows, never N detached boxes).
  // Active count tops out at ~4-5 in practice, so the expanded list stays
  // short.
  const isMulti = entries.length > 1;
  const agentIds = [
    ...new Set(
      entries.map((e) => e.task.agent_id).filter((id): id is string => !!id),
    ),
  ];
  const anyRunning = entries.some((e) => e.task.status === "running");

  return (
    // Sticky bar at the top of the main content, above the editable title —
    // answers "is anyone working on this issue right now?" while the comment
    // thread scrolls under it. One bordered container in every state: the
    // single row, the collapsed summary, and the expanded list all share it,
    // so the bar reads at one consistent width.
    <div className="mt-4 sticky top-4 z-10 rounded-lg bg-background/80 supports-[backdrop-filter]:bg-background/55 backdrop-blur-md">
      <div className="overflow-hidden rounded-lg border border-info/20 bg-info/5">
        {isMulti ? (
          <>
            <button
              type="button"
              onClick={() => setExpanded((v) => !v)}
              aria-expanded={expanded}
              className="flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-info/10 aria-expanded:bg-info/10 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
            >
              <AgentAvatarStack agentIds={agentIds} size={20} max={4} />
              <span className="flex min-w-0 items-center gap-1.5 text-xs">
                {anyRunning ? (
                  <Loader2 className="h-3 w-3 animate-spin text-info shrink-0" />
                ) : (
                  <Clock className="h-3 w-3 text-muted-foreground shrink-0" />
                )}
                <span className="truncate font-medium text-foreground">
                  {t(($) => $.agent_activity.hover_header, { count: agentIds.length })}
                </span>
              </span>
              <ChevronDown
                className={`ml-auto h-3.5 w-3.5 text-muted-foreground shrink-0 transition-transform ${expanded ? "" : "-rotate-90"}`}
              />
            </button>
            {expanded && (
              <div className="divide-y divide-info/15 border-t border-info/15">
                {entries.map(({ task, messages }) => (
                  <AgentLiveRow
                    key={task.id}
                    task={task}
                    items={buildTimeline(messages)}
                    agentName={resolveName(task.agent_id)}
                    onRequestCancel={() => setCancelTarget(task)}
                    cancelling={cancellingIds.has(task.id)}
                  />
                ))}
              </div>
            )}
          </>
        ) : (
          <AgentLiveRow
            task={firstEntry.task}
            items={buildTimeline(firstEntry.messages)}
            agentName={resolveName(firstEntry.task.agent_id)}
            onRequestCancel={() => setCancelTarget(firstEntry.task)}
            cancelling={cancellingIds.has(firstEntry.task.id)}
          />
        )}
      </div>
      <TerminateTaskConfirmDialog
        open={cancelTarget !== null}
        onOpenChange={(open) => {
          if (!open) setCancelTarget(null);
        }}
        onConfirm={() => void handleConfirmCancel()}
        // Matches the old per-card `!isParked`: a task that's queued or
        // parked on a directory lock isn't interrupting live work, so the
        // "this stops running work" note is suppressed for those only.
        showRunningNote={
          cancelTarget !== null &&
          cancelTarget.status !== "queued" &&
          cancelTarget.status !== "waiting_local_directory"
        }
      />
    </div>
  );
}

// ─── AgentLiveRow (one active task: avatar + status + Logs/Stop) ───────────

interface AgentLiveRowProps {
  task: AgentTask;
  items: TimelineItem[];
  agentName: string;
  // Cancel is owned by the parent AgentLiveCard (a single confirm dialog
  // serves both the lone row and the multi-agent list). The row only
  // requests it and reflects the in-flight state.
  onRequestCancel: () => void;
  cancelling: boolean;
}

function AgentLiveRow({ task, items, agentName, onRequestCancel, cancelling }: AgentLiveRowProps) {
  const { t } = useT("issues");
  const [elapsed, setElapsed] = useState("");

  const isQueued = task.status === "queued";
  // `waiting_local_directory` is the daemon-parked stage of an otherwise-
  // active task: it's been dispatched (no longer pure-queued) but hasn't
  // entered the running phase yet because another task on this daemon
  // holds the same local_directory lock.
  const isWaitingLocalDirectory = task.status === "waiting_local_directory";
  // Parked vs running is signalled by the icon (Clock vs spinning Loader2)
  // and the label below — the container chrome is shared, so there's no
  // per-row background variant.
  const isParked = isQueued || isWaitingLocalDirectory;

  // Elapsed time — ticks every second so users see the agent is alive.
  // For queued tasks neither started_at nor dispatched_at is set yet, so
  // anchor on created_at to show the "queued for Ns" wait window.
  useEffect(() => {
    const startRef = task.started_at ?? task.dispatched_at ?? task.created_at;
    if (!startRef) return;
    setElapsed(formatElapsed(startRef));
    const interval = setInterval(() => setElapsed(formatElapsed(startRef)), 1000);
    return () => clearInterval(interval);
  }, [task.started_at, task.dispatched_at, task.created_at]);

  const toolCount = items.filter((i) => i.type === "tool_use").length;

  return (
    <div className="flex items-center gap-2 px-3 py-2 text-muted-foreground">
      {task.agent_id ? (
        <ActorAvatar actorType="agent" actorId={task.agent_id} size={20} enableHoverCard showStatusDot />
      ) : (
        <div className="flex items-center justify-center h-5 w-5 rounded-full shrink-0 bg-info/10 text-info">
          <Bot className="h-3 w-3" />
        </div>
      )}
      <div className="flex items-center gap-1.5 text-xs min-w-0">
        {isParked ? (
          <Clock className="h-3 w-3 text-muted-foreground shrink-0" />
        ) : (
          <Loader2 className="h-3 w-3 animate-spin text-info shrink-0" />
        )}
        <span className="font-medium text-foreground truncate">
          {isWaitingLocalDirectory
            ? t(($) => $.agent_live.is_waiting_local_directory, { name: agentName })
            : isQueued
              ? t(($) => $.agent_live.is_queued, { name: agentName })
              : t(($) => $.agent_live.is_working, { name: agentName })}
        </span>
        <span className="text-muted-foreground tabular-nums shrink-0">
          {isParked
            ? t(($) => $.agent_live.queued_elapsed_prefix, { elapsed })
            : elapsed}
        </span>
        {!isParked && toolCount > 0 && (
          <span className="text-muted-foreground shrink-0">{t(($) => $.agent_live.tool_count, { count: toolCount })}</span>
        )}
      </div>
      <div className="ml-auto flex items-center gap-1 shrink-0">
        {!isParked && (
          <TranscriptButton
            task={task}
            agentName={agentName}
            items={items}
            isLive
            title={t(($) => $.agent_live.transcript_button)}
          />
        )}
        <button
          type="button"
          onClick={onRequestCancel}
          disabled={cancelling}
          className="flex items-center gap-1 rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors disabled:opacity-50"
          title={t(($) => $.agent_live.stop_tooltip)}
        >
          {cancelling ? <Loader2 className="h-3 w-3 animate-spin" /> : <Square className="h-3 w-3" />}
          <span>{t(($) => $.agent_live.stop_button)}</span>
        </button>
      </div>
    </div>
  );
}
