/**
 * Tauri adapter for {@link TerminalGateway} (L3). The single place that uses a
 * {@link Channel} for the high-frequency PTY byte stream and `invoke()` for the
 * control commands. Components reach it exclusively through the port.
 *
 * Flow (ARCHITECTURE §2 "Tauri Channels"):
 * - `openTerminal` creates a `Channel<number[]>`, passes it to the
 *   `open_terminal` command, and forwards every chunk to `onData` as a
 *   `Uint8Array`. The backend pumps PTY output into that channel via the
 *   `PtyBridge`.
 * - keystrokes go out through `write_terminal`, resize through
 *   `resize_terminal`, teardown through `close_terminal`.
 *
 * Commands and payload keys are camelCase, matching the backend DTO convention.
 */

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

import type {
  OpenTerminalOptions,
  ReattachResult,
  TerminalGateway,
  TerminalHandle,
} from "@/ports";

/** Wire shape returned by the `open_terminal` command. */
interface OpenTerminalResponse {
  sessionId: string;
  cwd: string;
  rows: number;
  cols: number;
}

/** Wire shape returned by the `reattach_terminal` command. */
interface ReattachResponse {
  sessionId: string;
  scrollback: number[];
}

/**
 * Builds a {@link TerminalHandle} over a session and its local output
 * {@link Channel}. `detach` stops the channel from delivering further bytes (the
 * view is gone) without touching the backend PTY; `close` kills the PTY.
 *
 * Shared by `openTerminal` and `reattach` so both produce identical handles.
 */
export function makeTerminalHandle(
  sessionId: string,
  channel: Channel<number[]>,
): TerminalHandle {
  // Serialise writes per handle. Each `write` chains its `invoke` after the
  // previous one resolves, so the order in which `write` is *called* is the
  // order the bytes reach the backend stdin — regardless of how Tauri's IPC
  // schedules concurrent `invoke`s. Without this, typing/pasting fast puts
  // several `invoke`s in flight at once and they can land out of order, garbling
  // the CLI input (e.g. "le même" → "le mO é J é IDEIDE…").
  //
  // The chain only sequences *ordering*; a failed write is swallowed for the
  // purpose of the chain (logged, then the chain continues) so one rejected
  // promise never blocks every subsequent keystroke. The error is still
  // surfaced to the caller of that specific `write` via its own promise.
  let chain: Promise<void> = Promise.resolve();

  return {
    sessionId,
    write(data: Uint8Array): Promise<void> {
      const run = chain.then(async () => {
        logTerminalWrite("start", sessionId, data);
        await invoke<void>("write_terminal", {
          request: { sessionId, data: Array.from(data) },
        });
        logTerminalWrite("ok", sessionId, data);
      });
      // Keep the chain alive even if this write rejects: the next write must
      // still run. Swallow the error on the *chain* copy only — `run` keeps the
      // rejection so the caller can observe it.
      chain = run.catch(() => {});
      return run;
    },
    async resize(rows: number, cols: number): Promise<void> {
      await invoke("resize_terminal", {
        request: { sessionId, rows, cols },
      });
    },
    detach(): void {
      // Drop the local subscription: the backend PTY keeps running, but this
      // view stops receiving output. A later `reattach` re-wires a fresh channel.
      channel.onmessage = () => {};
    },
    async close(): Promise<void> {
      await invoke("close_terminal", { sessionId });
    },
  };
}

function logTerminalWrite(phase: "start" | "ok", sessionId: string, data: Uint8Array): void {
  if (data.length === 1 && data[0] >= 0x20 && data[0] !== 0x7f) return;
  console.info("[terminal-write]", phase, {
    sessionId,
    bytes: data.length,
    control: describeBytes(data),
  });
}

function describeBytes(data: Uint8Array): string {
  if (data.length === 0) return "<empty>";
  if (data.length > 16) return "<bulk>";
  return Array.from(data)
    .map((byte) => {
      switch (byte) {
        case 0x0d:
          return "\\r";
        case 0x0a:
          return "\\n";
        case 0x7f:
          return "\\x7f";
        default:
          if (byte < 0x20) return `\\x${byte.toString(16).padStart(2, "0")}`;
          return String.fromCharCode(byte);
      }
    })
    .join("");
}

export class TauriTerminalGateway implements TerminalGateway {
  async openTerminal(
    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<OpenTerminalResponse>("open_terminal", {
      request: { cwd: options.cwd, rows: options.rows, cols: options.cols },
      onOutput: channel,
    });

    return makeTerminalHandle(res.sessionId, channel);
  }

  async reattach(
    sessionId: string,
    onData: (bytes: Uint8Array) => void,
  ): Promise<ReattachResult> {
    const channel = new Channel<number[]>();
    channel.onmessage = (chunk) => onData(Uint8Array.from(chunk));

    const res = await invoke<ReattachResponse>("reattach_terminal", {
      sessionId,
      onOutput: channel,
    });

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

  async closeTerminal(sessionId: string): Promise<void> {
    // Kills the PTY by id (the backend `close_terminal` command). Used when a
    // cell's agent changes and the old PTY must be torn down even though its
    // owning view only ever detaches.
    await invoke("close_terminal", { sessionId });
  }
}
