/**
 * `useAgents` — view-model hook for the agents feature (L6).
 *
 * Owns the agents feature state for a given project and exposes the actions the
 * UI triggers. Consumes {@link AgentGateway} and {@link ProfileGateway}
 * exclusively; never touches `invoke()` or `@tauri-apps/api`, keeping the
 * component layer testable with mock gateways (ARCHITECTURE §1.3).
 */

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

import type {
  Agent,
  AgentProfile,
  GatewayError,
  ModelServerStatus,
  TerminalSession,
} from "@/domain";
import type { LiveAgent, OpenTerminalOptions, TerminalHandle } from "@/ports";
import { useGateways } from "@/app/di";

/**
 * Per-agent session-limit state (ARCHITECTURE §21), populated from the limit
 * domain events. An agent absent from the map is in its normal (unlimited) state.
 */
export interface AgentLimitState {
  /**
   * Reset instant in epoch-milliseconds when known (from `agentRateLimited` /
   * `agentRateLimitSuspected`). Absent ⇒ "limité" without a reliable time.
   */
  limitedUntil?: number;
  /**
   * Wake-up deadline in epoch-milliseconds of an armed auto-resume (from
   * `agentResumeScheduled`). Present ⇒ show the countdown + "Annuler la reprise".
   */
  resumeFireAt?: number;
  /**
   * Human net (level 3): the limit was suspected without a reliable time
   * (`agentRateLimitSuspected`). The UI surfaces an "heure inconnue" state and
   * asks the user — IdeA never resumes blind.
   */
  suspected?: boolean;
}

/**
 * An agent-launch failure surfaced to the UI (F35), from `agentLaunchFailed`.
 * `cause` namespaces the origin (e.g. `"model_server"`); `code`/`message` are
 * the stable, actionable failure details.
 */
export interface AgentLaunchFailure {
  cause: string;
  code: string;
  message: string;
}

/** What the agents UI needs from this hook. */
export interface AgentsViewModel {
  /** All agents known to the project. */
  agents: Agent[];
  /** Id of the currently selected agent, or `null`. */
  selectedAgentId: string | null;
  /** The `.md` context of the selected agent (loaded on select). */
  context: string;
  /** Profiles available for assigning to a new agent. */
  profiles: AgentProfile[];
  /** Agents that currently own a live PTY session, as tracked by IdeA. */
  liveAgents: LiveAgent[];
  /**
   * The entry door (`"mcp"` | `"file"`) the *last* orchestration delegation from
   * each requester agent arrived through, keyed by requester id. Populated from
   * `orchestratorRequestProcessed` events; a requester absent here (or an event
   * without `source`) yields no badge.
   */
  delegationSourceByRequester: Record<string, "mcp" | "file">;
  /**
   * Per-agent session-limit state (ARCHITECTURE §21), keyed by agent id. Populated
   * from the limit domain events (`agentRateLimited`, `agentResumeScheduled`,
   * `agentResumeCancelled`, `agentResumed`, `agentRateLimitSuspected`). An agent
   * absent from the map is unlimited.
   */
  limitByAgent: Record<string, AgentLimitState>;
  /**
   * Local model-server lifecycle status keyed by `serverId` (F35), folded from
   * `modelServerStatusChanged`. Correlate an agent to its server through the
   * profile's `opencode.localModelServerId`. A server absent from the map has no
   * observed status yet.
   */
  modelServerStatusByServer: Record<string, ModelServerStatus>;
  /**
   * Last agent-launch failure keyed by agent id (F35), folded from
   * `agentLaunchFailed`. Carries an actionable `message`/`code`/`cause`. Cleared
   * for an agent as soon as it launches successfully (`agentLaunched`).
   */
  launchFailureByAgent: Record<string, AgentLaunchFailure>;
  /** Last error message, or `null`. */
  error: string | null;
  /** Whether a request is in flight. */
  busy: boolean;
  /**
   * Id of the agent whose terminal is currently running, or `null`.
   * Set when the user clicks Launch; cleared when the terminal is stopped.
   */
  runningAgentId: string | null;
  /** Reloads the agent list. */
  refresh: () => Promise<void>;
  /** Reloads the currently-live agent sessions. */
  refreshLiveAgents: () => Promise<void>;
  /** Creates a new agent and refreshes the list. */
  createAgent: (name: string, profileId: string, initialContent?: string) => Promise<void>;
  /** Selects an agent and loads its context. */
  selectAgent: (agentId: string) => Promise<void>;
  /** Saves the context for the currently selected agent. */
  saveContext: (content: string) => Promise<void>;
  /**
   * Hot-swaps an agent's runtime AI profile. The caller is expected to have
   * confirmed with the user first (the conversation history is abandoned). On a
   * live agent the backend relaunches the session and returns it so the cell can
   * rebind; the agent list is refreshed locally. Returns the relaunched session,
   * if any, so the panel can rebind the hosting cell.
   */
  changeAgentProfile: (
    agentId: string,
    profileId: string,
    rows: number,
    cols: number,
  ) => Promise<TerminalSession | undefined>;
  /** Deletes an agent; deselects if it was selected. */
  deleteAgent: (agentId: string) => Promise<void>;
  /**
   * Returns an opener compatible with `TerminalView`'s `open` prop for the
   * given agent. Calling it sets `runningAgentId`. The component unmounting
   * will call `handle.close()` automatically via the TerminalView cleanup.
   */
  launchAgent: (
    agentId: string,
    options: OpenTerminalOptions,
    onData: (bytes: Uint8Array) => void,
  ) => Promise<TerminalHandle>;
  /** Stops an agent PTY explicitly by closing its live session id. */
  stopAgent: (agentId?: string) => Promise<void>;
  /**
   * Cancels the auto-resume armed for a rate-limited agent (ARCHITECTURE §21):
   * the "Annuler la reprise" action. Optimistically drops the countdown; the
   * backend's `agentResumeCancelled` event confirms it. The agent stays "limité"
   * (no auto-resume will fire). Resolves to the backend verdict (`false` if the
   * resume had already fired / none was armed).
   */
  cancelResume: (agentId: string) => Promise<boolean>;
  /**
   * Human net (level 3, ARCHITECTURE §21.1): arms a resume at a user-chosen
   * `resetsAtMs` (epoch-ms) for an agent whose limit was suspected without a
   * reliable time. No optimistic mutation — the backend re-emits
   * `agentResumeScheduled`, which flips the badge to the nominal countdown.
   */
  setResumeAt: (agentId: string, resetsAtMs: number) => Promise<void>;
}

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

export function useAgents(projectId: string): AgentsViewModel {
  const { agent, profile, system, terminal, input } = useGateways();

  const [agents, setAgents] = useState<Agent[]>([]);
  const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
  const [context, setContext] = useState<string>("");
  const [profiles, setProfiles] = useState<AgentProfile[]>([]);
  const [liveAgents, setLiveAgents] = useState<LiveAgent[]>([]);
  const [error, setError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  const [runningAgentId, setRunningAgentId] = useState<string | null>(null);
  const [delegationSourceByRequester, setDelegationSourceByRequester] = useState<
    Record<string, "mcp" | "file">
  >({});
  const [limitByAgent, setLimitByAgent] = useState<
    Record<string, AgentLimitState>
  >({});
  const [modelServerStatusByServer, setModelServerStatusByServer] = useState<
    Record<string, ModelServerStatus>
  >({});
  const [launchFailureByAgent, setLaunchFailureByAgent] = useState<
    Record<string, AgentLaunchFailure>
  >({});

  const refresh = useCallback(async () => {
    setBusy(true);
    setError(null);
    try {
      const [agentList, profileList] = await Promise.all([
        agent.listAgents(projectId),
        profile.listProfiles(),
      ]);
      setAgents(agentList);
      setProfiles(profileList);
    } catch (e) {
      setError(describe(e));
    } finally {
      setBusy(false);
    }
  }, [agent, profile, projectId]);

  const refreshLiveAgents = useCallback(async () => {
    try {
      setLiveAgents(await agent.listLiveAgents(projectId));
    } catch {
      setLiveAgents([]);
    }
  }, [agent, projectId]);

  useEffect(() => {
    void refresh();
  }, [refresh]);

  useEffect(() => {
    void refreshLiveAgents();
  }, [refreshLiveAgents]);

  // Live refresh: the agents list is otherwise only reloaded on mount or after a
  // UI-driven CRUD. An agent created/launched/stopped *out of band* — most
  // notably by the orchestrator (`spawn_agent`, ARCHITECTURE §14.3) when an AI
  // delegates agent creation to IdeA — emits `agentLaunched` / `agentExited`.
  // Subscribing here makes the tab reflect those changes without a manual reload.
  // `system` may be absent in unit tests that inject a partial gateway set; guard.
  useEffect(() => {
    if (!system) return;
    let unsubscribe: (() => void) | undefined;
    let cancelled = false;
    void system
      .onDomainEvent((event) => {
        if (
          event.type === "agentLaunched" ||
          event.type === "agentExited" ||
          event.type === "agentProfileChanged"
        ) {
          void refresh();
          void refreshLiveAgents();
        }
        // A successful launch supersedes any prior launch failure for that agent
        // (F35): drop its stale error banner.
        if (event.type === "agentLaunched") {
          setLaunchFailureByAgent((prev) => {
            if (!(event.agentId in prev)) return prev;
            const { [event.agentId]: _cleared, ...rest } = prev;
            return rest;
          });
        }
        // Record which door a delegation came through (mcp vs file). Absent
        // `source` (older backend) leaves the map untouched → no badge.
        if (
          event.type === "orchestratorRequestProcessed" &&
          event.source !== undefined
        ) {
          const { requesterId, source } = event;
          setDelegationSourceByRequester((prev) =>
            prev[requesterId] === source
              ? prev
              : { ...prev, [requesterId]: source },
          );
        }
        // Session-limit lifecycle (ARCHITECTURE §21): fold each event into the
        // per-agent limit state. `agentResumed` clears the entry entirely.
        switch (event.type) {
          case "agentRateLimited":
            setLimitByAgent((prev) => ({
              ...prev,
              [event.agentId]: {
                limitedUntil: event.resetsAtMs,
                resumeFireAt: prev[event.agentId]?.resumeFireAt,
                suspected: false,
              },
            }));
            break;
          case "agentResumeScheduled":
            setLimitByAgent((prev) => ({
              ...prev,
              [event.agentId]: {
                ...prev[event.agentId],
                resumeFireAt: event.fireAtMs,
              },
            }));
            break;
          case "agentResumeCancelled":
            setLimitByAgent((prev) => {
              const current = prev[event.agentId];
              if (!current) return prev;
              // Keep the agent "limité" but drop the armed resume countdown.
              const { resumeFireAt: _dropped, ...rest } = current;
              return { ...prev, [event.agentId]: rest };
            });
            break;
          case "agentResumed":
            setLimitByAgent((prev) => {
              if (!(event.agentId in prev)) return prev;
              const { [event.agentId]: _cleared, ...rest } = prev;
              return rest;
            });
            break;
          case "agentRateLimitSuspected":
            setLimitByAgent((prev) => ({
              ...prev,
              [event.agentId]: {
                ...prev[event.agentId],
                limitedUntil: event.resetsAtMs,
                suspected: true,
              },
            }));
            break;
          // F35 — local model server lifecycle during launch, keyed by server id.
          case "modelServerStatusChanged":
            setModelServerStatusByServer((prev) => ({
              ...prev,
              [event.serverId]: event.status,
            }));
            break;
          // F35 — a launch failed with an actionable cause/message, keyed by agent.
          case "agentLaunchFailed":
            setLaunchFailureByAgent((prev) => ({
              ...prev,
              [event.agentId]: {
                cause: event.cause,
                code: event.code,
                message: event.message,
              },
            }));
            break;
          default:
            break;
        }
      })
      .then((un) => {
        if (cancelled) un();
        else unsubscribe = un;
      });
    return () => {
      cancelled = true;
      unsubscribe?.();
    };
  }, [system, refresh, refreshLiveAgents]);

  const createAgent = useCallback(
    async (name: string, profileId: string, initialContent?: string) => {
      setBusy(true);
      setError(null);
      try {
        const created = await agent.createAgent(projectId, {
          name,
          profileId,
          initialContent,
        });
        setAgents((prev) => [...prev, created]);
      } catch (e) {
        setError(describe(e));
      } finally {
        setBusy(false);
      }
    },
    [agent, projectId],
  );

  const selectAgent = useCallback(
    async (agentId: string) => {
      setBusy(true);
      setError(null);
      try {
        const ctx = await agent.readContext(projectId, agentId);
        setSelectedAgentId(agentId);
        setContext(ctx);
      } catch (e) {
        setError(describe(e));
      } finally {
        setBusy(false);
      }
    },
    [agent, projectId],
  );

  const saveContext = useCallback(
    async (content: string) => {
      if (!selectedAgentId) return;
      setBusy(true);
      setError(null);
      try {
        await agent.updateContext(projectId, selectedAgentId, content);
        setContext(content);
      } catch (e) {
        setError(describe(e));
      } finally {
        setBusy(false);
      }
    },
    [agent, projectId, selectedAgentId],
  );

  const changeAgentProfile = useCallback(
    async (
      agentId: string,
      profileId: string,
      rows: number,
      cols: number,
    ): Promise<TerminalSession | undefined> => {
      setBusy(true);
      setError(null);
      try {
        const { agent: updated, relaunchedSession } =
          await agent.changeAgentProfile(projectId, agentId, profileId, rows, cols);
        // Reflect the new profile locally; the `agentProfileChanged` event (when
        // wired) also triggers a full refresh, but updating here keeps the UI
        // responsive offline / in tests.
        setAgents((prev) =>
          prev.map((a) => (a.id === updated.id ? updated : a)),
        );
        // A hot relaunch produces a fresh live session id — keep the live list
        // in sync so the cell can rebind.
        void refreshLiveAgents();
        return relaunchedSession;
      } catch (e) {
        setError(describe(e));
        return undefined;
      } finally {
        setBusy(false);
      }
    },
    [agent, projectId, refreshLiveAgents],
  );

  const deleteAgent = useCallback(
    async (agentId: string) => {
      setBusy(true);
      setError(null);
      try {
        await agent.deleteAgent(projectId, agentId);
        setAgents((prev) => prev.filter((a) => a.id !== agentId));
        setSelectedAgentId((current) => (current === agentId ? null : current));
        if (selectedAgentId === agentId) setContext("");
        // Also stop the terminal if the running agent was just deleted.
        setRunningAgentId((current) => (current === agentId ? null : current));
      } catch (e) {
        setError(describe(e));
      } finally {
        setBusy(false);
      }
    },
    [agent, projectId, selectedAgentId],
  );

  const launchAgent = useCallback(
    async (
      agentId: string,
      options: OpenTerminalOptions,
      onData: (bytes: Uint8Array) => void,
    ): Promise<TerminalHandle> => {
      setError(null);
      try {
        const handle = await agent.launchAgent(projectId, agentId, options, onData);
        setRunningAgentId(agentId);
        void refreshLiveAgents();
        return handle;
      } catch (e) {
        setError(describe(e));
        throw e;
      }
    },
    [agent, projectId, refreshLiveAgents],
  );

  const stopAgent = useCallback(
    async (agentId?: string) => {
      const targetAgentId = agentId ?? runningAgentId;
      const live = targetAgentId
        ? liveAgents.find((candidate) => candidate.agentId === targetAgentId)
        : null;
      setRunningAgentId((current) =>
        current === targetAgentId || !targetAgentId ? null : current,
      );
      if (live?.sessionId) {
        try {
          await terminal?.closeTerminal(live.sessionId);
          setLiveAgents((current) =>
            current.filter((candidate) => candidate.agentId !== targetAgentId),
          );
        } catch (e) {
          setError(describe(e));
          await refreshLiveAgents();
        }
      }
    },
    [liveAgents, refreshLiveAgents, runningAgentId, terminal],
  );

  const cancelResume = useCallback(
    async (agentId: string): Promise<boolean> => {
      // Optimistically drop the countdown so the button feels responsive; the
      // backend's `agentResumeCancelled` event confirms it (and a `false` verdict
      // — already fired — is reconciled by the eventual `agentResumed`).
      setLimitByAgent((prev) => {
        const current = prev[agentId];
        if (!current?.resumeFireAt) return prev;
        const { resumeFireAt: _dropped, ...rest } = current;
        return { ...prev, [agentId]: rest };
      });
      try {
        return (await input?.cancelResume(agentId)) ?? false;
      } catch (e) {
        setError(describe(e));
        return false;
      }
    },
    [input],
  );

  const setResumeAt = useCallback(
    async (agentId: string, resetsAtMs: number): Promise<void> => {
      // No optimistic mutation: the backend re-emits `agentResumeScheduled`,
      // which the subscription folds into `limitByAgent` (badge → countdown).
      try {
        await input?.setResumeAt(agentId, resetsAtMs);
      } catch (e) {
        setError(describe(e));
      }
    },
    [input],
  );

  return {
    agents,
    selectedAgentId,
    context,
    profiles,
    liveAgents,
    delegationSourceByRequester,
    limitByAgent,
    modelServerStatusByServer,
    launchFailureByAgent,
    error,
    busy,
    runningAgentId,
    refresh,
    refreshLiveAgents,
    createAgent,
    selectAgent,
    saveContext,
    changeAgentProfile,
    deleteAgent,
    launchAgent,
    stopAgent,
    cancelResume,
    setResumeAt,
  };
}
