"use client";

import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react";
import { labelForKind, type Teaser, VERDICT_LABEL } from "@/lib/report";

/** The slice of the Turnstile widget API this component uses. */
declare global {
  interface Window {
    turnstile?: {
      render: (
        element: HTMLElement,
        options: {
          sitekey: string;
          callback: (token: string) => void;
          "expired-callback"?: () => void;
          "error-callback"?: () => void;
        },
      ) => string;
      reset: (widgetId?: string) => void;
    };
  }
}

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

type Phase = "idle" | "queued" | "running" | "done" | "failed";

export function AuditForm({ siteKey }: { siteKey: string }) {
  const [url, setUrl] = useState("");
  const [phase, setPhase] = useState<Phase>("idle");
  const [error, setError] = useState<string | null>(null);
  const [teaser, setTeaser] = useState<Teaser | null>(null);
  const [token, setToken] = useState<string | null>(null);
  const [claim, setClaim] = useState<string | null>(null);

  const widgetHost = useRef<HTMLDivElement>(null);
  const widgetId = useRef<string | null>(null);

  // Explicit rendering rather than the `cf-turnstile` class: React owns this
  // subtree, and the implicit renderer would fight it on every re-render.
  useEffect(() => {
    if (!siteKey) return;
    let cancelled = false;

    const render = () => {
      if (cancelled || !widgetHost.current || widgetId.current !== null) return;
      if (!window.turnstile) return;
      widgetId.current = window.turnstile.render(widgetHost.current, {
        sitekey: siteKey,
        callback: setToken,
        "expired-callback": () => setToken(null),
        "error-callback": () => setToken(null),
      });
    };

    if (window.turnstile) {
      render();
      return () => {
        cancelled = true;
      };
    }

    const existing = document.querySelector<HTMLScriptElement>(`script[src="${SCRIPT_SRC}"]`);
    const script = existing ?? document.createElement("script");
    script.src = SCRIPT_SRC;
    script.async = true;
    script.addEventListener("load", render);
    if (!existing) document.head.appendChild(script);

    return () => {
      cancelled = true;
      script.removeEventListener("load", render);
    };
  }, [siteKey]);

  const poll = useCallback(async (id: string) => {
    // Every two seconds until the job settles. An audit of ten pages takes
    // seconds, so there is nothing to gain from a websocket here.
    for (;;) {
      await new Promise((resolve) => setTimeout(resolve, 2000));
      const response = await fetch(`/api/audit/${id}`);
      if (!response.ok) {
        setPhase("failed");
        setError("l'audit est introuvable");
        return;
      }
      const body = (await response.json()) as {
        status: Phase;
        error: string | null;
        teaser: Teaser | null;
      };
      if (body.status === "failed") {
        setPhase("failed");
        setError(body.error ?? "l'audit a échoué");
        return;
      }
      if (body.status === "done") {
        setTeaser(body.teaser);
        setPhase("done");
        return;
      }
      setPhase(body.status);
    }
  }, []);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setError(null);
    setTeaser(null);

    if (!token) {
      setError("attendez la fin de la vérification anti-robot");
      return;
    }
    setPhase("queued");

    const response = await fetch("/api/audit", {
      method: "POST",
      headers: { "content-type": "application/json" },
      credentials: "include",
      body: JSON.stringify({ url, turnstileToken: token }),
    });
    // A Turnstile token is single-use: whatever happened, the current one is
    // spent and the widget needs a new one before a retry.
    window.turnstile?.reset(widgetId.current ?? undefined);
    setToken(null);

    if (!response.ok) {
      const body = (await response.json().catch(() => ({}))) as { error?: string };
      setPhase("failed");
      setError(body.error ?? "la requête a été refusée");
      return;
    }
    const { id, claim: sealed } = (await response.json()) as { id: string; claim: string };
    // Carried to /auth in the link below: the cookie the response also set is
    // third-party once this form is hosted on one of the other two sites, and
    // most browsers will not give it back on the way in.
    setClaim(sealed);
    await poll(id);
  }

  const busy = phase === "queued" || phase === "running";

  return (
    <div className="w-full max-w-xl">
      <form onSubmit={submit} className="flex flex-col gap-4">
        <label className="flex flex-col gap-2">
          <span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">
            Votre domaine
          </span>
          <input
            type="text"
            value={url}
            onChange={(event) => setUrl(event.target.value)}
            placeholder="exemple.com"
            autoComplete="url"
            required
            disabled={busy}
            className="rounded-lg border border-zinc-300 bg-white px-4 py-3 text-base text-zinc-900 outline-none placeholder:text-zinc-400 focus:border-zinc-900 disabled:opacity-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:focus:border-zinc-100"
          />
        </label>

        <div ref={widgetHost} />

        <button
          type="submit"
          disabled={busy || !token}
          className="rounded-lg bg-zinc-900 px-5 py-3 text-base font-medium text-white transition-colors hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
        >
          {busy ? "Audit en cours…" : "Auditer mon site"}
        </button>
      </form>

      {phase === "queued" && <Note>En file d'attente…</Note>}
      {phase === "running" && <Note>Le robot lit vos pages…</Note>}
      {error && (
        <p className="mt-4 rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-800 dark:border-red-900 dark:bg-red-950 dark:text-red-200">
          {error}
        </p>
      )}

      {teaser && <TeaserCard teaser={teaser} claim={claim} />}
    </div>
  );
}

function Note({ children }: { children: React.ReactNode }) {
  return <p className="mt-4 text-sm text-zinc-500 dark:text-zinc-400">{children}</p>;
}

/**
 * The free half of the product.
 *
 * It says how bad it is and names the two problems, and stops there — no URL,
 * no message, no page list. The rest is behind the email, which is the trade
 * the visitor is being offered.
 */
function TeaserCard({ teaser, claim }: { teaser: Teaser; claim: string | null }) {
  // Green is a claim about the site. A run that verified nothing has no claim to
  // make, so it does not get to borrow the reassuring colour.
  const alarming =
    teaser.verdict === "blocking" ||
    teaser.verdict === "fatal" ||
    teaser.verdict === "unverified";
  return (
    <section className="mt-6 rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900">
      <p
        className={`text-lg font-semibold ${
          alarming ? "text-red-700 dark:text-red-400" : "text-emerald-700 dark:text-emerald-400"
        }`}
      >
        {VERDICT_LABEL[teaser.verdict]}
      </p>

      <dl className="mt-4 grid grid-cols-3 gap-4 text-center">
        <Stat label="Bloquants" value={teaser.blocking} strong={teaser.blocking > 0} />
        <Stat label="Avertissements" value={teaser.warnings} />
        <Stat label="Pages vues" value={`${teaser.checked}/${teaser.total}`} />
      </dl>

      {teaser.topKinds.length > 0 && (
        <ul className="mt-5 space-y-1.5 text-sm text-zinc-700 dark:text-zinc-300">
          {teaser.topKinds.map((entry) => (
            <li key={entry.kind} className="flex justify-between gap-4">
              <span>{labelForKind(entry.kind)}</span>
              <span className="tabular-nums text-zinc-500 dark:text-zinc-400">
                {entry.count} page{entry.count > 1 ? "s" : ""}
              </span>
            </li>
          ))}
        </ul>
      )}

      <Link
        href={claim ? `/auth?claim=${encodeURIComponent(claim)}` : "/auth"}
        className="mt-6 inline-block rounded-lg bg-zinc-900 px-4 py-2.5 text-sm font-medium text-white hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
      >
        Recevoir le rapport complet
      </Link>
      <p className="mt-2 text-xs text-zinc-500 dark:text-zinc-400">
        Les URL concernées, le détail par page et les métriques sont dans le rapport.
      </p>
    </section>
  );
}

function Stat({
  label,
  value,
  strong = false,
}: {
  label: string;
  value: number | string;
  strong?: boolean;
}) {
  return (
    <div>
      <dd
        className={`text-2xl font-semibold tabular-nums ${
          strong ? "text-red-700 dark:text-red-400" : "text-zinc-900 dark:text-zinc-100"
        }`}
      >
        {value}
      </dd>
      <dt className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">{label}</dt>
    </div>
  );
}
