/**
 * `useResumeProject` — view-model for the reopen resume flow (L15 / lot B2,
 * ARCHITECTURE §15.2).
 *
 * On project open, it pulls the read-only resumable inventory
 * ({@link AgentGateway.listResumableAgents}) and drives the per-agent resume
 * actions of the {@link ResumeProjectPanel}. The actual resume **reuses the
 * existing `launch_agent` flow** — no new backend use case:
 *
 * - **Reprendre**  → `launchAgent(nodeId, conversationId)` (resume the CLI
 *   conversation). For a profile without a usable session strategy
 *   (`resumeSupported === false`) the entry simply carries no `conversationId`,
 *   so the launch starts fresh ("relance à neuf").
 * - **Nouvelle conversation** → `setCellConversation(nodeId, null)` first (so the
 *   backend treats the cell as fresh and assigns a new id), then
 *   `launchAgent(nodeId)` without a conversation id.
 * - **Ignorer** → drops the entry locally (no launch).
 *
 * The launch happens at the project level (no mounted xterm view yet): `onData`
 * is a no-op sink — the hosting cell reattaches to the spawned live session when
 * it mounts. It consumes the `agent` + `layout` gateways exclusively; never
 * `invoke()` (ARCHITECTURE §1.3), so it stays testable with mocks.
 */

import { useCallback, useEffect, useState } from "react";

import type { GatewayError, ResumableAgent } from "@/domain";
import { useGateways } from "@/app/di";

/** Default PTY geometry used for an open-time (headless) resume launch. */
const DEFAULT_ROWS = 24;
const DEFAULT_COLS = 80;

/** What the {@link ResumeProjectPanel} needs from this hook. */
export interface ResumeProjectViewModel {
  /** The agents still pending a decision (drains as the user acts). */
  pending: ResumableAgent[];
  /** Last error message, or `null`. */
  error: string | null;
  /** Whether a resume launch is in flight. */
  busy: boolean;
  /** Resumes one agent (keeps its conversation id ⇒ Resume / fresh if unsupported). */
  resume: (agent: ResumableAgent) => Promise<void>;
  /** Starts a fresh conversation for one agent (clears the id, then launches). */
  newConversation: (agent: ResumableAgent) => Promise<void>;
  /** Dismisses one agent without launching it. */
  ignore: (agentId: string) => void;
  /** Resumes every pending agent. */
  resumeAll: () => Promise<void>;
  /** Dismisses every pending agent without launching. */
  ignoreAll: () => void;
}

function describe(e: unknown): string {
  if (e && typeof e === "object" && "message" in e) {
    return String((e as GatewayError).message);
  }
  return String(e);
}

/** A no-op output sink: the hosting cell reattaches to the live session later. */
const NO_DATA = (_bytes: Uint8Array): void => {};

export function useResumeProject(
  projectId: string,
  cwd: string,
): ResumeProjectViewModel {
  const { agent, layout } = useGateways();
  const [pending, setPending] = useState<ResumableAgent[]>([]);
  const [error, setError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);

  // Pull the inventory once per (project) open. A failure degrades to an empty
  // list (no panel), never surfaces — the per-cell fallback still works.
  useEffect(() => {
    if (!agent) return;
    let cancelled = false;
    agent
      .listResumableAgents(projectId)
      .then((list) => {
        if (!cancelled) setPending(list);
      })
      .catch(() => {
        if (!cancelled) setPending([]);
      });
    return () => {
      cancelled = true;
    };
  }, [agent, projectId]);

  /** Removes one agent from the pending set (after a decision). */
  const drop = useCallback((agentId: string) => {
    setPending((prev) => prev.filter((a) => a.agentId !== agentId));
  }, []);

  /** Launches one agent, resuming `conversationId` when provided. */
  const launch = useCallback(
    async (a: ResumableAgent, conversationId: string | undefined) => {
      if (!agent) return;
      await agent.launchAgent(
        projectId,
        a.agentId,
        {
          cwd,
          rows: DEFAULT_ROWS,
          cols: DEFAULT_COLS,
          conversationId,
          nodeId: a.nodeId,
        },
        NO_DATA,
      );
    },
    [agent, projectId, cwd],
  );

  const resume = useCallback(
    async (a: ResumableAgent) => {
      setBusy(true);
      setError(null);
      try {
        // A profile without a usable session strategy carries no conversation id
        // ⇒ launches fresh ("relance à neuf"); otherwise resume the id.
        const convId = a.resumeSupported ? a.conversationId : undefined;
        await launch(a, convId ?? undefined);
        drop(a.agentId);
      } catch (e) {
        setError(describe(e));
      } finally {
        setBusy(false);
      }
    },
    [launch, drop],
  );

  const newConversation = useCallback(
    async (a: ResumableAgent) => {
      setBusy(true);
      setError(null);
      try {
        // Clear the persisted id BEFORE launching so the backend assigns a fresh
        // conversation (mirrors LayoutGrid's `onNewConversation`).
        await layout.mutateLayout(projectId, {
          type: "setCellConversation",
          target: a.nodeId,
          conversationId: null,
        });
        await launch(a, undefined);
        drop(a.agentId);
      } catch (e) {
        setError(describe(e));
      } finally {
        setBusy(false);
      }
    },
    [layout, projectId, launch, drop],
  );

  const ignore = useCallback((agentId: string) => drop(agentId), [drop]);

  const resumeAll = useCallback(async () => {
    // Snapshot the current pending set; `resume` drains it entry by entry.
    const snapshot = pending;
    for (const a of snapshot) {
      // eslint-disable-next-line no-await-in-loop
      await resume(a);
    }
  }, [pending, resume]);

  const ignoreAll = useCallback(() => setPending([]), []);

  return {
    pending,
    error,
    busy,
    resume,
    newConversation,
    ignore,
    resumeAll,
    ignoreAll,
  };
}
