import {
  GatewayError,
  helloPayloadSchema,
  parse,
  type ComputerRequest,
  type Frame,
  type GatewayErrorBody,
} from "./protocol.js";
import type { GatewayRegistry, GatewaySocket, Logger } from "./registry.js";
import type { OpencodeProbe } from "../../opencode.js";
import type { NetworkId } from "../../auth.js";

export type ConnectionDeps = {
  socket: GatewaySocket;
  registry: GatewayRegistry;
  probe: OpencodeProbe;
  resolveNetwork: (secret: string | undefined) => NetworkId | null;
  log: Logger;
};

export type Connection = {
  onMessage(raw: unknown): Promise<void>;
  onClose(code?: number, reason?: string): void;
};

type State = { networkId: NetworkId; traposId: string };

function toText(raw: unknown): string {
  if (typeof raw === "string") return raw;
  if (Buffer.isBuffer(raw)) return raw.toString("utf8");
  if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString("utf8");
  if (Array.isArray(raw)) return Buffer.concat(raw as Buffer[]).toString("utf8");
  return "";
}

// One per WebSocket connection. Owns the pre-/post-hello state machine. The only
// event that ever earns an error reply is `computer_request`; everything the gateway
// shouldn't receive is dropped (with a debug log). `unauthorized` is the only close.
export function createConnection(deps: ConnectionDeps): Connection {
  const { socket, registry, probe, resolveNetwork, log } = deps;
  let state: State | null = null;

  function reply(
    req: ComputerRequest,
    ok: boolean,
    payload?: Record<string, unknown>,
    error?: GatewayErrorBody,
  ): void {
    const frame: Record<string, unknown> = {
      event: "server_response",
      traposReceiverId: req.traposSenderId,
      messageId: req.messageId,
      type: req.type,
      ok,
    };
    if (payload) frame.payload = payload;
    if (error) frame.error = error;
    socket.send(JSON.stringify(frame));
  }

  function handleHello(req: ComputerRequest): void {
    const parsed = helloPayloadSchema.safeParse(req.payload);
    if (!parsed.success) {
      log.debug({ reason: "malformed" }, "hello with missing/empty traposId");
      reply(req, false, undefined, { code: "not_authenticated", message: "missing traposId" });
      return;
    }

    const { traposId, secret } = parsed.data;
    const networkId = resolveNetwork(secret);
    if (networkId === null) {
      log.warn({ traposId, reason: "unauthorized" }, "gateway hello rejected");
      reply(req, false, undefined, { code: "unauthorized", message: "invalid secret" });
      socket.close();
      return;
    }

    registry.register(networkId, traposId, socket);
    state = { networkId, traposId };
    log.info({ networkId, traposId }, "gateway hello accepted");
    reply(req, true, { networkId });
  }

  async function handleProbeGateway(req: ComputerRequest): Promise<void> {
    log.debug({ networkId: state?.networkId, traposId: state?.traposId, messageId: req.messageId }, "probing gateway dependencies");
    const result = await probe.probe();
    const payload: Record<string, unknown> = { serverOk: true, opencodeOk: result.ok };
    if (!result.ok && result.detail !== undefined) {
      payload.opencodeDetail = result.detail;
    }
    reply(req, true, payload);
  }

  function dispatchPreHello(frame: Frame): Promise<void> | void {
    if (frame.event === "computer_request") {
      if (frame.type === "hello") {
        return handleHello(frame);
      }
      reply(frame, false, undefined, {
        code: "not_authenticated",
        message: "hello required before any other request",
      });
      return;
    }
    log.debug({ event: frame.event, reason: "pre_hello" }, "dropping pre-hello frame");
  }

  async function dispatchPostHello(frame: Frame, current: State): Promise<void> {
    if (frame.event === "computer_request") {
      if (frame.type === "hello") {
        log.debug({ networkId: current.networkId, traposId: current.traposId, reason: "duplicate_hello" }, "ignoring duplicate hello on live connection");
        return;
      }
      if (frame.type === "probe_server") {
        await handleProbeGateway(frame);
        return;
      }
      log.debug({ networkId: current.networkId, traposId: current.traposId, messageId: frame.messageId, type: frame.type, reason: "unknown_type" }, "rejecting unknown computer_request type");
      reply(frame, false, undefined, { code: "unknown_type", message: `unknown type: ${frame.type}` });
      return;
    }
    if (frame.event === "computer_response") {
      // resolve a pending server_request
      registry.resolveResponse(socket, current.networkId, current.traposId, frame);
    }
  }

  async function onMessage(raw: unknown): Promise<void> {
    let frame: Frame;
    try {
      frame = parse(toText(raw));
    } catch {
      log.debug({ reason: "malformed" }, "dropping malformed frame");
      return;
    }

    // The gateway never legitimately receives gateway_* events from a client.
    if (frame.event === "server_request" || frame.event === "server_response") {
      log.debug({ event: frame.event, reason: "wrong_direction" }, "dropping wrong-direction frame");
      return;
    }

    if (state === null) {
      await dispatchPreHello(frame);
      return;
    }
    await dispatchPostHello(frame, state);
  }

  function onClose(code?: number, reason?: string): void {
    if (state) {
      log.info({ networkId: state.networkId, traposId: state.traposId, closeCode: code, closeReason: reason }, "gateway connection closed");
      registry.unregister(state.networkId, state.traposId, socket);
      state = null;
      return;
    }
    log.info({ closeCode: code, closeReason: reason }, "gateway connection closed before hello");
  }

  return { onMessage, onClose };
}

// Re-export so callers can detect coded errors without importing protocol directly.
export { GatewayError };
