import type { Context } from "@wrnexus/core";
import type {
  CaptchaEngine,
  CaptchaHttpHandlers,
  CreateCaptchaOptions,
  VerifyCaptchaInput,
} from "./types.ts";

export interface CaptchaHttpOptions {
  allowedOrigins?: string[];
  trustProxy?: boolean;
  createLimit?: number;
  verifyLimit?: number;
  windowMs?: number;
}

interface Counter {
  count: number;
  resetAt: number;
}

function json(body: unknown, status = 200, headers: HeadersInit = {}): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: {
      "content-type": "application/json; charset=utf-8",
      "cache-control": "no-store, max-age=0",
      pragma: "no-cache",
      "x-content-type-options": "nosniff",
      ...headers,
    },
  });
}

async function readPayload(request: Request): Promise<Record<string, unknown>> {
  const contentType = request.headers.get("content-type") ?? "";
  if (contentType.includes("application/json"))
    return (await request.json()) as Record<string, unknown>;
  if (contentType.includes("form")) {
    const form = await request.formData();
    const payload: Record<string, unknown> = {};
    for (const [key, value] of form) {
      if (key === "selections") {
        const current = payload[key];
        payload[key] = Array.isArray(current) ? [...current, String(value)] : [String(value)];
      } else payload[key] = typeof value === "string" ? value : value.name;
    }
    return payload;
  }
  return {};
}

function clientIp(request: Request, ctx: Context | undefined, trustProxy: boolean): string {
  if (trustProxy) {
    const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
    if (forwarded) return forwarded;
    const real = request.headers.get("x-real-ip");
    if (real) return real;
  }
  return ctx?.ip ?? "global";
}

function requestBinding(request: Request, ctx: Context | undefined, trustProxy: boolean) {
  const url = new URL(request.url);
  return {
    hostname: url.hostname,
    sessionId: ctx?.session.id(),
    ip: clientIp(request, ctx, trustProxy),
  };
}

export function createCaptchaHttpHandlers(
  engine: CaptchaEngine,
  options: CaptchaHttpOptions = {},
): CaptchaHttpHandlers {
  const counters = new Map<string, Counter>();
  const windowMs = options.windowMs ?? 60_000;
  const originAllowed = (request: Request): boolean => {
    const origin = request.headers.get("origin");
    if (!origin) return true;
    const own = new URL(request.url).origin;
    return origin === own || options.allowedOrigins?.includes(origin) === true;
  };
  const withinLimit = (key: string, maximum: number): { allowed: boolean; retryAfter: number } => {
    const now = Date.now();
    let counter = counters.get(key);
    if (!counter || counter.resetAt <= now) {
      counter = { count: 0, resetAt: now + windowMs };
      counters.set(key, counter);
    }
    counter.count += 1;
    return {
      allowed: counter.count <= maximum,
      retryAfter: Math.max(1, Math.ceil((counter.resetAt - now) / 1000)),
    };
  };

  const create = async (request: Request, ctx?: Context): Promise<Response> => {
    if (!originAllowed(request)) return json({ success: false, code: "origin-rejected" }, 403);
    if (request.method !== "POST")
      return json({ success: false, code: "method-not-allowed" }, 405, { allow: "POST" });
    const ip = clientIp(request, ctx, options.trustProxy ?? false);
    const limit = withinLimit(`create:${ip}`, options.createLimit ?? 30);
    if (!limit.allowed)
      return json({ success: false, code: "rate-limited" }, 429, {
        "retry-after": String(limit.retryAfter),
      });
    try {
      const payload = await readPayload(request);
      const challenge = await engine.create({
        ...(payload as unknown as CreateCaptchaOptions),
        ...requestBinding(request, ctx, options.trustProxy ?? false),
      });
      return json(challenge, 201);
    } catch (error) {
      return json(
        {
          success: false,
          code: "invalid-input",
          message: error instanceof Error ? error.message : "Unable to create CAPTCHA challenge",
        },
        400,
      );
    }
  };

  const verify = async (request: Request, ctx?: Context): Promise<Response> => {
    if (!originAllowed(request)) return json({ success: false, code: "origin-rejected" }, 403);
    if (request.method !== "POST")
      return json({ success: false, code: "method-not-allowed" }, 405, { allow: "POST" });
    const ip = clientIp(request, ctx, options.trustProxy ?? false);
    const limit = withinLimit(`verify:${ip}`, options.verifyLimit ?? 60);
    if (!limit.allowed)
      return json({ success: false, code: "rate-limited" }, 429, {
        "retry-after": String(limit.retryAfter),
      });
    try {
      const payload = await readPayload(request);
      const result = await engine.verify({
        ...(payload as unknown as VerifyCaptchaInput),
        ...requestBinding(request, ctx, options.trustProxy ?? false),
      });
      return json(result, result.success ? 200 : 400);
    } catch (error) {
      return json(
        {
          success: false,
          code: "invalid-input",
          message: error instanceof Error ? error.message : "Unable to verify CAPTCHA",
        },
        400,
      );
    }
  };

  const audio = async (request: Request): Promise<Response> => {
    if (!originAllowed(request)) return new Response("Forbidden", { status: 403 });
    if (request.method !== "GET" && request.method !== "HEAD")
      return new Response("Method Not Allowed", { status: 405 });
    const url = new URL(request.url);
    const prefix = `${engine.basePath}/audio/`;
    const id = decodeURIComponent(url.pathname.slice(prefix.length));
    const key = url.searchParams.get("key") ?? "";
    try {
      const audio = await engine.renderAudio(id, key);
      if (!audio) return new Response("Not Found", { status: 404 });

      const body: BodyInit | null =
        request.method === "HEAD" ? null : Uint8Array.from(audio.bytes).buffer;

      return new Response(body, {
        headers: {
          "content-type": audio.contentType,
          "content-length": String(audio.bytes.byteLength),
          "cache-control": "private, no-store, max-age=0",
          "x-content-type-options": "nosniff",
        },
      });
    } catch (error) {
      console.error(
        "[wrnexus/captcha] Unable to render CAPTCHA audio:",
        error instanceof Error ? error.message : error,
      );
      return new Response("CAPTCHA audio is unavailable", {
        status: 503,
        headers: {
          "content-type": "text/plain; charset=utf-8",
          "cache-control": "no-store, max-age=0",
          "x-content-type-options": "nosniff",
        },
      });
    }
  };

  return {
    create,
    verify,
    audio,
    async handle(request, ctx) {
      const pathname = new URL(request.url).pathname;
      if (pathname === `${engine.basePath}/challenge`) return create(request, ctx);
      if (pathname === `${engine.basePath}/verify`) return verify(request, ctx);
      if (pathname.startsWith(`${engine.basePath}/audio/`)) return audio(request);
      return undefined;
    },
  };
}
