// src/components/content/serp-panel.tsx — competitor SERP table for a keyword locale.
"use client";

import { ImportIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  SERP_IMPORT_MAX_FILE_SIZE,
  useSerpImport,
} from "@/hooks/use-serp-import";
import { getAnalyticsJob } from "@/lib/analytics/actions";
import { crawlSerpForKeywordAction } from "@/lib/keywords/serp-actions";
import type { SerpCompetitor, SerpSnapshot } from "@/lib/keywords/serp-types";

function fmt(n: number | null): string {
  return n == null ? "—" : n.toLocaleString("fr-FR");
}

// crawl-onpage job poll cadence + safety cap — mirrors ImportKeywordsDialog's
// startPolling (2 s) with a 6 min ceiling on top (the analytics service's own
// batch-scrape deadline is 5 min, cf. crawl-onpage.ts header note; this is
// just belt-and-braces against a wedged poll on THIS client).
const CRAWL_POLL_MS = 2000;
const CRAWL_POLL_MAX_ATTEMPTS = 180;

/** Progress/result shape written to `analyticsJobs.totals` for a
 *  `crawl-onpage` job (`completeJob(job.id, { crawled, blocked, failed })` in
 *  `services/analytics/src/crawl-onpage.ts`). The shared
 *  `AnalyticsJobStatus.totals` type only declares the csv-import shape, so
 *  this is a local, documented cast (same pattern as `SemanticJobTotals` in
 *  semantic-analysis-section.tsx). */
type CrawlOnpageJobTotals = {
  crawled?: number;
  blocked?: number;
  failed?: number;
};

export function SerpPanel({
  keywordId,
  snapshot,
  onImported,
}: {
  keywordId: string;
  snapshot: SerpSnapshot | null;
  onImported: () => void;
}) {
  const inputRef = useRef<HTMLInputElement>(null);
  const [crawling, setCrawling] = useState(false);
  const [dragging, setDragging] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [note, setNote] = useState<string | null>(null);
  const [expanded, setExpanded] = useState<string | null>(null);
  const crawlPollRef = useRef<ReturnType<typeof setInterval> | null>(null);

  function clearCrawlPoll() {
    if (crawlPollRef.current) {
      clearInterval(crawlPollRef.current);
      crawlPollRef.current = null;
    }
  }

  // Clear any pending poll on unmount (reads the ref inside, no deps needed —
  // same pattern as ImportKeywordsDialog).
  // biome-ignore lint/correctness/useExhaustiveDependencies: clearCrawlPoll only touches a ref (stable identity), safe to omit from deps.
  useEffect(() => clearCrawlPoll, []);

  const { importFile: runImport, busy } = useSerpImport(keywordId, {
    onSuccess: onImported,
    onError: setError,
  });

  async function importFile(file: File) {
    // La garde de taille (dans le hook) ne doit pas effacer une note de
    // crawl existante — seul un import réellement lancé le fait, comme
    // avant la factorisation.
    if (file.size > SERP_IMPORT_MAX_FILE_SIZE) {
      await runImport(file);
      return;
    }
    setError(null);
    setNote(null);
    await runImport(file);
  }

  function onFile(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    e.target.value = "";
    if (file) void importFile(file);
  }

  function onDrop(e: React.DragEvent) {
    e.preventDefault();
    setDragging(false);
    if (busy) return;
    const file = e.dataTransfer.files?.[0];
    if (file) void importFile(file);
  }

  // Enqueues a `crawl-onpage` analytics job (server no longer fetches
  // synchronously) and polls it every 2 s until it settles, keeping
  // `crawling` up for the whole poll — same inline feedback channel
  // (error/note) as the SERP import above, no toast in this panel.
  function pollCrawlJob(jobId: string) {
    clearCrawlPoll();
    let attempts = 0;
    crawlPollRef.current = setInterval(async () => {
      // Cap checked FIRST, before the fetch: a job stuck returning `null`
      // (see the `!job` guard below) or a fetch that keeps throwing must
      // still count towards the ceiling, otherwise the poll never stops.
      attempts++;
      if (attempts >= CRAWL_POLL_MAX_ATTEMPTS) {
        clearCrawlPoll();
        setCrawling(false);
        setError("Crawl : toujours en cours après 6 min, réessayez plus tard.");
        return;
      }
      try {
        const job = await getAnalyticsJob(jobId);
        if (!job) return;
        if (job.status === "done") {
          clearCrawlPoll();
          setCrawling(false);
          const totals = job.totals as CrawlOnpageJobTotals;
          setNote(
            `Crawl : ${totals.crawled ?? 0} ok${totals.blocked ? `, ${totals.blocked} bloqué(s)` : ""}${totals.failed ? `, ${totals.failed} échec(s)` : ""}`,
          );
          onImported();
          return;
        }
        if (job.status === "failed") {
          clearCrawlPoll();
          setCrawling(false);
          setError(job.error ?? "Crawl échoué");
          return;
        }
        // still queued/running — keep `crawling` up, the cap above will
        // eventually trigger if this never settles.
      } catch {
        // Network hiccup fetching job status — swallow and let the next tick
        // retry; the cap above bounds how long this can go on for.
      }
    }, CRAWL_POLL_MS);
  }

  async function crawl() {
    setCrawling(true);
    setError(null);
    setNote(null);
    try {
      const res = await crawlSerpForKeywordAction(keywordId);
      if (!res.ok) {
        setError(res.error ?? "Crawl échoué");
        setCrawling(false);
        return;
      }
      if (!res.jobId) {
        // 409: an identical job is already queued/running for this keyword —
        // nothing new to poll, just say so.
        setNote("Crawl déjà en cours.");
        setCrawling(false);
        return;
      }
      pollCrawlJob(res.jobId);
    } catch {
      setError("Crawl impossible.");
      setCrawling(false);
    }
  }

  return (
    <div className="rounded-lg border p-3">
      <div className="mb-2 flex items-center justify-between gap-2">
        <div className="font-medium text-sm">SERP / Concurrents</div>
        <div className="flex gap-2">
          {snapshot ? (
            <Button
              size="sm"
              variant="outline"
              disabled={crawling}
              onClick={crawl}
            >
              {crawling ? "Crawl…" : "Crawler on-page"}
            </Button>
          ) : null}
          <Button
            size="sm"
            variant="outline"
            disabled={busy}
            onClick={() => inputRef.current?.click()}
          >
            {busy ? "Import…" : "Importer export SERP"}
          </Button>
          <input
            ref={inputRef}
            type="file"
            accept=".csv,text/csv"
            className="hidden"
            onChange={onFile}
          />
        </div>
      </div>

      {error ? <p className="mb-2 text-destructive text-xs">{error}</p> : null}
      {note ? (
        <p className="mb-2 text-muted-foreground text-xs">{note}</p>
      ) : null}

      {!snapshot ? (
        // Dashed drop zone — mirrors the keyword/brief import dialogs so every
        // import in the app shares the same drag-and-drop affordance.
        <button
          type="button"
          disabled={busy}
          onClick={() => inputRef.current?.click()}
          onDragOver={(e) => {
            e.preventDefault();
            if (!busy) setDragging(true);
          }}
          onDragLeave={(e) => {
            if (e.currentTarget === e.target) setDragging(false);
          }}
          onDrop={onDrop}
          className={`flex h-28 w-full flex-col items-center justify-center gap-2 rounded-lg border border-input border-dashed text-muted-foreground text-sm transition-colors ${
            dragging ? "border-primary bg-primary/5 text-foreground" : ""
          }`}
        >
          <ImportIcon className="size-5" />
          {busy
            ? "Import…"
            : dragging
              ? "Dépose le fichier pour l'importer…"
              : "Dépose un export « SERP URLs » (SE Ranking / SerpChecker), ou clique"}
        </button>
      ) : (
        <>
          {snapshot.features.length > 0 ? (
            <div className="mb-2 flex flex-wrap gap-1">
              {snapshot.features.map((f) => (
                <Badge key={f} variant="secondary">
                  {f}
                </Badge>
              ))}
            </div>
          ) : null}
          <div className="overflow-x-auto">
            <table className="w-full text-xs">
              <thead className="text-muted-foreground">
                <tr className="border-b text-left">
                  <th className="py-1 pr-2">#</th>
                  <th className="py-1 pr-2">Domaine</th>
                  <th className="py-1 pr-2 text-right">Backlinks</th>
                  <th className="py-1 pr-2 text-right">Ref.dom</th>
                  <th className="py-1 pr-2 text-right">Page AS</th>
                  <th className="py-1 pr-2 text-right">Trafic</th>
                  <th className="py-1 pr-2">Meta</th>
                  <th className="py-1 pr-2 text-right">Img</th>
                  <th className="py-1 pr-2 text-right">Alt</th>
                  <th className="py-1">JSON-LD</th>
                </tr>
              </thead>
              <tbody>
                {snapshot.competitors.map((c) => (
                  <CompetitorRows
                    key={c.url}
                    c={c}
                    open={expanded === c.url}
                    onToggle={() =>
                      setExpanded((cur) => (cur === c.url ? null : c.url))
                    }
                  />
                ))}
              </tbody>
            </table>
          </div>
          <p className="mt-2 text-muted-foreground text-[11px]">
            {snapshot.crawledAt
              ? `On-page crawlé le ${new Date(snapshot.crawledAt).toLocaleString("fr-FR")} — clique une ligne pour les alt par image.`
              : "Meta / Img / Alt / JSON-LD : lance « Crawler on-page »."}
          </p>
        </>
      )}
    </div>
  );
}

function CompetitorRows({
  c,
  open,
  onToggle,
}: {
  c: SerpCompetitor;
  open: boolean;
  onToggle: () => void;
}) {
  const crawled = c.crawledAt != null;
  return (
    <>
      <tr
        className={`border-b last:border-0 ${crawled ? "cursor-pointer" : ""}`}
        onClick={crawled ? onToggle : undefined}
      >
        <td className="py-1 pr-2">
          {crawled ? (
            <button
              type="button"
              aria-expanded={open}
              aria-label={`Afficher le détail de ${c.domain}`}
              className="inline-flex items-center gap-1 rounded-sm focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2"
              onClick={(e) => {
                e.stopPropagation();
                onToggle();
              }}
            >
              <span aria-hidden="true">{open ? "▾" : "▸"}</span>
              {c.position}
            </button>
          ) : (
            c.position
          )}
        </td>
        <td className="py-1 pr-2">
          <a
            href={c.url}
            target="_blank"
            rel="noopener noreferrer"
            className="text-primary hover:underline"
            onClick={(e) => e.stopPropagation()}
          >
            {c.domain}
          </a>
        </td>
        <td className="py-1 pr-2 text-right">{fmt(c.backlinks)}</td>
        <td className="py-1 pr-2 text-right">{fmt(c.refDomains)}</td>
        <td className="py-1 pr-2 text-right">{fmt(c.pageAuthority)}</td>
        <td className="py-1 pr-2 text-right">{fmt(c.searchTraffic)}</td>
        <td className="py-1 pr-2" title={c.metaDescription ?? undefined}>
          {c.metaDescription ? "✓" : "—"}
        </td>
        <td className="py-1 pr-2 text-right">{fmt(c.images)}</td>
        <td
          className={`py-1 pr-2 text-right ${c.imagesWithoutAlt ? "text-destructive" : ""}`}
        >
          {c.imagesWithoutAlt == null ? "—" : `${c.imagesWithoutAlt} sans alt`}
        </td>
        <td className="py-1" title={c.jsonLdTypes?.join(", ") || undefined}>
          {c.hasJsonLd == null ? "—" : c.hasJsonLd ? "✓" : "✗"}
        </td>
      </tr>
      {open && crawled ? (
        <tr className="border-b bg-muted/30 last:border-0">
          <td colSpan={10} className="px-2 py-2">
            <div className="mb-1 text-muted-foreground text-[11px]">
              {c.title ?? "—"}
              {c.lang ? ` · ${c.lang}` : ""}
              {c.jsonLdTypes?.length
                ? ` · JSON-LD: ${c.jsonLdTypes.join(", ")}`
                : ""}
            </div>
            {c.imageAlts?.length ? (
              <table className="w-full text-[11px]">
                <tbody>
                  {c.imageAlts.map((img, i) => (
                    // biome-ignore lint/suspicious/noArrayIndexKey: static crawl snapshot, never reordered; src can repeat/empty so index disambiguates
                    <tr key={`${i}-${img.src}`} className="align-top">
                      <td className="w-1/2 truncate pr-2 text-muted-foreground">
                        {img.src}
                      </td>
                      <td className={img.alt.trim() ? "" : "text-destructive"}>
                        {img.alt.trim() ? img.alt : "(alt vide)"}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            ) : (
              <p className="text-muted-foreground text-[11px]">Aucune image.</p>
            )}
          </td>
        </tr>
      ) : null}
    </>
  );
}
