// @design-system: primitives/Turnstile
/**
 * Turnstile - Cloudflare Turnstile widget (managed mode) wrapped as a reusable
 * form-security primitive.
 *
 * Loads `https://challenges.cloudflare.com/turnstile/v0/api.js` exactly once
 * (idempotent across island remounts), renders the managed challenge into a
 * container ref, and surfaces the response token via `onToken`. The token is
 * single-use (~300s TTL, siteverify-enforced) — the parent MUST call the
 * imperative `reset()` (exposed via `ref`) after any submit that consumes the
 * token (success or a 403 TURNSTILE_FAILED), then await a fresh token.
 *
 * The widget is an iframe Cloudflare fully controls (reduced-motion, theming
 * and the visual challenge are handled inside it). We only own the surrounding
 * markup: a labelled, RTL-safe container with logical spacing. Widget language
 * is driven from the app locale (he/en) via `useLocale()`.
 *
 * Sitekey is read from `import.meta.env.PUBLIC_TURNSTILE_SITE_KEY` (Astro public
 * env). When the sitekey is missing (misconfiguration) the component renders a
 * localized error and never reports a token, so the gated form stays blocked.
 *
 * @example
 * ```tsx
 * const turnstileRef = useRef<TurnstileHandle>(null);
 * const [token, setToken] = useState<string | null>(null);
 * <Turnstile
 *   ref={turnstileRef}
 *   onToken={setToken}
 *   onExpire={() => setToken(null)}
 *   onError={() => setToken(null)}
 * />
 * // after a 403: turnstileRef.current?.reset(); setToken(null);
 * ```
 */

'use client';

import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
import { useT } from '@/lib/i18n/react';
import { useLocale } from '@/lib/i18n/react';
import { InlineNotice } from '@/components/ui/feedback/InlineNotice';
import { captureCaught } from '@/lib/observability';
import { cn } from '@/lib/cn';

const TURNSTILE_SCRIPT_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
const TURNSTILE_SCRIPT_ID = 'cf-turnstile-script';

// Minimal typings for the slice of the Turnstile JS API we use (managed mode).
interface TurnstileRenderOptions {
  sitekey: string;
  callback: (token: string) => void;
  'expired-callback'?: () => void;
  'error-callback'?: () => void;
  language?: string;
  theme?: 'auto' | 'light' | 'dark';
  action?: string;
}

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

declare global {
  interface Window {
    turnstile?: TurnstileApi;
    onloadTurnstileCallback?: () => void;
  }
}

/** Imperative handle exposed to parents so they can reset the single-use token. */
export interface TurnstileHandle {
  /** Reset the widget to its initial no-token state (e.g. after a 403). */
  reset: () => void;
}

/** Props for the Turnstile component. */
export interface TurnstileProps {
  /** Called with the response token once the challenge is solved. */
  onToken: (token: string) => void;
  /** Called when the token expires; parent should clear its held token. */
  onExpire?: () => void;
  /** Called when the widget errors; parent should clear its held token. */
  onError?: () => void;
  /**
   * Optional Turnstile `action` label (analytics dimension in the CF dashboard).
   * e.g. 'login', 'register', 'magic-link'.
   */
  action?: string;
  /** Additional classes on the wrapper. */
  className?: string;
}

/**
 * Idempotently inject the Turnstile script. Resolves once `window.turnstile` is
 * available. Guards against double-injection when multiple islands mount.
 */
function loadTurnstileScript(): Promise<void> {
  if (typeof window === 'undefined' || typeof document === 'undefined') {
    return Promise.resolve();
  }
  if (window.turnstile) return Promise.resolve();

  const existing = document.getElementById(TURNSTILE_SCRIPT_ID) as HTMLScriptElement | null;
  if (existing) {
    // Script tag present but API not yet ready — wait for it to finish loading.
    if (window.turnstile) return Promise.resolve();
    return new Promise<void>((resolve, reject) => {
      existing.addEventListener('load', () => resolve(), { once: true });
      existing.addEventListener('error', () => reject(new Error('turnstile script failed')), {
        once: true,
      });
    });
  }

  return new Promise<void>((resolve, reject) => {
    const script = document.createElement('script');
    script.id = TURNSTILE_SCRIPT_ID;
    script.src = TURNSTILE_SCRIPT_SRC;
    script.async = true;
    script.defer = true;
    script.addEventListener('load', () => resolve(), { once: true });
    script.addEventListener('error', () => reject(new Error('turnstile script failed')), {
      once: true,
    });
    document.head.appendChild(script);
  });
}

export const Turnstile = forwardRef<TurnstileHandle, TurnstileProps>(function Turnstile(
  { onToken, onExpire, onError, action, className },
  ref,
) {
  const t = useT('auth_flow');
  const { locale } = useLocale();
  const containerRef = useRef<HTMLDivElement | null>(null);
  const widgetIdRef = useRef<string | null>(null);
  const labelId = useId();
  const [loadFailed, setLoadFailed] = useState(false);

  const sitekey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY as string | undefined;

  // Keep the latest callbacks in refs so the render effect can stay keyed only
  // on `locale`/`sitekey` (re-rendering the widget) without re-subscribing on
  // every parent re-render.
  const onTokenRef = useRef(onToken);
  const onExpireRef = useRef(onExpire);
  const onErrorRef = useRef(onError);
  onTokenRef.current = onToken;
  onExpireRef.current = onExpire;
  onErrorRef.current = onError;

  useImperativeHandle(
    ref,
    () => ({
      reset() {
        if (window.turnstile && widgetIdRef.current) {
          window.turnstile.reset(widgetIdRef.current);
        }
      },
    }),
    [],
  );

  useEffect(() => {
    // Missing sitekey is a hard misconfiguration: surface an error and never
    // emit a token so the gated form remains blocked (fail-closed on the client).
    if (!sitekey) {
      setLoadFailed(true);
      return;
    }

    let cancelled = false;

    loadTurnstileScript()
      .then(() => {
        if (cancelled) return;
        const api = window.turnstile;
        const container = containerRef.current;
        if (!api || !container) {
          setLoadFailed(true);
          return;
        }
        setLoadFailed(false);
        widgetIdRef.current = api.render(container, {
          sitekey,
          language: locale,
          callback: (token: string) => onTokenRef.current(token),
          'expired-callback': () => onExpireRef.current?.(),
          'error-callback': () => onErrorRef.current?.(),
          ...(action ? { action } : {}),
        });
      })
      .catch((err: unknown) => {
        captureCaught(err, {
          scope: 'components.ui.primitives.Turnstile.load',
          severity: 'warning',
        });
        if (!cancelled) setLoadFailed(true);
      });

    return () => {
      cancelled = true;
      if (window.turnstile && widgetIdRef.current) {
        window.turnstile.remove(widgetIdRef.current);
        widgetIdRef.current = null;
      }
    };
    // Re-render the widget when locale changes so its language matches the app.
    // Callbacks are read from refs, so they are intentionally excluded.
  }, [sitekey, locale, action]);

  if (loadFailed) {
    return <InlineNotice tone="danger" description={t('turnstile_unavailable')} />;
  }

  return (
    <div className={cn('flex flex-col gap-1', className)}>
      <p id={labelId} className="sr-only">
        {t('turnstile_label')}
      </p>
      <div ref={containerRef} role="group" aria-labelledby={labelId} className="min-h-16" />
    </div>
  );
});
