// services/analytics/src/serp-semantics.ts — adaptateur mince du job
// `serp-semantics`. Câble les vraies dépendances (db du service, fetch mobile
// réel, storage R2) au cœur orchestrable `runSerpSemantics`
// (src/lib/semantic/analysis-runner.ts, Task 3, testé par test:all).
// Mirror de site-crawl.ts, avec une différence délibérée : le runner est un
// paramètre injectable (défaut = le vrai `runSerpSemantics`) plutôt qu'un
// import figé, pour tester le dispatch (succès/échec → completeJob/failJob)
// sans mock.module (cf. mémoire projet : mock.module fuit entre fichiers de
// test — la DI reste le chemin préféré).
import { runSerpSemantics } from "@/lib/semantic/analysis-runner";
import { fetchSemanticHtml } from "@/lib/semantic/fetch";
import { resolveRenderProvider } from "@/lib/semantic/render";
import { db } from "./db";
import { renderEnv } from "./env";
import { firecrawlConfigured } from "./firecrawl";
import { fetchViaFirecrawl } from "./firecrawl-fetch";
import { completeJob, failJob, type Job, updateJobTotals } from "./queue";
import { limitRenderConcurrency } from "./render-limiter";
import { deleteObject, putObject } from "./storage";

type Runner = typeof runSerpSemantics;

/** Repli du runner (`DEFAULT_MAX_RENDER_POSITION` dans analysis-runner.ts) —
 *  dupliquée ici volontairement : c'est la valeur EFFECTIVE si
 *  `RENDER_MAX_POSITION` est absent ou hors garde-fou, calculée avant même
 *  d'atteindre le runner. */
const DEFAULT_MAX_RENDER_POSITION = 5;
const MIN_RENDER_POSITION = 1;
const MAX_RENDER_POSITION_CAP = 10;

/** Parse + clamp `RENDER_MAX_POSITION` (entier dans [1, 10]) ; toute valeur
 *  absente, non entière ou hors bornes retombe sur le défaut (5) plutôt que
 *  de propager un réglage aberrant au runner. */
function parseMaxRenderPosition(raw: string | undefined): number {
  if (!raw) return DEFAULT_MAX_RENDER_POSITION;
  const n = Number.parseInt(raw, 10);
  if (
    !Number.isFinite(n) ||
    n < MIN_RENDER_POSITION ||
    n > MAX_RENDER_POSITION_CAP
  ) {
    return DEFAULT_MAX_RENDER_POSITION;
  }
  return n;
}

// Provider de rendu headless + sémaphore : résolus UNE FOIS au chargement du
// module (pas par job) pour que `limitRenderConcurrency` reste un cap GLOBAL
// process-wide (revue T2) — plusieurs jobs `serp-semantics` peuvent tourner
// en parallèle et ne doivent jamais dépasser `MAX_CONCURRENT_RENDERS` Chrome
// simultanés à eux tous. `CHROME_PATH` absent/introuvable → `null` (aucun
// rendu, dégradation identique au pré-S7 — cf. `resolveRenderProvider`).
const rawRenderProvider = resolveRenderProvider({
  chromePath: renderEnv().CHROME_PATH,
});
const renderProvider = rawRenderProvider
  ? limitRenderConcurrency(rawRenderProvider)
  : null;
const maxRenderPosition = parseMaxRenderPosition(
  renderEnv().RENDER_MAX_POSITION,
);

// Résolu UNE FOIS au chargement du module (même raisonnement que
// `renderProvider` ci-dessus) — la config Firecrawl (env) ne change pas en
// cours de process. Firecrawl configuré → il fetch ET rend (Playwright côté
// instance) : le rendu Chrome local devient inutile sur ce chemin. Non
// configuré (dev sans instance, et CI/test où les env FIRECRAWL sont
// absentes) → chemin legacy inchangé (fetchSemanticHtml + Chrome local).
const useFirecrawl = firecrawlConfigured();

/** Handler `serp-semantics` : orchestration déléguée au runner, statut du job
 *  géré ici (progress → running, succès → done, échec/payload manquant →
 *  failed). `runner` est injectable pour les tests (DI, pas de mock.module). */
export async function processSerpSemantics(
  job: Job,
  runner: Runner = runSerpSemantics,
): Promise<void> {
  try {
    // `job.payload` est l'union de tous les payloads de jobs (schéma partagé) ;
    // `urls` ne discrimine que SerpSemanticsPayload (absent de CrawlOnpagePayload) —
    // ce handler n'étant dispatché que pour `type: "serp-semantics"` (registre
    // HANDLERS de worker.ts), c'est un garde-fou TypeScript, pas un vrai risque.
    if (!job.payload || !("urls" in job.payload)) {
      throw new Error("serp-semantics job missing payload");
    }
    const result = await runner(job.id, job.siteId, job.payload, {
      db,
      fetchHtml: useFirecrawl ? fetchViaFirecrawl : fetchSemanticHtml,
      putObject,
      deleteObject,
      now: () => new Date().toISOString(),
      onProgress: async (p) => {
        await updateJobTotals(job.id, p);
      },
      render: useFirecrawl ? null : renderProvider,
      maxRenderPosition,
    });
    if (result.ok) {
      await completeJob(job.id, {});
    } else {
      await failJob(job.id, result.error);
    }
  } catch (e) {
    await failJob(job.id, (e as Error).message);
  }
}
