import type { Context, Middleware } from "@wrnexus/core";
import { open, seal, sealedKeyId, type EncryptionKeyring } from "./keyring.ts";

export const ENCRYPTED_HTTP_CONTENT_TYPE = "application/wrn+json";
export const ENCRYPTED_HTTP_VERSION = "wrn-http-1";

const REQUEST_ID = /^[A-Za-z0-9._:-]{8,128}$/;
const KEY_ID = /^[A-Za-z0-9._-]{1,64}$/;

export interface EncryptedHttpEnvelope {
  version: typeof ENCRYPTED_HTTP_VERSION;
  keyId: string;
  requestId: string;
  timestamp: number;
  ciphertext: string;
}

interface EncryptedHttpPayload<T> {
  method: string;
  path: string;
  requestId: string;
  timestamp: number;
  body: T;
}

export interface ReplayStore {
  consume(id: string, expiresAt: number): boolean | Promise<boolean>;
}

export interface EncryptedHttpOptions {
  keyring: EncryptionKeyring;
  maxAgeMs?: number;
  maxBodyBytes?: number;
  replayStore?: ReplayStore;
  now?: () => number;
  /** Require the clear request-id header used to bind encrypted responses. Default true. */
  requireRequestIdHeader?: boolean;
}

export interface DecryptedHttpBody<T> {
  body: T;
  requestId: string;
  timestamp: number;
  keyId: string;
}

function createRequestId(): string {
  if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
  const bytes = new Uint8Array(16);
  crypto.getRandomValues(bytes);
  return [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
}

function normalizePath(input: string | URL): string {
  const url = input instanceof URL ? input : new URL(input, "https://wrnexus.local");
  return `${url.pathname}${url.search}`;
}

function methodOf(method: string | undefined): string {
  return (method ?? "POST").toUpperCase();
}

function parseEnvelope(value: unknown): EncryptedHttpEnvelope {
  if (!value || typeof value !== "object") throw new Error("WRN-ENCRYPTION-HTTP-ENVELOPE");
  const envelope = value as Partial<EncryptedHttpEnvelope>;
  if (
    envelope.version !== ENCRYPTED_HTTP_VERSION ||
    typeof envelope.keyId !== "string" ||
    !KEY_ID.test(envelope.keyId) ||
    typeof envelope.requestId !== "string" ||
    !REQUEST_ID.test(envelope.requestId) ||
    typeof envelope.timestamp !== "number" ||
    !Number.isFinite(envelope.timestamp) ||
    envelope.timestamp <= 0 ||
    typeof envelope.ciphertext !== "string" ||
    envelope.ciphertext.length < 16 ||
    sealedKeyId(envelope.ciphertext) !== envelope.keyId
  ) {
    throw new Error("WRN-ENCRYPTION-HTTP-ENVELOPE");
  }
  return envelope as EncryptedHttpEnvelope;
}

function requestHeaderId(request: Request, required: boolean): string | undefined {
  const id = request.headers.get("x-wrn-request-id")?.trim();
  if (!id) {
    if (required) throw new Error("WRN-ENCRYPTION-HTTP-REQUEST-ID");
    return undefined;
  }
  if (!REQUEST_ID.test(id)) throw new Error("WRN-ENCRYPTION-HTTP-REQUEST-ID");
  return id;
}

export function createMemoryReplayStore(now: () => number = Date.now): ReplayStore {
  const seen = new Map<string, number>();
  return {
    consume(id, expiresAt) {
      const current = now();
      for (const [key, expiry] of seen) if (expiry <= current) seen.delete(key);
      if (seen.has(id)) return false;
      seen.set(id, expiresAt);
      return true;
    },
  };
}

export async function encryptHttpBody<T>(
  body: T,
  input: {
    keyring: EncryptionKeyring;
    method?: string;
    url: string | URL;
    requestId?: string;
    timestamp?: number;
  },
): Promise<EncryptedHttpEnvelope> {
  const id = input.requestId ?? createRequestId();
  if (!REQUEST_ID.test(id)) throw new TypeError("WRN-ENCRYPTION-HTTP-REQUEST-ID");
  const timestamp = input.timestamp ?? Date.now();
  if (!Number.isFinite(timestamp) || timestamp <= 0) {
    throw new TypeError("WRN-ENCRYPTION-HTTP-TIMESTAMP");
  }
  const payload: EncryptedHttpPayload<T> = {
    method: methodOf(input.method),
    path: normalizePath(input.url),
    requestId: id,
    timestamp,
    body,
  };
  const ciphertext = await seal(JSON.stringify(payload), input.keyring);
  return {
    version: ENCRYPTED_HTTP_VERSION,
    keyId: input.keyring.active().id,
    requestId: id,
    timestamp,
    ciphertext,
  };
}

export async function decryptHttpBody<T>(
  value: unknown,
  input: {
    keyring: EncryptionKeyring;
    method?: string;
    url: string | URL;
    maxAgeMs?: number;
    replayStore?: ReplayStore;
    now?: () => number;
    expectedRequestId?: string;
  },
): Promise<DecryptedHttpBody<T>> {
  const envelope = parseEnvelope(value);
  if (input.expectedRequestId && envelope.requestId !== input.expectedRequestId) {
    throw new Error("WRN-ENCRYPTION-HTTP-REQUEST-ID");
  }
  const plaintext = await open(envelope.ciphertext, input.keyring);
  const payload = JSON.parse(plaintext) as Partial<EncryptedHttpPayload<T>>;
  const now = input.now?.() ?? Date.now();
  const maxAgeMs = Math.max(1_000, input.maxAgeMs ?? 5 * 60_000);
  if (
    payload.requestId !== envelope.requestId ||
    payload.timestamp !== envelope.timestamp ||
    payload.method !== methodOf(input.method) ||
    payload.path !== normalizePath(input.url) ||
    !("body" in payload)
  ) {
    throw new Error("WRN-ENCRYPTION-HTTP-CONTEXT");
  }
  if (Math.abs(now - envelope.timestamp) > maxAgeMs) {
    throw new Error("WRN-ENCRYPTION-HTTP-EXPIRED");
  }
  if (input.replayStore) {
    const accepted = await input.replayStore.consume(envelope.requestId, now + maxAgeMs);
    if (!accepted) throw new Error("WRN-ENCRYPTION-HTTP-REPLAY");
  }
  return {
    body: payload.body as T,
    requestId: envelope.requestId,
    timestamp: envelope.timestamp,
    keyId: envelope.keyId,
  };
}

export async function createEncryptedRequest<T>(
  url: string | URL,
  body: T,
  input: Omit<RequestInit, "body"> & { keyring: EncryptionKeyring; requestId?: string },
): Promise<Request> {
  const { keyring, requestId, ...requestInit } = input;
  const method = methodOf(requestInit.method);
  const envelope = await encryptHttpBody(body, {
    keyring,
    method,
    url,
    requestId,
  });
  const headers = new Headers(requestInit.headers);
  headers.set("content-type", ENCRYPTED_HTTP_CONTENT_TYPE);
  headers.set("accept", ENCRYPTED_HTTP_CONTENT_TYPE);
  headers.set("x-wrn-request-id", envelope.requestId);
  return new Request(url, {
    ...requestInit,
    method,
    headers,
    body: JSON.stringify(envelope),
  });
}

export async function decryptRequest<T>(
  request: Request,
  options: EncryptedHttpOptions,
): Promise<DecryptedHttpBody<T>> {
  const contentLength = Number(request.headers.get("content-length") ?? "0");
  const maxBodyBytes = Math.max(1, options.maxBodyBytes ?? 1_048_576);
  if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
    throw new Error("WRN-ENCRYPTION-HTTP-BODY-LIMIT");
  }
  const source = await request.text();
  if (new TextEncoder().encode(source).byteLength > maxBodyBytes) {
    throw new Error("WRN-ENCRYPTION-HTTP-BODY-LIMIT");
  }
  const envelope = parseEnvelope(JSON.parse(source));
  const headerRequestId = requestHeaderId(request, options.requireRequestIdHeader !== false);
  if (headerRequestId && headerRequestId !== envelope.requestId) {
    throw new Error("WRN-ENCRYPTION-HTTP-REQUEST-ID");
  }
  return decryptHttpBody<T>(envelope, {
    keyring: options.keyring,
    method: request.method,
    url: request.url,
    maxAgeMs: options.maxAgeMs,
    replayStore: options.replayStore,
    now: options.now,
    expectedRequestId: headerRequestId,
  });
}

export async function encryptResponse<T>(
  body: T,
  request: Request,
  options: EncryptedHttpOptions & { status?: number; headers?: HeadersInit },
): Promise<Response> {
  const originalRequestId = requestHeaderId(request, options.requireRequestIdHeader !== false);
  const envelope = await encryptHttpBody(body, {
    keyring: options.keyring,
    method: request.method,
    url: request.url,
    requestId: originalRequestId,
  });
  const headers = new Headers(options.headers);
  headers.set("content-type", `${ENCRYPTED_HTTP_CONTENT_TYPE}; charset=utf-8`);
  headers.set("cache-control", "no-store");
  headers.set("x-wrn-request-id", envelope.requestId);
  return new Response(JSON.stringify(envelope), { status: options.status ?? 200, headers });
}

export async function decryptEncryptedResponse<T>(
  response: Response,
  request: Request,
  options: EncryptedHttpOptions,
): Promise<DecryptedHttpBody<T>> {
  if (
    !response.headers.get("content-type")?.toLowerCase().startsWith(ENCRYPTED_HTTP_CONTENT_TYPE)
  ) {
    throw new Error("WRN-ENCRYPTION-HTTP-RESPONSE-CONTENT-TYPE");
  }
  const requestId = requestHeaderId(request, options.requireRequestIdHeader !== false);
  const responseRequestId = response.headers.get("x-wrn-request-id")?.trim();
  if (requestId && responseRequestId !== requestId) {
    throw new Error("WRN-ENCRYPTION-HTTP-REQUEST-ID");
  }
  return decryptHttpBody<T>(await response.json(), {
    keyring: options.keyring,
    method: request.method,
    url: request.url,
    maxAgeMs: options.maxAgeMs,
    replayStore: options.replayStore,
    now: options.now,
    expectedRequestId: requestId,
  });
}

export async function encryptedFetch<TRequest, TResponse>(
  url: string | URL,
  body: TRequest,
  input: Omit<RequestInit, "body"> & EncryptedHttpOptions,
): Promise<TResponse> {
  const request = await createEncryptedRequest(url, body, input);
  const response = await fetch(request);
  if (!response.ok) throw new Error(`WRN-ENCRYPTION-HTTP-RESPONSE: ${response.status}`);
  const decrypted = await decryptEncryptedResponse<TResponse>(response, request, input);
  return decrypted.body;
}

export function encryptedBody(options: EncryptedHttpOptions): Middleware {
  return async (ctx: Context, next) => {
    if (
      !ctx.req.headers.get("content-type")?.toLowerCase().startsWith(ENCRYPTED_HTTP_CONTENT_TYPE)
    ) {
      return Response.json(
        { ok: false, error: "Encrypted request body required" },
        { status: 415 },
      );
    }
    try {
      const result = await decryptRequest(ctx.req, options);
      ctx.locals.encryptedBody = result.body;
      ctx.locals.encryptedRequest = result;
      return next();
    } catch (error) {
      return Response.json(
        { ok: false, error: error instanceof Error ? error.message : "Invalid encrypted body" },
        { status: 400, headers: { "cache-control": "no-store" } },
      );
    }
  };
}

export function encryptedExchange(
  options: EncryptedHttpOptions & { encryptResponses?: boolean },
): Middleware {
  return async (ctx: Context, next) => {
    if (
      !ctx.req.headers.get("content-type")?.toLowerCase().startsWith(ENCRYPTED_HTTP_CONTENT_TYPE)
    ) {
      return Response.json(
        { ok: false, error: "Encrypted request body required" },
        { status: 415, headers: { "cache-control": "no-store" } },
      );
    }

    let result: DecryptedHttpBody<unknown>;
    try {
      result = await decryptRequest(ctx.req, options);
    } catch (error) {
      return Response.json(
        { ok: false, error: error instanceof Error ? error.message : "Invalid encrypted exchange" },
        { status: 400, headers: { "cache-control": "no-store" } },
      );
    }

    ctx.locals.encryptedBody = result.body;
    ctx.locals.encryptedRequest = result;
    const response = await next();
    if (options.encryptResponses === false || response.status === 204 || response.status === 304) {
      return response;
    }
    if (
      response.headers.get("content-type")?.toLowerCase().startsWith(ENCRYPTED_HTTP_CONTENT_TYPE)
    ) {
      return response;
    }
    const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
    const body = contentType.includes("json")
      ? await response.clone().json()
      : await response.text();
    const headers = new Headers(response.headers);
    headers.delete("content-length");
    headers.delete("content-encoding");
    headers.delete("etag");
    return encryptResponse(body, ctx.req, {
      ...options,
      status: response.status,
      headers,
    });
  };
}
