/**
 * Tauri adapter for {@link AgentGateway} (L6).
 *
 * NOTE: The Tauri commands wired here (`list_agents`, `create_agent`, …) are
 * defined in the backend `app-tauri` crate and will be registered in a
 * subsequent lot. This adapter is complete on the frontend side; the mock
 * gateway covers tests and offline dev today. The real mode will work
 * transparently once the commands are registered.
 *
 * Commands use snake_case (Tauri convention); payload keys are camelCase
 * (matching the backend DTO `#[serde(rename_all = "camelCase")]`), consistent
 * with the other adapters in this directory.
 */

import { Channel, invoke } from "@tauri-apps/api/core";

import type {
  Agent,
  AgentContextDocument,
  EffortSelection,
  ResumableAgent,
  TerminalSession,
} from "@/domain";
import type {
  AgentGateway,
  ConversationDetails,
  CreateAgentInput,
  LiveAgent,
  OpenTerminalOptions,
  ReattachResult,
  StoppedLiveAgent,
  TerminalHandle,
} from "@/ports";
import { makeTerminalHandle } from "./terminal";

/** Wire shape returned by the `launch_agent` command (mirrors `open_terminal`). */
interface LaunchAgentResponse {
  sessionId: string;
  cwd: string;
  rows: number;
  cols: number;
  /** Conversation id minted by this launch (omitted when nothing was assigned). */
  assignedConversationId?: string;
}

export class TauriAgentGateway implements AgentGateway {
  listAgents(projectId: string): Promise<Agent[]> {
    return invoke<Agent[]>("list_agents", { projectId });
  }

  listLiveAgents(projectId: string): Promise<LiveAgent[]> {
    return invoke<LiveAgent[]>("list_live_agents", { projectId });
  }

  listResumableAgents(projectId: string): Promise<ResumableAgent[]> {
    // `list_resumable_agents` takes a top-level `projectId` (camelCase) and
    // returns `{ resumable: ResumableAgentDto[] }`. The DTO is already camelCase
    // and shape-compatible with `ResumableAgent`, so we just unwrap the list.
    return invoke<{ resumable: ResumableAgent[] }>("list_resumable_agents", {
      projectId,
    }).then((res) => res.resumable);
  }

  attachLiveAgent(
    projectId: string,
    agentId: string,
    nodeId: string,
  ): Promise<LiveAgent> {
    return invoke<LiveAgent>("attach_live_agent", {
      request: { projectId, agentId, nodeId },
    });
  }

  stopLiveAgent(projectId: string, agentId: string): Promise<StoppedLiveAgent> {
    return invoke<StoppedLiveAgent>("stop_live_agent", {
      request: { projectId, agentId },
    });
  }

  createAgent(projectId: string, input: CreateAgentInput): Promise<Agent> {
    // The `create_agent` command takes a single `request` DTO; `projectId` must
    // live *inside* it (camelCase), not at the top level.
    return invoke<Agent>("create_agent", {
      request: {
        projectId,
        name: input.name,
        profileId: input.profileId,
        initialContent: input.initialContent ?? null,
      },
    });
  }

  changeAgentProfile(
    projectId: string,
    agentId: string,
    profileId: string,
    rows: number,
    cols: number,
  ): Promise<{ agent: Agent; relaunchedSession?: TerminalSession }> {
    // `change_agent_profile` takes a single `request` DTO; the camelCase keys
    // match the backend `ChangeAgentProfileRequestDto`. The response carries the
    // mutated agent and, when a live session was hot-swapped, the relaunched one
    // (omitted otherwise — `relaunchedSession` stays `undefined`).
    return invoke<{ agent: Agent; relaunchedSession?: TerminalSession }>(
      "change_agent_profile",
      { request: { projectId, agentId, profileId, rows, cols } },
    );
  }

  updateAgentEffort(
    projectId: string,
    agentId: string,
    effort: EffortSelection | null,
  ): Promise<Agent> {
    return invoke<Agent>("update_agent_effort", {
      request: { projectId, agentId, effort },
    });
  }

  readContext(projectId: string, agentId: string): Promise<string> {
    return invoke<AgentContextDocument>("read_agent_context", {
      projectId,
      agentId,
    }).then((res) => res.content);
  }

  async updateContext(
    projectId: string,
    agentId: string,
    content: string,
  ): Promise<void> {
    // `update_agent_context` takes a single `request` DTO.
    await invoke("update_agent_context", {
      request: { projectId, agentId, content },
    });
  }

  async deleteAgent(projectId: string, agentId: string): Promise<void> {
    await invoke("delete_agent", { projectId, agentId });
  }

  async launchAgent(
    projectId: string,
    agentId: string,
    options: OpenTerminalOptions,
    onData: (bytes: Uint8Array) => void,
  ): Promise<TerminalHandle> {
    // Per-session output channel. The backend serialises chunks as byte arrays.
    const channel = new Channel<number[]>();
    channel.onmessage = (chunk) => onData(Uint8Array.from(chunk));

    const res = await invoke<LaunchAgentResponse>("launch_agent", {
      request: {
        projectId,
        agentId,
        rows: options.rows,
        cols: options.cols,
        // Resume id: the leaf's persisted conversation id, when any (T4b). The
        // backend resumes it; absent ⇒ a fresh cell that may get a new id.
        conversationId: options.conversationId ?? null,
        // Hosting cell: lets the backend enforce one-live-session-per-agent.
        nodeId: options.nodeId ?? null,
      },
      onOutput: channel,
    });

    const base = makeTerminalHandle(res.sessionId, channel);
    // Surface the id assigned by this launch (so the caller persists it on the
    // leaf and resumes next time).
    return {
      ...base,
      ...(res.assignedConversationId
        ? { assignedConversationId: res.assignedConversationId }
        : {}),
    };
  }

  async reattach(
    sessionId: string,
    onData: (bytes: Uint8Array) => void,
  ): Promise<ReattachResult> {
    // Agent sessions reattach through the same session-based `reattach_terminal`
    // command as plain terminals (the PTY is identified by its session id).
    const channel = new Channel<number[]>();
    channel.onmessage = (chunk) => onData(Uint8Array.from(chunk));

    const res = await invoke<{ sessionId: string; scrollback: number[] }>(
      "reattach_terminal",
      { sessionId, onOutput: channel },
    );

    return {
      handle: makeTerminalHandle(res.sessionId, channel),
      scrollback: Uint8Array.from(res.scrollback),
    };
  }

  inspectConversation(
    projectId: string,
    agentId: string,
    conversationId: string,
  ): Promise<ConversationDetails> {
    // `inspect_conversation` takes a single `request` DTO; absent fields are
    // omitted from the response (best-effort), so an empty object is valid.
    return invoke<ConversationDetails>("inspect_conversation", {
      request: { projectId, agentId, conversationId },
    });
  }
}
