import { ForbiddenException, Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'

// Pluggable abuse guard for the public booking surface (spec §6.1). The default
// is a no-op so unconfigured environments behave exactly as before. When a
// captcha provider is configured (currently Cloudflare Turnstile via
// TURNSTILE_SECRET) the guard verifies the visitor-supplied token and rejects
// requests it can't validate. We deliberately do NOT solve captchas — we only
// forward the token the browser widget produced to the provider's siteverify.

export const ABUSE_GUARD = Symbol('ABUSE_GUARD')

export interface AbuseGuard {
  // Whether a captcha token is expected from the client. The booking app uses
  // this to decide whether to render a widget (mirrored client-side via the
  // public site-key env), but the server is the source of truth on enforcement.
  readonly enabled: boolean

  // Verify a request. `token` is the captcha token from the client, if any.
  // `remoteIp` is the visitor IP for providers that score on it. Throws a 4xx
  // when the token is missing/invalid; resolves silently when acceptable.
  verify(token: string | undefined, remoteIp?: string): Promise<void>
}

// No-op guard — accepts everything. Used when no captcha provider is configured.
@Injectable()
export class NoopAbuseGuard implements AbuseGuard {
  readonly enabled = false

  async verify(): Promise<void> {
    // Intentionally does nothing.
  }
}

interface TurnstileVerifyResponse {
  success: boolean
  'error-codes'?: string[]
}

// Cloudflare Turnstile verifier. Posts the client token to siteverify with the
// secret; any non-success (missing token, network error, provider rejection)
// becomes a 403 so we fail closed once captcha is switched on.
@Injectable()
export class TurnstileAbuseGuard implements AbuseGuard {
  readonly enabled = true
  private readonly logger = new Logger(TurnstileAbuseGuard.name)
  private static readonly SITEVERIFY = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'

  constructor(private readonly secret: string) {}

  async verify(token: string | undefined, remoteIp?: string): Promise<void> {
    if (!token) throw new ForbiddenException('Captcha verification required.')

    const form = new URLSearchParams()
    form.set('secret', this.secret)
    form.set('response', token)
    if (remoteIp) form.set('remoteip', remoteIp)

    let body: TurnstileVerifyResponse
    try {
      const res = await fetch(TurnstileAbuseGuard.SITEVERIFY, {
        method: 'POST',
        headers: { 'content-type': 'application/x-www-form-urlencoded' },
        body: form,
      })
      body = (await res.json()) as TurnstileVerifyResponse
    } catch (err) {
      // Fail closed: if we can't reach the verifier we don't let the request through.
      this.logger.error(`Turnstile siteverify request failed: ${String(err)}`)
      throw new ForbiddenException('Captcha verification unavailable. Please try again.')
    }

    if (!body.success) {
      this.logger.warn(`Turnstile rejected token: ${(body['error-codes'] ?? []).join(', ') || 'unknown'}`)
      throw new ForbiddenException('Captcha verification failed.')
    }
  }
}

// Factory used by the module: pick Turnstile when its secret is set, else no-op.
export function abuseGuardFactory(config: ConfigService): AbuseGuard {
  const secret = config.get<string>('TURNSTILE_SECRET')?.trim()
  if (secret) return new TurnstileAbuseGuard(secret)
  return new NoopAbuseGuard()
}
