"use client";

/**
 * Cloudflare Turnstile widget (task 8.5).
 *
 * The API has verified `turnstile_token` server-side since the security pass; until this
 * component existed nothing in the browser ever produced one. The form carried a bare
 * `<div class="cf-turnstile">` with no script behind it, which renders as empty space —
 * the shape of a bot defence with none of the substance.
 *
 * Rendered explicitly rather than by Cloudflare's auto-scan, for one reason: a token is
 * single-use. After a rejected submission the widget has to be reset or every retry
 * re-sends a token Cloudflare has already burned, and the guest is stuck in a loop of
 * failures with nothing on screen explaining why.
 */
import { useCallback, useEffect, useRef } from "react";

const SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";

interface RenderOptions {
  sitekey: string;
  callback: (token: string) => void;
  "expired-callback": () => void;
  "error-callback": () => void;
  "timeout-callback": () => void;
  theme?: "light" | "dark" | "auto";
  appearance?: "always" | "execute" | "interaction-only";
}

interface TurnstileApi {
  render: (container: HTMLElement, options: RenderOptions) => string;
  reset: (widgetId: string) => void;
  remove: (widgetId: string) => void;
}

declare global {
  interface Window {
    turnstile?: TurnstileApi;
  }
}

/**
 * One load per document, shared across mounts. `next/script` would do this too, but it
 * gives no signal precise enough to call `render()` against — and rendering before the
 * script settles is the whole failure mode.
 */
let scriptLoad: Promise<void> | null = null;

function loadScript(): Promise<void> {
  if (window.turnstile) return Promise.resolve();
  if (scriptLoad) return scriptLoad;

  scriptLoad = new Promise<void>((resolve, reject) => {
    const script = document.createElement("script");
    script.src = SCRIPT_SRC;
    script.async = true;
    script.defer = true;
    script.onload = () => resolve();
    script.onerror = () => {
      // Cleared so a later mount retries. An ad blocker or a network blip should not
      // permanently disable registration for the rest of the page's life.
      scriptLoad = null;
      reject(new Error("Turnstile script did not load"));
    };
    document.head.appendChild(script);
  });

  return scriptLoad;
}

export function Turnstile({
  siteKey,
  onToken,
  resetSignal,
  className,
}: {
  siteKey: string;
  /** Called with a fresh token, or null whenever the current one stops being usable. */
  onToken: (token: string | null) => void;
  /** Increment to force a new challenge — after a rejected submission, for instance. */
  resetSignal: number;
  className?: string;
}) {
  const containerRef = useRef<HTMLDivElement>(null);
  const widgetIdRef = useRef<string | null>(null);

  // Held in a ref so a parent that re-creates its handler on every render does not tear
  // down and re-render the widget, which would discard a token the guest already solved.
  const onTokenRef = useRef(onToken);
  onTokenRef.current = onToken;

  const clearToken = useCallback(() => onTokenRef.current(null), []);

  useEffect(() => {
    let cancelled = false;

    loadScript()
      .then(() => {
        if (cancelled || !containerRef.current || !window.turnstile) return;
        widgetIdRef.current = window.turnstile.render(containerRef.current, {
          sitekey: siteKey,
          callback: (token) => onTokenRef.current(token),
          // Turnstile tokens expire after ~5 minutes. A guest who leaves the form open
          // must not be allowed to submit one the server will reject.
          "expired-callback": clearToken,
          "error-callback": clearToken,
          "timeout-callback": clearToken,
          appearance: "always",
        });
      })
      .catch(() => {
        if (!cancelled) clearToken();
      });

    return () => {
      cancelled = true;
      const widgetId = widgetIdRef.current;
      widgetIdRef.current = null;
      if (widgetId && window.turnstile) window.turnstile.remove(widgetId);
    };
  }, [siteKey, clearToken]);

  useEffect(() => {
    // Skipped on mount: there is nothing to reset, and the widget may not exist yet.
    if (resetSignal === 0) return;
    const widgetId = widgetIdRef.current;
    if (widgetId && window.turnstile) {
      window.turnstile.reset(widgetId);
      clearToken();
    }
  }, [resetSignal, clearToken]);

  return <div ref={containerRef} className={className} />;
}
