// src/lib/gsc/import-actions.ts — GSC discovery preview + commit, gatés par site.
"use server";
import { and, eq } from "drizzle-orm";
import * as wasm from "import-core";
import { revalidatePath } from "next/cache";
import { getActiveOrgId } from "@/lib/active-org";
import { db } from "@/lib/db/client";
import {
  keywords as keywordsTable,
  sites as sitesTable,
} from "@/lib/db/domain-schema";
import { currentUserCanAccessSite } from "@/lib/sites/access";
import { siteBelongsToOrg } from "@/lib/sites/assert-site";
import { createGscKeywordsForSite } from "./discovery";
import { gscFailureMessage } from "./failure-message";
import type {
  GscImportCommitResult,
  GscImportPreview,
  GscImportRow,
} from "./import-types";
import {
  type QueryCountryImpressions,
  resolveGscCountry,
  resolveQueryLanguages,
} from "./language";
import {
  querySearchAnalytics,
  querySearchAnalyticsByCountry,
} from "./search-analytics";
import { resolveSiteGscToken } from "./site-token";
import type { GscQueryRow } from "./types";

/** Fetch the active site's GSC queries, annotated new/existing vs the reservoir. */
export async function previewGscImportAction(
  siteId: string,
): Promise<GscImportPreview> {
  if (!(await currentUserCanAccessSite(siteId))) {
    return { ok: false, error: "Accès refusé à ce site." };
  }
  // DEUX gardes, pas une : `currentUserCanAccessSite` relit le rôle dans l'org
  // PROPRIÉTAIRE du site, pas dans l'org active. Un utilisateur membre de A
  // (session active) et owner de B passerait donc la première garde sur un site
  // de B — 25 000 requêtes GSC de B rendues dans une session A, et le `select`
  // sur `keywords` plus bas n'est scopé que par siteId. Le cloisonnement de
  // session tient à ce second contrôle. Ordre volontaire : l'accès d'abord,
  // même message ensuite — aucun oracle d'énumération inter-organisations.
  if (!(await siteBelongsToOrg(siteId, await getActiveOrgId()))) {
    return { ok: false, error: "Accès refusé à ce site." };
  }
  // Propriété + autorité + compte épinglé + jeton frais : un seul appel, le même
  // que le worker. La propriété sort de la résolution — pas de seconde lecture
  // de la table, qui pourrait diverger de ce sur quoi le jeton a été obtenu.
  const resolved = await resolveSiteGscToken(siteId);
  if (!resolved.ok) {
    return { ok: false, error: gscFailureMessage(resolved.reason) };
  }

  let queryRows: GscQueryRow[];
  try {
    queryRows = await querySearchAnalytics(
      resolved.accessToken,
      resolved.property,
      { days: 28, rowLimit: 25000 },
    );
  } catch {
    return { ok: false, error: "Échec de la récupération GSC." };
  }
  // Annotate by the deterministic id (siteId + slug) so the preview matches the
  // real dedup (commit upserts on keywords.id), not just an exact-term match.
  const existing = new Set(
    (
      await db
        .select({ id: keywordsTable.id })
        .from(keywordsTable)
        .where(eq(keywordsTable.siteId, siteId))
    ).map((r) => r.id),
  );
  const rows: GscImportRow[] = queryRows
    .map((r) => ({
      ...r,
      status: existing.has(`${siteId}-${wasm.slugify(r.query)}`)
        ? ("existing" as const)
        : ("new" as const),
    }))
    .sort((a, b) => b.impressions - a.impressions);
  return { ok: true, rows, total: queryRows.length };
}

/** Create the selected GSC queries as keywords, routed by destination. */
export async function commitGscImportAction(
  siteId: string,
  queries: GscQueryRow[],
  destination: "reservoir" | "recherche",
): Promise<GscImportCommitResult> {
  if (!(await currentUserCanAccessSite(siteId))) {
    return { ok: false, error: "Accès refusé à ce site." };
  }
  const orgId = await getActiveOrgId();
  const [site] = await db
    .select({
      lang: sitesTable.primaryLanguage,
      languages: sitesTable.languages,
      countries: sitesTable.targetCountries,
    })
    .from(sitesTable)
    .where(and(eq(sitesTable.id, siteId), eq(sitesTable.orgId, orgId)));
  if (!site) return { ok: false, error: "Site introuvable." };
  const stages: ("research" | "planning")[] =
    destination === "recherche" ? ["research"] : [];
  const fallbackCountry = site.countries[0] ?? "US";

  // Attribue chaque requête au pays où elle rank RÉELLEMENT (breakdown GSC par
  // pays, en préférant les pays cibles) et à une langue configurée — même
  // pipeline que « Synchroniser GSC ». Fini le tout-taguer-countries[0]. Best-
  // effort : si le breakdown échoue (token/API), fallback pays primaire.
  //
  // Best-effort assumé, y compris sur la résolution du jeton : une raison typée
  // (`no-property`, `token-refused`…) ne doit PAS faire échouer le commit et
  // perdre la sélection de l'utilisateur — les mots-clés sont créés, simplement
  // tagués langue/pays primaires. Le message d'échec est rendu là où il est
  // actionnable : la preview, qui précède toujours ce commit.
  //
  // La résolution reste DANS le `try` : `resolveSiteGscToken` ne rattrape que
  // l'échec de `getAccessToken`, ses lectures SQL (dérivation de l'état +
  // revalidation de l'accès de l'autorité) peuvent jeter — pool saturé, par
  // exemple. Hors du `try`, un incident base ferait échouer tout l'import au
  // lieu de retomber sur le pays primaire, exactement ce que ce repli existe
  // pour éviter.
  let queryLang: Map<string, string | null> | undefined;
  let queryCountries: Map<string, QueryCountryImpressions[]> | undefined;
  try {
    const resolved = await resolveSiteGscToken(siteId);
    if (resolved.ok) {
      queryCountries = new Map<string, QueryCountryImpressions[]>();
      for (const r of await querySearchAnalyticsByCountry(
        resolved.accessToken,
        resolved.property,
        { days: 28, rowLimit: 25000 },
      )) {
        const key = r.query.toLowerCase();
        const list = queryCountries.get(key) ?? [];
        list.push({ country: r.country, impressions: r.impressions });
        queryCountries.set(key, list);
      }
      const configured = [...new Set([site.lang, ...(site.languages ?? [])])];
      queryLang = resolveQueryLanguages({
        rows: queries,
        configured,
        primary: site.lang,
        queryCountries,
      });
    }
  } catch {
    queryLang = undefined;
    queryCountries = undefined;
  }

  // Regroupe par langue attribuée (null → langue primaire : aucune requête
  // sélectionnée n'est écartée) ; le PAYS est résolu par-requête dans
  // createGscKeywordsForSite via son breakdown réel.
  const byLang = new Map<string, GscQueryRow[]>();
  for (const row of queries) {
    const lang = queryLang?.get(row.query.toLowerCase()) ?? site.lang;
    const list = byLang.get(lang) ?? [];
    list.push(row);
    byLang.set(lang, list);
  }
  let created = 0;
  for (const [lang, langRows] of byLang) {
    const res = await createGscKeywordsForSite(siteId, langRows, {
      lang,
      country: (q) =>
        resolveGscCountry(
          queryCountries?.get(q.toLowerCase()),
          site.countries,
          fallbackCountry,
        ),
      stages,
    });
    created += res.created;
  }

  revalidatePath("/keywords/reservoir");
  if (destination === "recherche") revalidatePath("/keywords/recherche");
  return { ok: true, created };
}
