// TurnstileWidget.jsx — v0.12.0 / roadmap item #10.
//
// Renders the CloudFlare Turnstile JS widget on the email-entry step of
// `/login`. Reads the site key from `import.meta.env.VITE_TURNSTILE_SITE_KEY`
// (Vite convention — VITE_* prefix is build-time embedded). When the
// site key is unset/empty, this component renders nothing and reports
// a `null` token through `onToken` so the parent form can still submit.
// The backend's `TURNSTILE_REQUIRED` policy decides what happens to a
// request that arrives without a token; the frontend is intentionally
// not in that loop. See `backend/app/turnstile.py` for the matrix.
//
// The CloudFlare script is loaded once per page on first widget mount.
// Subsequent mounts (e.g. user goes back to email-entry after a failed
// OTC request) reuse the script tag and re-render the widget on the
// fresh container `div`. Unmounting removes the widget instance via
// `turnstile.remove(widgetId)` so a remount produces a new challenge
// rather than reusing a stale, already-consumed token.
//
// Turnstile contract:
//   * `data-callback` fires with the token string on a successful
//     challenge; the token is single-use and expires after ~5 minutes.
//   * `data-error-callback` fires on a failed challenge (network,
//     blocked, etc.); we surface a `null` token so the parent shows
//     a retry hint.
//   * `data-expired-callback` fires when the token times out before
//     submission; we also drop to `null` and re-render so the user
//     gets a fresh challenge on retry.
//
// We do **not** import the CloudFlare script at build time; loading it
// dynamically here keeps the bundle clean of an external request the
// page may not need (anonymous viewers reading RFCs never see Login).

import { useEffect, useRef } from 'react'

const TURNSTILE_SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
const SITE_KEY = import.meta.env.VITE_TURNSTILE_SITE_KEY || ''

// Promise-keyed: only one script tag, only one resolution chain.
let scriptLoadPromise = null

function loadTurnstileScript() {
  if (typeof window === 'undefined') return Promise.resolve(null)
  if (window.turnstile) return Promise.resolve(window.turnstile)
  if (scriptLoadPromise) return scriptLoadPromise

  scriptLoadPromise = new Promise((resolve, reject) => {
    const existing = document.querySelector(`script[src="${TURNSTILE_SCRIPT_URL}"]`)
    if (existing) {
      existing.addEventListener('load', () => resolve(window.turnstile))
      existing.addEventListener('error', reject)
      return
    }
    const script = document.createElement('script')
    script.src = TURNSTILE_SCRIPT_URL
    script.async = true
    script.defer = true
    script.addEventListener('load', () => resolve(window.turnstile))
    script.addEventListener('error', reject)
    document.head.appendChild(script)
  })
  return scriptLoadPromise
}

export function turnstileEnabled() {
  return !!SITE_KEY
}

export default function TurnstileWidget({ onToken, theme = 'auto' }) {
  const containerRef = useRef(null)
  const widgetIdRef = useRef(null)

  useEffect(() => {
    if (!SITE_KEY) {
      // No site key configured → surface a null token immediately so
      // the parent form's submit-disabled gate doesn't lock up
      // waiting on a challenge that will never arrive. The backend
      // decides whether a tokenless request is admitted.
      onToken?.(null)
      return undefined
    }

    let cancelled = false
    loadTurnstileScript()
      .then(turnstile => {
        if (cancelled || !turnstile || !containerRef.current) return
        widgetIdRef.current = turnstile.render(containerRef.current, {
          sitekey: SITE_KEY,
          theme,
          callback: token => onToken?.(token),
          'error-callback': () => onToken?.(null),
          'expired-callback': () => onToken?.(null),
        })
      })
      .catch(() => {
        // Script load failure — surface null so the parent can decide
        // what to do (today: still let submit through; the backend
        // policy decides admission).
        if (!cancelled) onToken?.(null)
      })

    return () => {
      cancelled = true
      if (widgetIdRef.current && window.turnstile) {
        try {
          window.turnstile.remove(widgetIdRef.current)
        } catch (_) {
          // Already gone or never registered — nothing to clean up.
        }
        widgetIdRef.current = null
      }
    }
    // We intentionally do not list `onToken` in the dependency array;
    // a parent re-rendering with a fresh closure should not tear down
    // and rebuild the widget (which would consume a fresh challenge).
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  if (!SITE_KEY) return null
  return <div ref={containerRef} className="turnstile-widget" />
}
