/**
 * Read-only project work-state panel: one row per agent, showing live/offline
 * and idle/busy state from the backend read-model, plus the current input queue.
 */

import { Component, type ReactNode, useEffect, useRef, useState } from "react";

import type {
  AgentTicketState,
  AgentWorkState,
  BackgroundCompletion,
  ConversationPreviewStatus,
  ConversationWorkSummary,
  InboxItem,
  LeafCell,
} from "@/domain";
import type { BackgroundTaskAttachment } from "@/ports";
import { useGateways } from "@/app/di";
import { leaves } from "@/features/layout/layout";
import { useLayout } from "@/features/layout/useLayout";
import type { LayoutViewModel } from "@/features/layout/useLayout";
import { Button, Panel, Spinner, cn } from "@/shared";
import { useProjectWorkState } from "./useProjectWorkState";

export interface ProjectWorkStatePanelProps {
  projectId: string;
  /** Opens the read-only transcript viewer for a conversation pair (LS7). */
  onOpenConversation?: (conversationId: string) => void;
}

interface WorkStatePanelErrorBoundaryProps {
  children: ReactNode;
}

interface WorkStatePanelErrorBoundaryState {
  error: Error | null;
  retryKey: number;
}

export class WorkStatePanelErrorBoundary extends Component<
  WorkStatePanelErrorBoundaryProps,
  WorkStatePanelErrorBoundaryState
> {
  state: WorkStatePanelErrorBoundaryState = { error: null, retryKey: 0 };

  static getDerivedStateFromError(error: Error): Partial<WorkStatePanelErrorBoundaryState> {
    return { error };
  }

  private retry = () => {
    this.setState(({ retryKey }) => ({ error: null, retryKey: retryKey + 1 }));
  };

  render() {
    if (this.state.error) {
      return (
        <Panel title="Work">
          <div
            role="alert"
            className="rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger"
          >
            <p className="font-medium">Work panel failed to render.</p>
            <p className="mt-1 text-xs text-danger/80">
              {this.state.error.message}
            </p>
            <Button
              size="sm"
              variant="ghost"
              className="mt-2"
              onClick={this.retry}
            >
              Retry
            </Button>
          </div>
        </Panel>
      );
    }

    return <div key={this.state.retryKey}>{this.props.children}</div>;
  }
}

function shortTicket(ticket: string): string {
  return ticket.length <= 8 ? ticket : ticket.slice(0, 8);
}

function formatStatus(status: AgentTicketState["status"]): string {
  return status === "inProgress" ? "In progress" : "Queued";
}

function formatTaskStatus(status: BackgroundCompletion["status"]): string {
  switch (status) {
    case "running":
      return "Running";
    case "completed":
      return "Completed";
    case "failed":
      return "Failed";
    case "cancelled":
      return "Cancelled";
    case "delivered":
      return "Delivered";
    case "pending":
      return "Pending";
  }
}

/** Terminal (finished) task states that carry a result summary (ticket #5). */
function isTerminalTaskStatus(status: BackgroundCompletion["status"]): boolean {
  return status === "completed" || status === "failed" || status === "cancelled";
}

function taskStatusClass(status: BackgroundCompletion["status"]): string {
  if (status === "running" || status === "pending") return "bg-warning/15 text-warning";
  if (status === "completed" || status === "delivered") return "bg-success/10 text-success";
  if (status === "failed" || status === "cancelled") return "bg-danger/10 text-danger";
  return "bg-raised text-muted";
}

function requesterLabel(ticket: AgentTicketState): string {
  if (ticket.source.kind === "human") return "Human";
  const id = shortTicket(ticket.source.agentId);
  return ticket.requesterLabel.trim()
    ? `${ticket.requesterLabel} (${id})`
    : `Agent ${id}`;
}

function summaryLabel(status: ConversationPreviewStatus): string {
  switch (status) {
    case "ready":
      return "Summary";
    case "missing":
      return "No summary";
    case "partial":
      return "Partial";
    case "unavailable":
      return "Unavailable";
  }
}

function summaryText(summary: ConversationWorkSummary): string | null {
  if (summary.objectivePreview) return `Goal: ${summary.objectivePreview}`;
  if (summary.summaryPreview) return summary.summaryPreview;
  const lastTurn = (summary.recentTurns ?? []).at(-1);
  return lastTurn ? `Last: ${lastTurn.textPreview}` : null;
}

function goToCell(nodeId: string): void {
  if (typeof document === "undefined") return;
  const el = document.querySelector<HTMLElement>(`[data-node-id="${nodeId}"]`);
  if (!el) return;
  el.scrollIntoView?.({ block: "nearest", inline: "nearest" });
  const previous = el.style.outline;
  el.style.outline = "2px solid var(--color-primary, #5b9bd5)";
  window.setTimeout(() => {
    el.style.outline = previous;
  }, 1200);
}

async function copyToClipboard(text: string): Promise<void> {
  if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
    await navigator.clipboard.writeText(text);
    return;
  }
  throw new Error("Clipboard unavailable");
}

function ConversationSummaryLine({
  summary,
  onOpenConversation,
}: {
  summary: ConversationWorkSummary;
  onOpenConversation?: (conversationId: string) => void;
}) {
  const text = summaryText(summary);
  const [expanded, setExpanded] = useState(false);
  const [copied, setCopied] = useState(false);
  if (!text) return null;
  return (
    <div className="mt-1 min-w-0 pl-1 text-xs text-muted">
      <div className="flex min-w-0 items-start gap-2">
        <span
          className={cn(
            "mt-0.5 shrink-0 rounded-full px-1.5 py-0.5 font-medium",
            summary.status === "ready"
              ? "bg-success/10 text-success"
              : "bg-raised text-muted",
          )}
        >
          {summaryLabel(summary.status)}
        </span>
        {onOpenConversation ? (
          <button
            type="button"
            aria-label={`open conversation ${summary.conversationId}`}
            className="min-w-0 flex-1 break-words text-left hover:text-content hover:underline"
            onClick={() => onOpenConversation(summary.conversationId)}
          >
            {text}
          </button>
        ) : (
          <span className="min-w-0 flex-1 break-words">{text}</span>
        )}
        <button
          type="button"
          className="shrink-0 rounded px-1 py-0.5 text-[11px] text-muted hover:bg-raised hover:text-content"
          onClick={() => setExpanded((v) => !v)}
        >
          {expanded ? "Hide" : "View"}
        </button>
        <button
          type="button"
          className="shrink-0 rounded px-1 py-0.5 text-[11px] text-muted hover:bg-raised hover:text-content disabled:opacity-50"
          disabled={!text}
          onClick={() => {
            void copyToClipboard(text).then(() => {
              setCopied(true);
              window.setTimeout(() => setCopied(false), 1000);
            });
          }}
        >
          {copied ? "Copied" : "Copy"}
        </button>
      </div>
      {expanded && (
        <div
          aria-label={`conversation ${summary.conversationId} details`}
          className="mt-1 space-y-1 border-l border-border pl-2"
        >
          {summary.objectivePreview && (
            <p>
              <span className="font-medium text-content">Goal:</span>{" "}
              {summary.objectivePreview}
            </p>
          )}
          {summary.summaryPreview && (
            <p>
              <span className="font-medium text-content">Summary:</span>{" "}
              {summary.summaryPreview}
            </p>
          )}
          {(summary.recentTurns ?? []).length > 0 && (
            <ul className="space-y-0.5">
              {(summary.recentTurns ?? []).map((turn) => (
                <li key={`${turn.atMs}-${turn.role}-${turn.textPreview}`}>
                  <span className="font-medium text-content">{turn.role}:</span>{" "}
                  {turn.textPreview}
                </li>
              ))}
            </ul>
          )}
        </div>
      )}
    </div>
  );
}

function TicketRow({
  ticket,
  summary,
  onOpenConversation,
}: {
  ticket: AgentTicketState;
  summary?: ConversationWorkSummary;
  onOpenConversation?: (conversationId: string) => void;
}) {
  const truncated = ticket.taskLen > ticket.taskPreview.length;
  return (
    <li className="min-w-0 text-xs text-muted">
      <div className="flex min-w-0 items-start gap-2">
        <span
          className={cn(
            "mt-0.5 shrink-0 rounded-full px-1.5 py-0.5 font-medium",
            ticket.status === "inProgress"
              ? "bg-warning/15 text-warning"
              : "bg-raised text-muted",
          )}
        >
          #{ticket.position + 1} {formatStatus(ticket.status)}
        </span>
        <span className="min-w-0 flex-1">
          <span className="font-medium text-content">{requesterLabel(ticket)}</span>
          <span className="text-muted"> · </span>
          <span className="break-words">{ticket.taskPreview}</span>
          {truncated && (
            <span
              aria-label={`${ticket.taskLen - ticket.taskPreview.length} more characters`}
              className="text-muted"
            >
              {" "}
              +{ticket.taskLen - ticket.taskPreview.length}
            </span>
          )}
        </span>
        <code
          aria-label={`ticket ${shortTicket(ticket.ticketId)}`}
          title={ticket.ticketId}
          className="mt-0.5 shrink-0 rounded bg-raised px-1.5 py-0.5 text-[11px] text-muted"
        >
          {shortTicket(ticket.ticketId)}
        </code>
      </div>
      {summary && (
        <ConversationSummaryLine
          summary={summary}
          onOpenConversation={onOpenConversation}
        />
      )}
    </li>
  );
}

function InboxRow({ item }: { item: InboxItem }) {
  return (
    <li className="min-w-0 text-xs text-muted">
      <div className="flex min-w-0 items-start gap-2">
        <span className="mt-0.5 shrink-0 rounded-full bg-raised px-1.5 py-0.5 font-medium text-muted">
          {item.kind}
        </span>
        <span className="min-w-0 flex-1 break-words">{item.body}</span>
        <code
          title={item.id}
          className="mt-0.5 shrink-0 rounded bg-raised px-1.5 py-0.5 text-[11px] text-muted"
        >
          {shortTicket(item.id)}
        </code>
      </div>
    </li>
  );
}

function BackgroundTaskRow({
  task,
  onRefresh,
}: {
  task: BackgroundCompletion;
  onRefresh: () => Promise<void>;
}) {
  const { workState } = useGateways();
  const [open, setOpen] = useState(false);
  const [liveOpen, setLiveOpen] = useState(false);
  const [liveOutput, setLiveOutput] = useState("");
  const [actionBusy, setActionBusy] = useState(false);
  const [liveBusy, setLiveBusy] = useState(false);
  const [message, setMessage] = useState<string | null>(null);
  const attachmentRef = useRef<BackgroundTaskAttachment | null>(null);
  const attachSeqRef = useRef(0);
  const hasOutput = Boolean(task.stdoutTail || task.stderrTail);
  const canCancel = task.status === "running" || task.status === "pending";
  const canRetry = task.status === "failed" || task.status === "cancelled";
  const canAttachLive = canCancel;

  function detachLive(): void {
    attachSeqRef.current += 1;
    attachmentRef.current?.detach();
    attachmentRef.current = null;
    setLiveOpen(false);
  }

  useEffect(() => {
    return () => {
      attachSeqRef.current += 1;
      attachmentRef.current?.detach();
      attachmentRef.current = null;
    };
  }, [task.taskId]);

  useEffect(() => {
    if (!canAttachLive && attachmentRef.current) detachLive();
  }, [canAttachLive]);

  async function runAction(
    action: (taskId: string) => Promise<void>,
  ): Promise<void> {
    setActionBusy(true);
    setMessage(null);
    try {
      await action(task.taskId);
      await onRefresh();
    } catch (e) {
      setMessage(e instanceof Error ? e.message : String(e));
    } finally {
      setActionBusy(false);
    }
  }

  async function toggleLive(): Promise<void> {
    if (liveOpen || attachmentRef.current) {
      detachLive();
      return;
    }

    const seq = attachSeqRef.current + 1;
    attachSeqRef.current = seq;
    setLiveBusy(true);
    setMessage(null);
    setLiveOutput("");
    try {
      const decoder = new TextDecoder();
      const attachment = await workState.attachBackgroundTask(task.taskId, (bytes) => {
        setLiveOutput((prev) => prev + decoder.decode(bytes));
      });
      if (seq !== attachSeqRef.current) {
        attachment.detach();
        return;
      }
      attachmentRef.current = attachment;
      setLiveOutput(decoder.decode(attachment.scrollback));
      setLiveOpen(true);
    } catch (e) {
      setMessage(e instanceof Error ? e.message : String(e));
    } finally {
      if (seq === attachSeqRef.current) setLiveBusy(false);
    }
  }

  return (
    <li className="min-w-0 text-xs text-muted">
      <div className="flex min-w-0 items-start gap-2">
        <span
          className={cn(
            "mt-0.5 shrink-0 rounded-full px-1.5 py-0.5 font-medium",
            taskStatusClass(task.status),
          )}
        >
          {formatTaskStatus(task.status)}
        </span>
        <span className="min-w-0 flex-1">
          <span className="font-medium text-content">{task.kind}</span>
          {task.exitCode !== null && (
            <span className="text-muted"> · exit {task.exitCode}</span>
          )}
        </span>
        <Button
          size="sm"
          variant="ghost"
          disabled={!canCancel || actionBusy}
          loading={actionBusy}
          onClick={() =>
            void runAction((id) => workState.cancelBackgroundTask(id))
          }
        >
          Cancel
        </Button>
        <Button
          size="sm"
          variant="ghost"
          disabled={!canAttachLive || liveBusy}
          loading={liveBusy}
          onClick={() => void toggleLive()}
        >
          {liveOpen ? "Detach" : "Live"}
        </Button>
        <Button
          size="sm"
          variant="ghost"
          disabled={!hasOutput}
          onClick={() => setOpen((v) => !v)}
        >
          Output
        </Button>
        <Button
          size="sm"
          variant="ghost"
          disabled={!canRetry || actionBusy}
          loading={actionBusy}
          onClick={() =>
            void runAction((id) => workState.retryBackgroundTask(id))
          }
        >
          Retry
        </Button>
        <code
          title={task.taskId}
          className="mt-0.5 shrink-0 rounded bg-raised px-1.5 py-0.5 text-[11px] text-muted"
        >
          {shortTicket(task.taskId)}
        </code>
      </div>
      {task.summary && isTerminalTaskStatus(task.status) && (
        <p
          aria-label={`task ${shortTicket(task.taskId)} summary`}
          title={task.summary}
          className="mt-1 truncate pl-1 text-[11px] text-muted"
        >
          {task.summary}
        </p>
      )}
      {message && (
        <p role="alert" className="mt-1 text-xs text-danger">
          {message}
        </p>
      )}
      {open && hasOutput && (
        <pre
          aria-label={`task ${shortTicket(task.taskId)} output`}
          className="mt-1 max-h-40 overflow-auto whitespace-pre-wrap rounded border border-border bg-canvas p-2 text-[11px] text-muted"
        >
          {task.stdoutTail ? `stdout\n${task.stdoutTail}` : ""}
          {task.stdoutTail && task.stderrTail ? "\n\n" : ""}
          {task.stderrTail ? `stderr\n${task.stderrTail}` : ""}
        </pre>
      )}
      {liveOpen && (
        <pre
          aria-label={`task ${shortTicket(task.taskId)} live output`}
          className="mt-1 max-h-40 overflow-auto whitespace-pre-wrap rounded border border-border bg-canvas p-2 text-[11px] text-muted"
        >
          {liveOutput || "No live output yet."}
        </pre>
      )}
    </li>
  );
}

function AgentRow({
  agent,
  conversations,
  visibleLeaves,
  layout,
  projectId,
  onRefresh,
  onOpenConversation,
}: {
  agent: AgentWorkState;
  conversations: Map<string, ConversationWorkSummary>;
  visibleLeaves: LeafCell[];
  layout: LayoutViewModel;
  projectId: string;
  onRefresh: () => Promise<void>;
  onOpenConversation?: (conversationId: string) => void;
}) {
  const { agent: agentGateway } = useGateways();
  const [message, setMessage] = useState<string | null>(null);
  const [actionBusy, setActionBusy] = useState(false);
  const live = agent.live !== undefined;
  const busyState = agent.busy ?? { state: "idle" };
  const busy = busyState.state === "busy";
  const tickets = [...(agent.tickets ?? [])].sort((a, b) => a.position - b.position);
  const inbox = [...(agent.inbox ?? [])].sort((a, b) => a.createdAtMs - b.createdAtMs);
  const backgroundTasks = [...(agent.backgroundTasks ?? [])].sort(
    (a, b) => b.updatedAtMs - a.updatedAtMs || a.taskId.localeCompare(b.taskId),
  );
  const inboxDepth = Math.max(agent.inboxDepth ?? 0, inbox.length, tickets.length);
  const visibleNodeIds = new Set(visibleLeaves.map((leaf) => leaf.id));
  const liveVisible = live && visibleNodeIds.has(agent.live!.nodeId);
  const attachTarget = visibleLeaves.find((leaf) => !leaf.session && !leaf.agent);
  const currentTurn =
    busy || tickets.some((ticket) => ticket.status === "inProgress");

  async function refreshAfterAction(): Promise<void> {
    await onRefresh();
  }

  async function attach(): Promise<void> {
    if (!agent.live || !attachTarget || !agentGateway?.attachLiveAgent) return;
    setActionBusy(true);
    setMessage(null);
    try {
      const attached = await agentGateway.attachLiveAgent(
        projectId,
        agent.agentId,
        attachTarget.id,
      );
      await layout.attachLiveAgentToCell(
        attachTarget.id,
        agent.agentId,
        attached.sessionId,
      );
      await refreshAfterAction();
      goToCell(attachTarget.id);
    } catch (e) {
      setMessage(e instanceof Error ? e.message : String(e));
    } finally {
      setActionBusy(false);
    }
  }

  async function stop(): Promise<void> {
    if (!agent.live || !agentGateway?.stopLiveAgent) return;
    const warning = currentTurn
      ? " The current turn will be interrupted."
      : "";
    if (!window.confirm(`Stop ${agent.name}?${warning}`)) return;
    setActionBusy(true);
    setMessage(null);
    try {
      const stopped = await agentGateway.stopLiveAgent(projectId, agent.agentId);
      if (visibleNodeIds.has(agent.live.nodeId)) {
        const leaf = visibleLeaves.find((candidate) => candidate.id === agent.live?.nodeId);
        if (!leaf?.session || leaf.session === stopped.sessionId) {
          await layout.setSession(agent.live.nodeId, null);
        }
      }
      await refreshAfterAction();
    } catch (e) {
      setMessage(e instanceof Error ? e.message : String(e));
    } finally {
      setActionBusy(false);
    }
  }

  return (
    <li className="py-2 first:pt-0 last:pb-0">
      <div className="flex items-center justify-between gap-3">
        <span className="flex min-w-0 flex-col gap-0.5">
          <span className="truncate text-sm font-medium text-content">
            {agent.name}
          </span>
          <code className="truncate text-xs text-muted">{agent.profileId}</code>
        </span>
        <span className="flex shrink-0 items-center gap-1.5">
          <span
            className={cn(
              "rounded-full px-2 py-0.5 text-xs font-medium",
              live
                ? "bg-success/15 text-success"
                : "bg-raised text-muted",
            )}
          >
            {live ? "Live" : "Offline"}
          </span>
          <span
            className={cn(
              "rounded-full px-2 py-0.5 text-xs font-medium",
              busy
                ? "bg-warning/15 text-warning"
                : "bg-raised text-muted",
            )}
          >
            {busy ? "Busy" : "Idle"}
          </span>
          {inboxDepth > 0 && (
            <span
              aria-label={`${agent.name} queue depth ${inboxDepth}`}
              className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary"
            >
              Queue {inboxDepth}
            </span>
          )}
          {busyState.state === "busy" && (
            <code
              aria-label={`busy ticket ${shortTicket(busyState.ticket)}`}
              title={busyState.ticket}
              className="rounded bg-raised px-1.5 py-0.5 text-xs text-muted"
            >
              {shortTicket(busyState.ticket)}
            </code>
          )}
          {agent.live && liveVisible && (
            <Button
              size="sm"
              variant="ghost"
              onClick={() => goToCell(agent.live!.nodeId)}
            >
              Open
            </Button>
          )}
          {agent.live && !liveVisible && (
            <Button
              size="sm"
              variant="ghost"
              disabled={!attachTarget || !agentGateway?.attachLiveAgent}
              loading={actionBusy}
              title={
                attachTarget
                  ? undefined
                  : "No empty visible cell available"
              }
              onClick={() => void attach()}
            >
              Attach
            </Button>
          )}
          {agent.live && (
            <Button
              size="sm"
              variant="danger"
              loading={actionBusy}
              disabled={!agentGateway?.stopLiveAgent}
              onClick={() => void stop()}
            >
              Stop
            </Button>
          )}
        </span>
      </div>
      {agent.live && !liveVisible && !attachTarget && (
        <p className="mt-1 text-xs text-muted">No empty visible cell.</p>
      )}
      {message && (
        <p role="alert" className="mt-1 text-xs text-danger">
          {message}
        </p>
      )}
      {tickets.length > 0 && (
        <ul
          aria-label={`${agent.name} tickets`}
          className="mt-2 flex flex-col gap-1"
        >
          {tickets.map((ticket) => (
            <TicketRow
              key={ticket.ticketId}
              ticket={ticket}
              summary={conversations.get(ticket.conversationId)}
              onOpenConversation={onOpenConversation}
            />
          ))}
        </ul>
      )}
      {inbox.length > 0 && (
        <div className="mt-2">
          <p className="text-[11px] font-semibold uppercase tracking-wide text-faint">
            Inbox
          </p>
          <ul aria-label={`${agent.name} inbox`} className="mt-1 flex flex-col gap-1">
            {inbox.map((item) => (
              <InboxRow key={item.id} item={item} />
            ))}
          </ul>
        </div>
      )}
      {backgroundTasks.length > 0 && (
        <div className="mt-2">
          <p className="text-[11px] font-semibold uppercase tracking-wide text-faint">
            Background tasks
          </p>
          <ul
            aria-label={`${agent.name} background tasks`}
            className="mt-1 flex flex-col gap-1"
          >
            {backgroundTasks.map((task) => (
              <BackgroundTaskRow
                key={task.taskId}
                task={task}
                onRefresh={refreshAfterAction}
              />
            ))}
          </ul>
        </div>
      )}
    </li>
  );
}

function ProjectWorkStatePanelContent({
  projectId,
  onOpenConversation,
}: ProjectWorkStatePanelProps) {
  const vm = useProjectWorkState(projectId);
  const layoutVm = useLayout(projectId);
  const agents = vm.state?.agents ?? [];
  const visibleLeaves = layoutVm.layout ? leaves(layoutVm.layout) : [];
  const conversations = new Map(
    (vm.state?.conversations ?? []).map((summary) => [
      summary.conversationId,
      summary,
    ]),
  );

  return (
    <Panel
      title="Work"
      actions={
        <Button
          size="sm"
          variant="ghost"
          onClick={() => void vm.refresh()}
          loading={vm.busy}
        >
          Refresh
        </Button>
      }
    >
      {vm.error && (
        <p
          role="alert"
          className="mb-3 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger"
        >
          {vm.error}
        </p>
      )}
      {vm.busy && vm.state === null ? (
        <div className="flex items-center gap-2 text-sm text-muted">
          <Spinner size={14} />
          <span>Loading work state…</span>
        </div>
      ) : agents.length === 0 ? (
        <p className="text-sm text-muted">No agent work state.</p>
      ) : (
        <ul className="flex flex-col divide-y divide-border">
          {agents.map((agent) => (
            <AgentRow
              key={agent.agentId}
              agent={agent}
              conversations={conversations}
              visibleLeaves={visibleLeaves}
              layout={layoutVm}
              projectId={projectId}
              onRefresh={vm.refresh}
              onOpenConversation={onOpenConversation}
            />
          ))}
        </ul>
      )}
    </Panel>
  );
}

export function ProjectWorkStatePanel(props: ProjectWorkStatePanelProps) {
  return (
    <WorkStatePanelErrorBoundary>
      <ProjectWorkStatePanelContent {...props} />
    </WorkStatePanelErrorBoundary>
  );
}
