// src/lib/seranking/client.ts — client minimal de la Data API SE-Ranking
// (bulk keyword export). CLÉ PLATEFORME : chaque appel passe par le limiteur
// process-wide (la clé est partagée par toutes les orgs — 10 RPS max).
import "server-only";
import { env } from "@/env";
import {
  createRateLimiter,
  type RateLimiter,
  serankingLimiter,
} from "./rate-limit";

const API_BASE = "https://api.seranking.com/v1";
const TIMEOUT_MS = 30_000;
const RETRY_BACKOFF_MS = 2_000;
/** BYO-key rate — même prudence process-wide que `serankingLimiter` (5 RPS),
 *  mais un limiteur PAR org : la clé de l'org n'est pas partagée. */
const BYOK_MIN_INTERVAL_MS = 200;

export type SerankingErrorKind =
  | "insufficient-credits"
  | "rate-limited"
  | "invalid-source"
  | "auth"
  | "http"
  | "network"
  | "no-syncs";

export class SerankingError extends Error {
  readonly kind: SerankingErrorKind;
  constructor(kind: SerankingErrorKind, message: string) {
    super(message);
    this.name = "SerankingError";
    this.kind = kind;
  }
}

function classify(status: number, body: string): SerankingError {
  const lower = body.toLowerCase();
  if (
    status === 400 &&
    (lower.includes("insufficient") || lower.includes("credit"))
  ) {
    return new SerankingError(
      "insufficient-credits",
      "Crédits SE-Ranking épuisés.",
    );
  }
  if (status === 400 && lower.includes("source")) {
    return new SerankingError(
      "invalid-source",
      `Base régionale refusée: ${body.slice(0, 200)}`,
    );
  }
  if (status === 401 || status === 403) {
    return new SerankingError(
      "auth",
      "Clé API SE-Ranking refusée ou licence expirée.",
    );
  }
  if (status === 429) {
    return new SerankingError("rate-limited", "Rate limit SE-Ranking atteint.");
  }
  return new SerankingError(
    "http",
    `SE-Ranking HTTP ${status}: ${body.slice(0, 200)}`,
  );
}

/** Message utilisateur (FR) pour chaque famille d'erreur SE-Ranking. Vit ici
 *  (et pas dans actions.ts) : un module "use server" ne peut exporter que des
 *  async functions. */
export function serankingErrorMessage(e: SerankingError): string {
  switch (e.kind) {
    case "insufficient-credits":
      return "Crédits SE-Ranking épuisés — réessaie plus tard ou contacte l'équipe.";
    case "rate-limited":
      return "SE-Ranking limite le débit — réessaie dans quelques minutes.";
    case "auth":
      return "Connexion SE-Ranking refusée — configuration plateforme à vérifier.";
    case "invalid-source":
      return "Base régionale SE-Ranking refusée pour ce pays.";
    case "no-syncs":
      return "Solde de syncs épuisé — recharge depuis Facturation.";
    default:
      return "Échec du sync SE-Ranking — réessaie plus tard.";
  }
}

/** Limiteurs BYO — un par org (la clé de l'org n'est pas partagée avec les
 *  autres). Map module-level : même instance réutilisée pour un orgId donné
 *  (sérialise les appels d'une même org entre eux), une instance distincte
 *  par org (n'attend pas derrière le débit d'une autre org). */
const byokLimiters = new Map<string, RateLimiter>();

export function limiterForKey(orgId: string): RateLimiter {
  let limiter = byokLimiters.get(orgId);
  if (!limiter) {
    limiter = createRateLimiter({ minIntervalMs: BYOK_MIN_INTERVAL_MS });
    byokLimiters.set(orgId, limiter);
  }
  return limiter;
}

/** POST /keywords/export — métriques bulk (1–5000 kw, 100 crédits/requête).
 *  Résout en BODY BRUT (JSON string) : la normalisation est faite en Rust
 *  (wasm `parse_seranking_api`), pas ici. 1 retry sur 429/5xx. */
export async function bulkKeywordMetrics({
  source,
  keywords,
  apiKey,
  fetchImpl = fetch,
  limiter = serankingLimiter,
  sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms)),
}: {
  source: string;
  keywords: string[];
  /** Clé BYO de l'org (mode byok) — défaut la clé plateforme (`env`). */
  apiKey?: string;
  fetchImpl?: typeof fetch;
  limiter?: RateLimiter;
  sleep?: (ms: number) => Promise<void>;
}): Promise<string> {
  const key = apiKey ?? env.SERANKING_API_KEY;
  if (!key)
    throw new SerankingError("auth", "SERANKING_API_KEY non configurée.");

  const doFetch = () =>
    limiter.schedule(async () => {
      let res: Response;
      let body: string;
      try {
        res = await fetchImpl(
          `${API_BASE}/keywords/export?source=${encodeURIComponent(source)}`,
          {
            method: "POST",
            headers: {
              Authorization: `Token ${key}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({ keywords }),
            signal: AbortSignal.timeout(TIMEOUT_MS),
          },
        );
        body = await res.text();
      } catch (e) {
        throw new SerankingError(
          "network",
          e instanceof Error ? e.message : "Échec réseau SE-Ranking.",
        );
      }
      if (!res.ok) throw classify(res.status, body);
      return body;
    });

  try {
    return await doFetch();
  } catch (e) {
    const retryable =
      e instanceof SerankingError &&
      (e.kind === "rate-limited" || e.kind === "http" || e.kind === "network");
    if (!retryable) throw e;
    await sleep(RETRY_BACKOFF_MS);
    return doFetch();
  }
}
