import type { Hono, Context } from 'hono';
import type { TransferEngine } from '../engine.js';
import { TransferProtocolError } from '../errors.js';

/**
 * Auth contract for incoming chunk POSTs / control GETs. Verifies the
 * signature attached by the client's `TransferAuthenticator` and returns
 * the resolved sender address.
 */
export interface TransferRouteAuthenticator {
  verifyChunk(args: {
    request: Request;
    streamId: string;
    laneId: number;
    seq: bigint;
    bodyHash: Uint8Array;
  }): Promise<{ senderAddress: string }>;

  verifyControl(args: {
    request: Request;
    streamId: string;
    method: string;
    path: string;
  }): Promise<{ senderAddress: string }>;
}

export interface TransferRouteOptions {
  /** Maximum chunk body size in bytes. Default 16 MiB. */
  maxChunkBytes?: number;
  /** Server-side authenticator. Defaults to a permissive one for trusted/local nets. */
  authenticator?: TransferRouteAuthenticator;
}

/**
 * Permissive authenticator: extracts sender address from the
 * `X-Shade-Sender-Address` header without verification. Suitable ONLY for
 * trusted/local network testing; the SDK's M-Stream-5 integration replaces
 * this with an Ed25519-verifying implementation.
 */
export const PermissiveAuthenticator: TransferRouteAuthenticator = {
  async verifyChunk({ request }) {
    const senderAddress = request.headers.get('X-Shade-Sender-Address');
    if (senderAddress === null || senderAddress === '') {
      throw new TransferProtocolError('Missing X-Shade-Sender-Address header');
    }
    return { senderAddress };
  },
  async verifyControl({ request }) {
    const senderAddress = request.headers.get('X-Shade-Sender-Address');
    if (senderAddress === null || senderAddress === '') {
      throw new TransferProtocolError('Missing X-Shade-Sender-Address header');
    }
    return { senderAddress };
  },
};

const DEFAULT_MAX_CHUNK_BYTES = 16 * 1024 * 1024 + 1024;

/**
 * Mount the transfer-receive routes on a Hono router. Returns the same
 * Hono instance for fluent composition. The consumer mounts under any
 * base path (e.g. `app.route('/shade', createTransferRoutes(engine))`).
 *
 * Hono is a peer-dep so non-server consumers can omit it.
 */
export async function createTransferRoutes(
  engine: TransferEngine,
  options: TransferRouteOptions = {},
): Promise<Hono> {
  const { Hono: HonoCtor } = (await import('hono')) as { Hono: new () => Hono };
  const app = new HonoCtor();
  const auth = options.authenticator ?? PermissiveAuthenticator;
  const maxBytes = options.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;

  app.get('/v1/transfer/health', (c) => c.json({ ok: true }));

  app.post('/v1/transfer/:streamId/chunk', async (c) => {
    const streamId = c.req.param('streamId');
    const laneIdRaw = c.req.header('X-Shade-Lane-Id');
    const seqRaw = c.req.header('X-Shade-Seq');
    if (laneIdRaw === undefined || seqRaw === undefined) {
      return c.json({ error: 'missing X-Shade-Lane-Id or X-Shade-Seq' }, 400);
    }
    const laneId = Number(laneIdRaw);
    const seq = BigInt(seqRaw);
    if (!Number.isInteger(laneId) || laneId < 0) {
      return c.json({ error: 'invalid lane id' }, 400);
    }

    const contentLength = c.req.header('content-length');
    if (contentLength !== undefined && Number(contentLength) > maxBytes) {
      return c.json({ error: `chunk exceeds maxChunkBytes (${maxBytes})` }, 413);
    }
    const ab = await c.req.arrayBuffer();
    if (ab.byteLength > maxBytes) {
      return c.json({ error: `chunk exceeds maxChunkBytes (${maxBytes})` }, 413);
    }
    const body = new Uint8Array(ab);
    const bodyHash = new Uint8Array(
      await globalThis.crypto.subtle.digest('SHA-256', ab),
    );

    let senderAddress: string;
    try {
      const result = await auth.verifyChunk({
        request: c.req.raw,
        streamId,
        laneId,
        seq,
        bodyHash,
      });
      senderAddress = result.senderAddress;
    } catch (err) {
      return errorResponse(c, err);
    }

    try {
      const ack = await engine.receiveChunk(senderAddress, streamId, laneId, seq, body);
      return c.json(ack);
    } catch (err) {
      return errorResponse(c, err);
    }
  });

  app.get('/v1/transfer/:streamId/state', async (c) => {
    const streamId = c.req.param('streamId');
    let senderAddress: string;
    try {
      const result = await auth.verifyControl({
        request: c.req.raw,
        streamId,
        method: 'GET',
        path: c.req.path,
      });
      senderAddress = result.senderAddress;
    } catch (err) {
      return errorResponse(c, err);
    }

    const state = await engine.getResumeState(senderAddress, streamId);
    if (state === null) return c.json({ error: 'no state' }, 404);
    return c.json(state);
  });

  return app;
}

function errorResponse(c: Context, err: unknown): Response {
  const message = err instanceof Error ? err.message : String(err);
  const code =
    err instanceof Error && (err as unknown as { code?: unknown }).code !== undefined
      ? String((err as unknown as { code: unknown }).code)
      : 'UNKNOWN';
  let status = 500;
  if (code === 'SHADE_TRANSFER_PROTOCOL') status = 400;
  if (code === 'SHADE_VALIDATION') status = 400;
  if (code === 'SHADE_UNAUTHORIZED') status = 401;
  if (code === 'SHADE_INVALID_SIGNATURE') status = 401;
  if (code === 'SHADE_STREAM_REPLAY') status = 409;
  if (code === 'SHADE_STREAM_OUT_OF_ORDER') status = 409;
  return c.json({ error: message, code }, status as 400 | 401 | 409 | 500);
}
