import type {
  CaptchaProvider,
  CaptchaProviderClientConfig,
  CaptchaProviderName,
  CaptchaVerificationResult,
  VerifyCaptchaInput,
} from "../types.ts";

export interface SiteverifyProviderOptions {
  secretKey: string;
  siteKey?: string;
  expectedHostnames?: string[];
  expectedAction?: string;
  minScore?: number;
  timeoutMs?: number;
  fetch?: typeof fetch;
}

export interface SiteverifyPreset {
  name: CaptchaProviderName;
  endpoint: string;
  client: CaptchaProviderClientConfig;
  sendSiteKey?: boolean;
  scoreDirection?: "higher-is-human" | "higher-is-risk";
}

interface SiteverifyPayload {
  success?: boolean;
  hostname?: string;
  action?: string;
  score?: number;
  challenge_ts?: string;
  "error-codes"?: string[];
  error_codes?: string[];
  [key: string]: unknown;
}

function providerFailure(
  name: CaptchaProviderName,
  action: string,
  code: string,
  message: string,
  metadata?: Record<string, unknown>,
): CaptchaVerificationResult {
  return { success: false, provider: name, action, code, message, metadata };
}

export class SiteverifyCaptchaProvider implements CaptchaProvider {
  readonly name: CaptchaProviderName;
  readonly client: CaptchaProviderClientConfig;

  constructor(
    private readonly preset: SiteverifyPreset,
    private readonly options: SiteverifyProviderOptions,
  ) {
    if (!options.secretKey) throw new TypeError(`${preset.name} secretKey is required`);
    this.name = preset.name;
    this.client = { ...preset.client, siteKey: options.siteKey };
  }

  async verify(input: VerifyCaptchaInput): Promise<CaptchaVerificationResult> {
    const token = input.providerToken ?? input.responseToken;
    const action = input.action;
    if (!token)
      return providerFailure(this.name, action, "missing-input", "Missing provider response token");
    if (token.length > 4096)
      return providerFailure(this.name, action, "invalid-input", "Provider token is too long");
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 10_000);
    const body = new URLSearchParams({ secret: this.options.secretKey, response: token });
    if (input.ip) body.set("remoteip", input.ip);
    if (this.preset.sendSiteKey && this.options.siteKey) body.set("sitekey", this.options.siteKey);
    try {
      const response = await (this.options.fetch ?? fetch)(this.preset.endpoint, {
        method: "POST",
        headers: {
          "content-type": "application/x-www-form-urlencoded",
          accept: "application/json",
        },
        body,
        signal: controller.signal,
      });
      if (!response.ok) {
        return providerFailure(
          this.name,
          action,
          "provider-error",
          `${this.name} verification returned HTTP ${response.status}`,
        );
      }
      const data = (await response.json()) as SiteverifyPayload;
      if (!data.success) {
        const codes = data["error-codes"] ?? data.error_codes ?? [];
        return providerFailure(
          this.name,
          action,
          codes.includes("timeout-or-duplicate") || codes.includes("already-seen-response")
            ? "already-used"
            : codes.includes("expired-input-response")
              ? "expired"
              : "invalid-input",
          "Provider verification failed",
          { errorCodes: codes },
        );
      }
      const expectedAction = this.options.expectedAction ?? action;
      if (data.action && expectedAction && data.action !== expectedAction) {
        return providerFailure(
          this.name,
          action,
          "action-mismatch",
          "Provider action does not match",
          {
            receivedAction: data.action,
          },
        );
      }
      if (
        this.options.expectedHostnames?.length &&
        (!data.hostname || !this.options.expectedHostnames.includes(data.hostname))
      ) {
        return providerFailure(
          this.name,
          action,
          "hostname-mismatch",
          "Provider hostname does not match",
          {
            hostname: data.hostname,
          },
        );
      }
      if (this.options.minScore !== undefined && typeof data.score === "number") {
        const rejected =
          this.preset.scoreDirection === "higher-is-risk"
            ? data.score >= this.options.minScore
            : data.score < this.options.minScore;
        if (rejected) {
          return providerFailure(
            this.name,
            action,
            "risk-rejected",
            "Provider risk score did not pass",
            {
              score: data.score,
            },
          );
        }
      }
      return {
        success: true,
        provider: this.name,
        action,
        score: data.score,
        hostname: data.hostname,
        metadata: {
          challengeTimestamp: data.challenge_ts,
          raw: data,
        },
      };
    } catch (error) {
      return providerFailure(
        this.name,
        action,
        error instanceof DOMException && error.name === "AbortError"
          ? "network-error"
          : "provider-error",
        error instanceof DOMException && error.name === "AbortError"
          ? `${this.name} verification timed out`
          : `${this.name} verification failed`,
      );
    } finally {
      clearTimeout(timeout);
    }
  }
}
