/**
 * Stream + mixed gateways for the web transport — ticket #13, lot F1.
 *
 * These gateways combine HTTP request/response (via {@link HttpInvoker}) with the
 * WebSocket live streams (via {@link WsLiveClient}), per the B0 contract:
 *  - {@link HttpSystemGateway}: `health` over HTTP; `onDomainEvent` over the WS
 *    `event.domain` stream; `pickFolder` is desktop-only ⇒ unsupported on web.
 *  - {@link HttpAgentGateway}: all agent request/response over HTTP; `launchAgent`
 *    / `reattach` over the WS PTY stream (**skeleton** — full round-trip in
 *    F3/B5/B6).
 *  - {@link HttpTicketGateway}: all `ticket_*`/`sprint_*` over HTTP; `sendTicketChat`
 *    over the WS `chat.*` stream (**skeleton**).
 *  - {@link HttpTerminalGateway}: PTY open/reattach/close over the WS terminal
 *    stream (**skeleton**).
 *
 * "Skeleton" means the frames are wired to the B0 contract and the handles are
 * structurally correct against the ports, but a live end-to-end terminal needs
 * the server (B3/B4) and the F3/B5 wiring. Lives in `src/adapters/**`; no
 * `@tauri-apps/api`.
 */

import type {
  Agent,
  AppExitWorkGuardState,
  DomainEvent,
  HealthReport,
  ReplyChunk,
  ResumableAgent,
  Sprint,
  TerminalSession,
  Ticket,
  TicketCarnet,
  TicketChat,
  TicketLinkKind,
  TicketList,
  Unsubscribe,
} from "@/domain";
import type {
  AgentGateway,
  ConversationDetails,
  CreateAgentInput,
  CreateTicketInput,
  LiveAgent,
  OpenTerminalOptions,
  ReattachResult,
  StoppedLiveAgent,
  SystemGateway,
  TerminalGateway,
  TerminalHandle,
  TicketGateway,
  TicketListQuery,
  UpdateTicketInput,
} from "@/ports";
import type { HttpInvoker } from "./httpInvoker";
import { WsLiveClient } from "./wsLiveClient";
import { type ChatOutputPayload } from "./frames";
import { unsupportedOnWeb } from "./unsupported";

/**
 * Builds a {@link TerminalHandle} whose control operations are WS frames. The
 * output stream is delivered through the sink the gateway registered on the
 * {@link WsLiveClient} for this `sessionId`.
 */
export function makeWsTerminalHandle(
  sessionId: string,
  ws: WsLiveClient,
  assignedConversationId?: string,
): TerminalHandle {
  return {
    sessionId,
    ...(assignedConversationId ? { assignedConversationId } : {}),
    async write(data: Uint8Array): Promise<void> {
      await ws.sendFireAndForget("terminal.input", {
        sessionId,
        bytesBase64: WsLiveClient.encodeInput(data),
      });
    },
    async resize(rows: number, cols: number): Promise<void> {
      await ws.sendFireAndForget("terminal.resize", { sessionId, rows, cols });
    },
    detach(): void {
      // Stop delivering output locally and tell the server the view is gone. The
      // backend PTY keeps running (detach ≠ close), matching the Tauri handle.
      // Untracking also disables reconnection re-attach for this session.
      void ws.detachTerminal(sessionId);
    },
    async close(): Promise<void> {
      await ws.closeTerminalSession(sessionId);
    },
  };
}

export class HttpSystemGateway implements SystemGateway {
  constructor(
    private readonly http: HttpInvoker,
    private readonly ws: WsLiveClient,
  ) {}

  health(note?: string): Promise<HealthReport> {
    return this.http.invoke<HealthReport>("health", {
      request: note === undefined ? null : { note },
    });
  }

  async onDomainEvent(handler: (event: DomainEvent) => void): Promise<Unsubscribe> {
    // The low-frequency domain-event stream rides the same WS connection
    // (`event.domain`), replacing Tauri `listen("domain://event")`.
    await this.ws.ensureConnected();
    this.ws.setDomainEventHandler(handler);
    return () => this.ws.clearDomainEventHandler();
  }

  pickFolder(): Promise<string | null> {
    // Desktop-only: the native OS dialog has no web equivalent. A server-side
    // folder browser would be its own lot (flagged in the F1 report).
    return unsupportedOnWeb("Native folder picker");
  }

  onAppExitWorkGuard(
    _handler: (state: AppExitWorkGuardState) => void,
  ): Promise<Unsubscribe> {
    // Desktop-only (ticket #83): there is no interceptable native window to
    // guard on the web client, so this never fires — an inert unsubscribe,
    // not a rejection, so callers can subscribe unconditionally.
    return Promise.resolve(() => {});
  }

  confirmAppExit(): Promise<void> {
    return unsupportedOnWeb("Confirming an app exit");
  }
}

export class HttpTerminalGateway implements TerminalGateway {
  constructor(private readonly ws: WsLiveClient) {}

  async openTerminal(
    options: OpenTerminalOptions,
    onData: (bytes: Uint8Array) => void,
  ): Promise<TerminalHandle> {
    // B5: `terminal.open` carries no projectId; `cwd` is a server-validated path.
    // A fresh open has empty scrollback (the view does not repaint on open).
    const res = await this.ws.openTerminal({
      cwd: options.cwd,
      rows: options.rows,
      cols: options.cols,
      nodeId: options.nodeId ?? null,
      onData,
    });
    return makeWsTerminalHandle(res.sessionId, this.ws);
  }

  async reattach(
    sessionId: string,
    onData: (bytes: Uint8Array) => void,
  ): Promise<ReattachResult> {
    // The view repaints the returned scrollback itself (TerminalView contract);
    // the client does not also push it through `onData` on the initial attach.
    const res = await this.ws.attachTerminal({ sessionId, onData });
    return {
      handle: makeWsTerminalHandle(res.sessionId, this.ws),
      scrollback: res.scrollback,
    };
  }

  async closeTerminal(sessionId: string): Promise<void> {
    await this.ws.closeTerminalSession(sessionId);
  }
}

export class HttpAgentGateway implements AgentGateway {
  constructor(
    private readonly http: HttpInvoker,
    private readonly ws: WsLiveClient,
  ) {}

  listAgents(projectId: string): Promise<Agent[]> {
    return this.http.invoke<Agent[]>("list_agents", { projectId });
  }
  listLiveAgents(projectId: string): Promise<LiveAgent[]> {
    return this.http.invoke<LiveAgent[]>("list_live_agents", { projectId });
  }
  listResumableAgents(projectId: string): Promise<ResumableAgent[]> {
    return this.http
      .invoke<{ resumable: ResumableAgent[] }>("list_resumable_agents", { projectId })
      .then((res) => res.resumable);
  }
  attachLiveAgent(projectId: string, agentId: string, nodeId: string): Promise<LiveAgent> {
    return this.http.invoke<LiveAgent>("attach_live_agent", { request: { projectId, agentId, nodeId } });
  }
  stopLiveAgent(projectId: string, agentId: string): Promise<StoppedLiveAgent> {
    return this.http.invoke<StoppedLiveAgent>("stop_live_agent", { request: { projectId, agentId } });
  }
  createAgent(projectId: string, input: CreateAgentInput): Promise<Agent> {
    return this.http.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 }> {
    return this.http.invoke<{ agent: Agent; relaunchedSession?: TerminalSession }>("change_agent_profile", {
      request: { projectId, agentId, profileId, rows, cols },
    });
  }
  readContext(projectId: string, agentId: string): Promise<string> {
    return this.http.invoke<string>("read_agent_context", { projectId, agentId });
  }
  async updateContext(projectId: string, agentId: string, content: string): Promise<void> {
    await this.http.invoke("update_agent_context", { request: { projectId, agentId, content } });
  }
  async deleteAgent(projectId: string, agentId: string): Promise<void> {
    await this.http.invoke("delete_agent", { projectId, agentId });
  }
  inspectConversation(
    projectId: string,
    agentId: string,
    conversationId: string,
  ): Promise<ConversationDetails> {
    return this.http.invoke<ConversationDetails>("inspect_conversation", {
      request: { projectId, agentId, conversationId },
    });
  }

  async launchAgent(
    projectId: string,
    agentId: string,
    options: OpenTerminalOptions,
    onData: (bytes: Uint8Array) => void,
  ): Promise<TerminalHandle> {
    // B6: a raw-CLI agent streams over the same PTY channel as a terminal. The
    // WS client tracks the session (reconnection/replay/status parity with F3);
    // the unified `terminal.attached` ack yields the sessionId + the conversation
    // id minted by this launch. Structured agents are refused server-side with
    // `UNSUPPORTED` — the rejection propagates to the caller (a clear cell error,
    // no crash). A fresh launch has empty scrollback (no repaint on open).
    const res = await this.ws.launchAgent({
      projectId,
      agentId,
      nodeId: options.nodeId ?? null,
      rows: options.rows,
      cols: options.cols,
      conversationId: options.conversationId ?? null,
      onData,
    });
    return makeWsTerminalHandle(res.sessionId, this.ws, res.assignedConversationId);
  }

  async reattach(
    sessionId: string,
    onData: (bytes: Uint8Array) => void,
  ): Promise<ReattachResult> {
    // Agent sessions re-attach through the same `terminal.attach` mechanics as a
    // terminal — no relaunch, bounded scrollback replayed for the view to repaint.
    const res = await this.ws.attachTerminal({ sessionId, onData });
    return {
      handle: makeWsTerminalHandle(res.sessionId, this.ws),
      scrollback: res.scrollback,
    };
  }
}

export class HttpTicketGateway implements TicketGateway {
  constructor(
    private readonly http: HttpInvoker,
    private readonly ws: WsLiveClient,
  ) {}

  create(projectId: string, input: CreateTicketInput): Promise<Ticket> {
    return this.http.invoke<Ticket>("ticket_create", { request: { projectId, ...input } });
  }
  read(projectId: string, ref: string, includeCarnet = false): Promise<Ticket> {
    return this.http.invoke<Ticket>("ticket_read", { request: { projectId, ref, includeCarnet } });
  }
  list(projectId: string, query?: TicketListQuery): Promise<TicketList> {
    const { statuses, priorities, ...rest } = query ?? {};
    return this.http.invoke<TicketList>("ticket_list", {
      request: { projectId, statuses: statuses ?? [], priorities: priorities ?? [], ...rest },
    });
  }
  update(projectId: string, ref: string, input: UpdateTicketInput): Promise<Ticket> {
    return this.http.invoke<Ticket>("ticket_update", { request: { projectId, ref, ...input } });
  }
  async delete(projectId: string, ref: string): Promise<void> {
    await this.http.invoke<void>("ticket_delete", { request: { projectId, ref } });
  }
  readCarnet(projectId: string, ref: string): Promise<TicketCarnet> {
    return this.http.invoke<TicketCarnet>("ticket_read_carnet", { request: { projectId, ref } });
  }
  updateCarnet(projectId: string, ref: string, carnet: string, expectedVersion: number): Promise<Ticket> {
    return this.http.invoke<Ticket>("ticket_update_carnet", {
      request: { projectId, ref, carnet, expectedVersion },
    });
  }
  link(
    projectId: string,
    ref: string,
    targetRef: string,
    kind: TicketLinkKind,
    expectedVersion: number,
  ): Promise<Ticket> {
    return this.http.invoke<Ticket>("ticket_link", {
      request: { projectId, ref, targetRef, kind, expectedVersion },
    });
  }
  unlink(
    projectId: string,
    ref: string,
    targetRef: string,
    expectedVersion: number,
    kind?: TicketLinkKind,
  ): Promise<Ticket> {
    return this.http.invoke<Ticket>("ticket_unlink", {
      request: { projectId, ref, targetRef, expectedVersion, kind },
    });
  }
  assign(
    projectId: string,
    ref: string,
    agentId: string,
    assigned: boolean,
    expectedVersion: number,
  ): Promise<Ticket> {
    return this.http.invoke<Ticket>("ticket_assign", {
      request: { projectId, ref, agentId, assigned, expectedVersion },
    });
  }
  async listSprints(projectId: string): Promise<Sprint[]> {
    const list = await this.http.invoke<{ items: Sprint[] }>("sprint_list", { request: { projectId } });
    return list.items;
  }
  setTicketSprint(
    projectId: string,
    ref: string,
    sprintId: string | null,
    expectedVersion: number,
  ): Promise<Ticket> {
    if (sprintId === null) {
      return this.http.invoke<Ticket>("ticket_unassign_sprint", { request: { projectId, ref, expectedVersion } });
    }
    return this.http.invoke<Ticket>("ticket_assign_sprint", {
      request: { projectId, ref, sprintId, expectedVersion },
    });
  }
  createSprint(projectId: string, name: string): Promise<Sprint> {
    return this.http.invoke<Sprint>("sprint_create", { request: { projectId, name } });
  }
  renameSprint(projectId: string, sprintId: string, name: string, expectedVersion: number): Promise<Sprint> {
    return this.http.invoke<Sprint>("sprint_rename", { request: { projectId, sprintId, name, expectedVersion } });
  }
  async reorderSprints(projectId: string, orderedIds: string[]): Promise<Sprint[]> {
    const list = await this.http.invoke<{ items: Sprint[] }>("sprint_reorder", {
      request: { projectId, orderedIds },
    });
    return list.items;
  }
  async deleteSprint(projectId: string, sprintId: string): Promise<void> {
    await this.http.invoke<void>("sprint_delete", { request: { projectId, sprintId } });
  }
  openTicketChat(projectId: string, issueRef: string, profileId: string): Promise<TicketChat> {
    return this.http.invoke<TicketChat>("open_ticket_chat", { request: { projectId, issueRef, profileId } });
  }
  async closeTicketChat(projectId: string, issueRef: string): Promise<void> {
    await this.http.invoke<void>("close_ticket_chat", { request: { projectId, issueRef } });
  }

  async sendTicketChat(
    sessionId: string,
    message: string,
    onChunk: (chunk: ReplyChunk) => void,
  ): Promise<void> {
    // Structured assistant reply stream over WS (`chat.*`, B0). Skeleton: route
    // this session's `chat.output` chunks to `onChunk` until the `final` chunk.
    // TODO(F3/B5): the WsLiveClient currently routes PTY output + domain events;
    // a per-session chat sink is added alongside the terminal sink in F3.
    const ack = await this.ws.send("chat.send", { sessionId, prompt: message });
    const payload = ack.payload as unknown as ChatOutputPayload | undefined;
    if (payload && payload.chunk) onChunk(payload.chunk as ReplyChunk);
  }
}
