// src/components/pages/pages-view.tsx — inventaire des pages + crawl.
// Table (path, statut, title/desc + longueur, H1, issues, nb kw), recherche
// debounced (refetch server), filtre issue, bouton « Crawler le site » qui
// enqueue puis poll `getAnalyticsJob` (même pattern setInterval/useRef/cleanup
// que import-keywords-dialog) avec barre de progression phase/done/total et
// toast récap. Clic ligne → sheet détail (métas complètes + kw liés).
"use client";

import { FileSearchIcon, RefreshCwIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
} from "@/components/ui/sheet";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { getAnalyticsJob } from "@/lib/analytics/actions";
import {
  fetchPageDetailAction,
  fetchSitePagesAction,
  getActiveSiteCrawlAction,
  type PageDetail,
  type SitePageRow,
  startSiteCrawlAction,
} from "@/lib/pages/actions";

const POLL_MS = 2000;

/** Codes d'issue connus (spec §Issues) — pilotent le filtre et les badges. */
const ISSUE_CODES = [
  "title-missing",
  "title-long",
  "title-short",
  "description-missing",
  "description-long",
  "description-short",
  "h1-missing",
  "h1-multiple",
  "noindex",
  "canonical-mismatch",
  "error",
] as const;

type IssueCode = (typeof ISSUE_CODES)[number];

/** Miroir client de `CrawlTotals` (crawl-runner, server-only) : le totals jsonb
 *  renvoyé par getAnalyticsJob est typé large côté action. */
type CrawlProgress = {
  phase: "discover" | "crawl" | "map" | "done";
  done: number;
  total: number;
  pages: number;
  errors: number;
  mapped: number;
  unmapped: number;
};

/** Chemin (path) court à partir d'une URL absolue, pour la colonne principale. */
function toPath(url: string): string {
  try {
    const u = new URL(url);
    return `${u.pathname}${u.search}` || "/";
  } catch {
    return url;
  }
}

function statusVariant(
  status: number,
): "secondary" | "destructive" | "outline" {
  if (status === 0 || status >= 400) return "destructive";
  if (status >= 300) return "secondary";
  return "outline";
}

export function PagesView({
  siteId,
  initialPages,
}: {
  siteId: string;
  initialPages: SitePageRow[];
}) {
  const t = useTranslations("Pages");
  const router = useRouter();

  const [pages, setPages] = useState<SitePageRow[]>(initialPages);
  const [query, setQuery] = useState("");
  const [issue, setIssue] = useState<string>("all");
  const [crawling, setCrawling] = useState(false);
  const [progress, setProgress] = useState<CrawlProgress | null>(null);

  const [detail, setDetail] = useState<PageDetail | null>(null);
  const [detailOpen, setDetailOpen] = useState(false);
  const [detailLoading, setDetailLoading] = useState(false);

  const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
  // The server already provided `initialPages` for this site (empty query, issue
  // "all"). Track the loaded params so we never refetch on mount — a boolean ref
  // is defeated by React Strict Mode's double-invoked effects; a key is not.
  const loadedKey = useRef(`${siteId}|${query.trim()}|${issue}`);
  // Per-site guard so we attach to an in-flight crawl exactly once. A boolean is
  // defeated by Strict Mode's double-invoked effect; a key keyed on siteId is not
  // (and it re-arms when the active site changes).
  const attachedKey = useRef<string | null>(null);

  function clearPoll() {
    if (pollRef.current) {
      clearInterval(pollRef.current);
      pollRef.current = null;
    }
  }

  // Clear any pending poll on unmount (read the ref inside so there are no deps).
  useEffect(
    () => () => {
      if (pollRef.current) clearInterval(pollRef.current);
    },
    [],
  );

  // Debounced server refetch on search / issue change. Skips when the current
  // params match the already-loaded key so the RSC-provided `initialPages` are
  // not immediately re-fetched (survives Strict Mode's double-invoked effect).
  useEffect(() => {
    const key = `${siteId}|${query.trim()}|${issue}`;
    if (key === loadedKey.current) return;
    const handle = setTimeout(async () => {
      const rows = await fetchSitePagesAction(siteId, {
        q: query.trim() || undefined,
        issue: issue === "all" ? undefined : issue,
      });
      loadedKey.current = key;
      setPages(rows);
    }, 300);
    return () => clearTimeout(handle);
  }, [siteId, query, issue]);

  // On mount and whenever the active site changes, look for an already-running
  // crawl (reload mid-crawl, other tab, API-triggered enqueue) and attach the
  // progress bar to it. Keyed guard survives Strict Mode's double-invoke; the
  // unmount cleanup (clearPoll effect above) tears the poll down.
  // biome-ignore lint/correctness/useExhaustiveDependencies: attach once per site — only siteId drives this; clearPoll/startPolling are stable component-scoped declarations and the attachedKey guard makes re-runs idempotent.
  useEffect(() => {
    if (attachedKey.current === siteId) return;
    // Switching sites: drop any poll/bar from the previous site before probing.
    clearPoll();
    setCrawling(false);
    setProgress(null);
    attachedKey.current = siteId;
    let cancelled = false;
    (async () => {
      const active = await getActiveSiteCrawlAction(siteId);
      if (cancelled || !active) return;
      setCrawling(true);
      startPolling(active.jobId);
    })();
    return () => {
      cancelled = true;
    };
  }, [siteId]);

  async function refetch() {
    const rows = await fetchSitePagesAction(siteId, {
      q: query.trim() || undefined,
      issue: issue === "all" ? undefined : issue,
    });
    setPages(rows);
  }

  function startPolling(jobId: string) {
    clearPoll();
    pollRef.current = setInterval(async () => {
      const job = await getAnalyticsJob(jobId);
      if (!job) return;
      const totals = job.totals as unknown as Partial<CrawlProgress>;
      if (
        totals &&
        typeof totals.phase === "string" &&
        typeof totals.done === "number"
      ) {
        setProgress(totals as CrawlProgress);
      }
      if (job.status === "done") {
        clearPoll();
        setCrawling(false);
        setProgress(null);
        const p = (job.totals as unknown as CrawlProgress) ?? null;
        toast.success(
          t("toastDone", {
            pages: p?.pages ?? 0,
            errors: p?.errors ?? 0,
            mapped: p?.mapped ?? 0,
            unmapped: p?.unmapped ?? 0,
          }),
        );
        await refetch();
        router.refresh();
      } else if (job.status === "failed") {
        clearPoll();
        setCrawling(false);
        setProgress(null);
        toast.error(job.error ?? t("toastFailed"));
      }
    }, POLL_MS);
  }

  async function crawl() {
    setCrawling(true);
    setProgress(null);
    try {
      const res = await startSiteCrawlAction(siteId);
      if (!res.ok) {
        // Déjà en cours : au lieu du seul toast, rattache la barre au job actif
        // (crawl lancé ailleurs — autre onglet, API) pour ne pas rester aveugle.
        if (res.error === "already-running") {
          const active = await getActiveSiteCrawlAction(siteId);
          if (active) {
            startPolling(active.jobId);
            return;
          }
        }
        setCrawling(false);
        const msg =
          res.error === "missing-url"
            ? t("errorMissingUrl")
            : res.error === "already-running"
              ? t("errorAlreadyRunning")
              : t("toastFailed");
        toast.error(msg);
        return;
      }
      toast.success(t("crawlStarted"));
      startPolling(res.jobId);
    } catch {
      // Race du double-clic : le service peut refuser (409) et enqueue jette au
      // lieu de renvoyer `already-running` — même traitement qu'un job actif.
      setCrawling(false);
      toast.error(t("errorAlreadyRunning"));
    }
  }

  async function openDetail(pageId: string) {
    setDetailOpen(true);
    setDetailLoading(true);
    setDetail(null);
    const d = await fetchPageDetailAction(siteId, pageId);
    setDetail(d);
    setDetailLoading(false);
  }

  const pct =
    progress && progress.total > 0
      ? Math.min(100, Math.round((progress.done / progress.total) * 100))
      : null;

  return (
    <div className="flex flex-col gap-4 p-4">
      <div className="flex flex-wrap items-center justify-between gap-3">
        <div className="flex flex-wrap items-center gap-2">
          <Input
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder={t("searchPlaceholder")}
            className="w-64"
            aria-label={t("searchPlaceholder")}
          />
          <Select value={issue} onValueChange={setIssue}>
            <SelectTrigger className="w-52" aria-label={t("filterIssue")}>
              <SelectValue />
            </SelectTrigger>
            <SelectContent position="popper">
              <SelectItem value="all">{t("filterIssueAll")}</SelectItem>
              {ISSUE_CODES.map((code) => (
                <SelectItem key={code} value={code}>
                  {t(`issues.${code as IssueCode}`)}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
        <Button onClick={crawl} disabled={crawling}>
          <RefreshCwIcon
            className={crawling ? "size-4 animate-spin" : "size-4"}
          />
          {crawling ? t("crawling") : t("crawlButton")}
        </Button>
      </div>

      {crawling ? (
        <div className="flex flex-col gap-1.5">
          <div className="flex items-center justify-between text-muted-foreground text-xs">
            <span>
              {progress ? t(`phase.${progress.phase}`) : t("phase.discover")}
            </span>
            {progress && progress.total > 0 ? (
              <span>
                {progress.done} / {progress.total}
              </span>
            ) : null}
          </div>
          <div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
            <div
              className="h-full rounded-full bg-primary transition-all"
              style={{ width: `${pct ?? 10}%` }}
            />
          </div>
        </div>
      ) : null}

      {pages.length === 0 ? (
        <div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed py-16 text-center">
          <FileSearchIcon className="size-8 text-muted-foreground" />
          <p className="font-medium">{t("emptyTitle")}</p>
          <p className="max-w-md text-muted-foreground text-sm">
            {t("emptyDescription")}
          </p>
        </div>
      ) : (
        <div className="overflow-x-auto rounded-lg border">
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("colPath")}</TableHead>
                <TableHead className="w-20">{t("colStatus")}</TableHead>
                <TableHead>{t("colTitle")}</TableHead>
                <TableHead>{t("colDescription")}</TableHead>
                <TableHead>{t("colH1")}</TableHead>
                <TableHead>{t("colIssues")}</TableHead>
                <TableHead className="w-16 text-right">
                  {t("colKeywords")}
                </TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {pages.map((p) => (
                <TableRow
                  key={p.id}
                  className="cursor-pointer"
                  onClick={() => openDetail(p.id)}
                >
                  <TableCell
                    className="max-w-[16rem] truncate font-medium"
                    title={p.url}
                  >
                    {toPath(p.url)}
                  </TableCell>
                  <TableCell>
                    <Badge variant={statusVariant(p.status)}>
                      {p.status || "—"}
                    </Badge>
                  </TableCell>
                  <TableCell className="max-w-[14rem]">
                    <span className="block truncate">{p.title ?? "—"}</span>
                    {p.title ? (
                      <span className="text-muted-foreground text-xs">
                        {t("chars", { count: p.titleLength })}
                      </span>
                    ) : null}
                  </TableCell>
                  <TableCell className="max-w-[14rem]">
                    <span className="block truncate">
                      {p.metaDescription ?? "—"}
                    </span>
                    {p.metaDescription ? (
                      <span className="text-muted-foreground text-xs">
                        {t("chars", { count: p.descriptionLength })}
                      </span>
                    ) : null}
                  </TableCell>
                  <TableCell className="max-w-[12rem]">
                    <span className="block truncate">{p.h1 ?? "—"}</span>
                  </TableCell>
                  <TableCell>
                    <div className="flex flex-wrap gap-1">
                      {p.issues.map((code) => (
                        <Badge
                          key={code}
                          variant={
                            code === "error" || code === "noindex"
                              ? "destructive"
                              : "secondary"
                          }
                        >
                          {t(`issues.${code as IssueCode}`)}
                        </Badge>
                      ))}
                    </div>
                  </TableCell>
                  <TableCell className="text-right tabular-nums">
                    {p.keywordCount}
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </div>
      )}

      <Sheet open={detailOpen} onOpenChange={setDetailOpen}>
        <SheetContent className="w-full overflow-y-auto sm:max-w-md">
          <SheetHeader>
            <SheetTitle className="break-all">
              {detail ? toPath(detail.page.url) : t("sheetLoading")}
            </SheetTitle>
            {detail ? (
              <SheetDescription className="break-all">
                {detail.page.url}
              </SheetDescription>
            ) : null}
          </SheetHeader>
          {detailLoading ? (
            <p className="px-4 text-muted-foreground text-sm">
              {t("sheetLoading")}
            </p>
          ) : detail ? (
            <div className="flex flex-col gap-4 px-4 pb-4">
              <dl className="grid grid-cols-1 gap-3 text-sm">
                <DetailRow label={t("sheetStatus")}>
                  <Badge variant={statusVariant(detail.page.status)}>
                    {detail.page.status || "—"}
                  </Badge>
                </DetailRow>
                <DetailRow label={t("colTitle")}>
                  {detail.page.title ?? "—"}
                  {detail.page.title ? (
                    <span className="ml-1 text-muted-foreground text-xs">
                      ({t("chars", { count: detail.page.titleLength })})
                    </span>
                  ) : null}
                </DetailRow>
                <DetailRow label={t("colDescription")}>
                  {detail.page.metaDescription ?? "—"}
                  {detail.page.metaDescription ? (
                    <span className="ml-1 text-muted-foreground text-xs">
                      ({t("chars", { count: detail.page.descriptionLength })})
                    </span>
                  ) : null}
                </DetailRow>
                <DetailRow label={t("colH1")}>
                  {detail.page.h1 ?? "—"}
                </DetailRow>
                <DetailRow label={t("sheetCanonical")}>
                  {detail.page.canonical ?? "—"}
                </DetailRow>
                <DetailRow label={t("sheetRobots")}>
                  {detail.page.robots ?? "—"}
                </DetailRow>
                <DetailRow label={t("sheetLang")}>
                  {detail.page.lang ?? "—"}
                </DetailRow>
                <DetailRow label={t("sheetWordCount")}>
                  {detail.page.wordCount}
                </DetailRow>
              </dl>

              {detail.page.issues.length > 0 ? (
                <div className="flex flex-col gap-1.5">
                  <p className="font-medium text-sm">{t("colIssues")}</p>
                  <div className="flex flex-wrap gap-1">
                    {detail.page.issues.map((code) => (
                      <Badge
                        key={code}
                        variant={
                          code === "error" || code === "noindex"
                            ? "destructive"
                            : "secondary"
                        }
                      >
                        {t(`issues.${code as IssueCode}`)}
                      </Badge>
                    ))}
                  </div>
                </div>
              ) : null}

              <div className="flex flex-col gap-1.5">
                <div className="flex items-center justify-between">
                  <p className="font-medium text-sm">{t("sheetKeywords")}</p>
                  {detail.keywords.length > 0 ? (
                    <Button
                      variant="link"
                      size="sm"
                      className="h-auto p-0"
                      onClick={() => router.push("/keywords/reservoir")}
                    >
                      {t("sheetViewReservoir")}
                    </Button>
                  ) : null}
                </div>
                {detail.keywords.length === 0 ? (
                  <p className="text-muted-foreground text-sm">
                    {t("sheetNoKeywords")}
                  </p>
                ) : (
                  <ul className="flex flex-col gap-1">
                    {detail.keywords.map((k) => (
                      <li
                        key={k.id}
                        className="flex items-center justify-between text-sm"
                      >
                        <span className="truncate">{k.term}</span>
                        {k.source ? (
                          <Badge variant="outline">{k.source}</Badge>
                        ) : null}
                      </li>
                    ))}
                  </ul>
                )}
              </div>
            </div>
          ) : (
            <p className="px-4 text-muted-foreground text-sm">
              {t("sheetNotFound")}
            </p>
          )}
        </SheetContent>
      </Sheet>
    </div>
  );
}

function DetailRow({
  label,
  children,
}: {
  label: string;
  children: React.ReactNode;
}) {
  return (
    <div className="flex flex-col gap-0.5">
      <dt className="text-muted-foreground text-xs">{label}</dt>
      <dd className="break-words">{children}</dd>
    </div>
  );
}
