import type { Context } from "@wrnexus/core";
import { shouldRequireCaptcha, validCaptchaGrant, type CaptchaSessionGrant } from "./policy.ts";
import { selfHostedProvider } from "./providers/self-hosted.ts";
import type {
  CaptchaGuardOptions,
  CaptchaPageGateOptions,
  CaptchaProvider,
  CaptchaVerificationResult,
} from "./types.ts";

const DEFAULT_FIELD = "wrn-captcha-response";

function resolveAction(value: CaptchaGuardOptions["action"], ctx: Context): string {
  return typeof value === "function" ? value(ctx) : value;
}

async function bodyValue(request: Request, field: string): Promise<string | undefined> {
  const header = request.headers.get("x-wrn-captcha-token");
  if (header) return header;
  if (request.method === "GET" || request.method === "HEAD") return undefined;
  const clone = request.clone();
  const contentType = clone.headers.get("content-type") ?? "";
  try {
    if (contentType.includes("application/json")) {
      const body = (await clone.json()) as Record<string, unknown>;
      const value = body[field] ?? body.captchaToken ?? body.responseToken;
      return value === undefined ? undefined : String(value);
    }
    if (contentType.includes("form")) {
      const form = await clone.formData();
      const value = form.get(field);
      return typeof value === "string" ? value : undefined;
    }
  } catch {
    return undefined;
  }
  return undefined;
}

function providerFor(options: CaptchaGuardOptions): CaptchaProvider {
  if (options.provider) return options.provider;
  if (options.engine) return selfHostedProvider(options.engine);
  throw new TypeError("captchaGuard requires provider or engine");
}

async function verifyRequest(
  ctx: Context,
  options: CaptchaGuardOptions,
): Promise<CaptchaVerificationResult> {
  const provider = providerFor(options);
  const action = resolveAction(options.action, ctx);
  const field = options.responseField ?? provider.client.responseField ?? DEFAULT_FIELD;
  const token = await bodyValue(ctx.req, field);
  return provider.verify({
    action,
    providerToken: token,
    responseToken: token,
    hostname: options.bindHostname === false ? undefined : ctx.url.hostname,
    sessionId: options.bindSession === false ? undefined : ctx.session.id(),
    ip: options.bindIp ? ctx.ip : undefined,
  });
}

function defaultFailure(options: CaptchaGuardOptions, result: CaptchaVerificationResult): Response {
  return new Response(options.failureMessage ?? result.message ?? "CAPTCHA verification failed", {
    status: options.failureStatus ?? 403,
    headers: {
      "content-type": "text/plain; charset=utf-8",
      "cache-control": "no-store",
      "x-wrn-captcha-error": result.code ?? "verification-failed",
    },
  });
}

export function captchaGuard(options: CaptchaGuardOptions) {
  const verifiedForMs = options.verifiedForMs ?? 0;
  if (!Number.isFinite(verifiedForMs) || verifiedForMs < 0) {
    throw new TypeError("captchaGuard verifiedForMs must be a non-negative finite duration");
  }
  const sessionKey = options.sessionKey ?? "wrnexus.captcha.grants";

  return async (ctx: Context, next: () => Promise<Response> | Response): Promise<Response> => {
    const action = resolveAction(options.action, ctx);
    const now = Date.now();
    const grants = ctx.session.get<CaptchaSessionGrant[]>(sessionKey) ?? [];
    const grant = verifiedForMs > 0 ? validCaptchaGrant(grants, action, now) : undefined;
    if (grant) {
      ctx.locals.captcha = {
        success: true,
        provider: grant.provider,
        action,
      };
      ctx.locals.captchaVerified = true;
      return next();
    }

    const result = await verifyRequest(ctx, options);
    ctx.locals.captcha = result;
    if (!result.success)
      return options.onFailure ? options.onFailure(ctx, result) : defaultFailure(options, result);

    if (verifiedForMs > 0) {
      const fresh: CaptchaSessionGrant = {
        action,
        provider: result.provider,
        expiresAt: now + verifiedForMs,
      };
      ctx.session.set(sessionKey, [
        ...grants.filter((item) => item.expiresAt > now && item.action !== action),
        fresh,
      ]);
    }

    return next();
  };
}

export function captchaPageGate(options: CaptchaPageGateOptions) {
  const sessionKey = options.sessionKey ?? "wrnexus.captcha.grants";
  const returnToParam = options.returnToParam ?? "returnTo";
  const challengePath = options.challengePath ?? "/captcha";
  const policy = options.policy ?? { mode: "session" as const };
  return async (ctx: Context, next: () => Promise<Response> | Response): Promise<Response> => {
    const action = resolveAction(options.action, ctx);
    const now = Date.now();
    const routeGroup = policy.routeGroups?.find((group) => ctx.url.pathname.startsWith(group));
    const grants = ctx.session.get<CaptchaSessionGrant[]>(sessionKey) ?? [];
    if (validCaptchaGrant(grants, action, now, routeGroup)) return next();

    const signals = (await options.signals?.(ctx)) ?? {};
    const decision = shouldRequireCaptcha(action, policy, signals);
    ctx.locals.captchaRisk = decision;
    if (!decision.challenge) return next();

    const token = await bodyValue(ctx.req, options.responseField ?? DEFAULT_FIELD);
    if (token) {
      const result = await verifyRequest(ctx, options);
      ctx.locals.captcha = result;
      if (result.success) {
        const grant: CaptchaSessionGrant = {
          action,
          routeGroup,
          provider: result.provider,
          expiresAt: now + (policy.verifiedForMs ?? 15 * 60_000),
        };
        ctx.session.set(sessionKey, [...grants.filter((item) => item.expiresAt > now), grant]);
        return next();
      }
      if (ctx.req.method !== "GET" && ctx.req.method !== "HEAD") {
        return options.onFailure ? options.onFailure(ctx, result) : defaultFailure(options, result);
      }
    }

    const redirect = new URL(challengePath, ctx.url);
    redirect.searchParams.set(returnToParam, `${ctx.url.pathname}${ctx.url.search}`);
    redirect.searchParams.set("action", action);
    return Response.redirect(redirect, 302);
  };
}

export function clearCaptchaGrants(ctx: Context, sessionKey = "wrnexus.captcha.grants"): void {
  ctx.session.delete(sessionKey);
}
