/**
 * Request-response RPC route for `@shade/files`.
 *
 * Mounts a single `POST /rpc` Hono endpoint that accepts an encrypted
 * `RpcRequest` envelope, dispatches it through the file handler, and
 * returns the encrypted `RpcResponse` (or `RpcError`) envelope in the
 * SAME HTTP response.
 *
 * This is the browser-friendly transport: the server never needs to
 * make outbound calls back to the client, so a browser tab — which
 * cannot host an HTTP server — can fully consume `@shade/files`.
 *
 * ### Wire contract
 *
 * Request:
 * ```
 * POST <mount>/rpc HTTP/1.1
 * Content-Type: application/octet-stream
 * X-Shade-Sender-Address: <peer address>
 *
 * <wire-encoded ShadeEnvelope (0x01 PreKeyMessage or 0x02 RatchetMessage)
 *  containing JSON-encoded RpcRequest>
 * ```
 *
 * Response (success):
 * ```
 * 200 OK
 * Content-Type: application/octet-stream
 *
 * <wire-encoded ShadeEnvelope (0x02 RatchetMessage)
 *  containing JSON-encoded RpcResponse | RpcError>
 * ```
 *
 * Response (transport-level failure — no session, undecryptable, etc.):
 * ```
 * 4xx
 * Content-Type: application/json
 *
 * { "error": "..." }
 * ```
 *
 * ### Symmetry with shade-auth-middleware
 *
 * The shape mirrors `@shade/server`'s shade-auth-middleware: an
 * encrypted envelope rides the request body, the server decrypts via
 * the existing ratchet session, performs the protected operation,
 * and returns an encrypted envelope in the response. No bidirectional
 * channel required.
 *
 * @see {@link createFilesHttpClient} for the matching browser client.
 */
import { Hono } from 'hono';
import { decodeEnvelope, encodeEnvelope } from '@shade/proto';
import type { ShadeBridge } from '../integration/shade-bridge.js';
import {
  encodeEnvelope as encodeRpcEnvelope,
  tryParseEnvelope,
} from '../protocol/envelope-codec.js';
import type { FileHandler } from './handler.js';
import type { RpcError, RpcRequest, RpcResponse } from '../schemas/envelope.js';
import { KIND_ERROR_V1 } from '../protocol/kinds.js';

export interface FilesRpcRouteOptions {
  /**
   * Maximum request body size in bytes. Default 1 MiB. Inline payloads
   * are capped at 256 KiB by the protocol; the headroom is for
   * custom-op payloads and base64 inflation.
   */
  maxBodyBytes?: number;
  /**
   * Allow this server to accept the very first message (PreKeyMessage,
   * `0x01`) over the RPC route. Disabled by default — most browser
   * clients establish a session via `shade.initSessionFromBundle`
   * before the first RPC. Enable when you want the RPC route to also
   * be the X3DH carrier (uncommon but supported).
   */
  acceptFirstMessage?: boolean;
}

const DEFAULT_MAX_BODY_BYTES = 1 * 1024 * 1024;

/**
 * Build a Hono app with a single `POST /rpc` route. Mount under any
 * base path: `app.route('/api/v1/shade-files', shade.files.rpcRoute())`.
 *
 * The `handler` must already be attached (typically via
 * `shade.files.serve(handlerConfig)`); this route only ships the
 * transport — it does not register a new file handler.
 */
export function createFilesRpcRoute(
  shade: ShadeBridge,
  handler: FileHandler,
  options: FilesRpcRouteOptions = {},
): Hono {
  const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
  const app = new Hono();

  app.post('/rpc', async (c) => {
    const senderAddress = c.req.header('X-Shade-Sender-Address');
    if (senderAddress === undefined || senderAddress === '') {
      return c.json({ error: 'missing X-Shade-Sender-Address header' }, 400);
    }

    const contentLengthHeader = c.req.header('Content-Length');
    if (contentLengthHeader !== undefined) {
      const contentLength = Number.parseInt(contentLengthHeader, 10);
      if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
        return c.json(
          { error: `body exceeds maxBodyBytes (${contentLength} > ${maxBodyBytes})` },
          413,
        );
      }
    }

    let bodyBytes: Uint8Array;
    try {
      const ab = await c.req.arrayBuffer();
      if (ab.byteLength > maxBodyBytes) {
        return c.json(
          { error: `body exceeds maxBodyBytes (${ab.byteLength} > ${maxBodyBytes})` },
          413,
        );
      }
      bodyBytes = new Uint8Array(ab);
    } catch (err) {
      return c.json({ error: `failed to read request body: ${(err as Error).message}` }, 400);
    }

    if (bodyBytes.byteLength === 0) {
      return c.json({ error: 'empty request body' }, 400);
    }

    // Decode the wire envelope. `decodeEnvelope` handles both `0x01`
    // PreKeyMessage and `0x02` RatchetMessage shapes.
    let plaintext: string;
    try {
      const envelope = decodeEnvelope(bodyBytes);
      // First-message gate: only allow `prekey` envelopes when the
      // operator has explicitly opted in.
      if (options.acceptFirstMessage !== true && envelope.type === 'prekey') {
        return c.json(
          {
            error:
              'PreKeyMessage envelopes are not accepted on this RPC route — establish the session first via shade.initSessionFromBundle, or set acceptFirstMessage: true',
          },
          400,
        );
      }
      plaintext = await shade.receive(senderAddress, envelope);
    } catch (err) {
      // Decryption failure — could be no session, corrupted envelope,
      // or sender address mismatch. Treat as 401 since the envelope is
      // self-authenticating: a valid sender would decrypt cleanly.
      return c.json({ error: `decrypt failed: ${(err as Error).message}` }, 401);
    }

    // Parse the plaintext as an RpcRequest.
    const classified = tryParseEnvelope(plaintext);
    if (classified === null) {
      return c.json({ error: 'plaintext is not a valid @shade/files envelope' }, 400);
    }
    if (classified.kind !== 'request') {
      // Cancel envelopes are silently dropped — RPC route is request/
      // response only. Cancellation across HTTP is achieved via
      // AbortController on the client side, not protocol-level.
      if (classified.kind === 'cancel') {
        handler.handleCancel(senderAddress, classified.envelope);
        // No response body — the cancel was best-effort.
        return new Response(null, { status: 204 });
      }
      return c.json(
        { error: `unexpected envelope kind on RPC route: ${classified.kind}` },
        400,
      );
    }

    const request: RpcRequest = classified.envelope;

    // Dispatch through the file handler.
    let result: RpcResponse | RpcError;
    try {
      result = await handler.handleRequest(senderAddress, request);
    } catch (err) {
      // Should never happen — handler.handleRequest catches its own
      // errors and returns RpcError. If it didn't, that's a bug; emit
      // a generic transport-level RpcError so the client can surface
      // it deterministically.
      result = {
        kind: KIND_ERROR_V1,
        id: request.id,
        error: {
          code: 'INTERNAL',
          message: `handler raised: ${(err as Error).message}`,
        },
      };
    }

    // Encrypt the response and return it as wire bytes.
    let responseBytes: Uint8Array;
    try {
      const responsePlaintext = encodeRpcEnvelope(result);
      const responseEnvelope = await shade.send(senderAddress, responsePlaintext);
      responseBytes = encodeEnvelope(responseEnvelope);
    } catch (err) {
      return c.json(
        { error: `failed to encrypt response: ${(err as Error).message}` },
        500,
      );
    }

    return new Response(new Blob([responseBytes as unknown as ArrayBuffer]), {
      status: 200,
      headers: { 'Content-Type': 'application/octet-stream' },
    });
  });

  return app;
}
