import "server-only";
// src/lib/mangools/client.ts — client HTTP de l'API Mangools v3 (BYOK).
// Le token est reçu en paramètre (jamais lu ici depuis le store). Chaque appel
// passe par un throttle (~2 req/s, sérialisé) ; les erreurs sont taxonomisées.
// Aucune donnée du token ne doit fuiter dans les messages d'erreur.
//
// DÉCISION (spike réel 2026-07-25, validée Bastien) : on n'envoie JAMAIS de
// `language_id`. Preuves : (1) les volumes sont identiques avec/sans langue
// (données Google Ads = par localisation, pas par langue — testé kw FR /
// location 2250 : sv 57700 dans les deux cas) ; (2) un id non reconnu par
// Mangools (même le 1002 « French » officiel Google Ads !) renvoie ZÉRO
// résultat en silence — un mauvais mapping ferait partir tout l'enrichissement
// en not_found ; (3) aucun endpoint /mangools/languages pour découvrir leurs
// ids. La localisation par mot-clé porte toute l'info marché. Ne pas
// « améliorer » ça sans re-spiker.

import { createThrottle, type Throttle } from "./throttle";
import type {
  MangoolsError,
  MangoolsKeywordItem,
  MangoolsList,
  MangoolsListDetail,
  MangoolsLocation,
  MangoolsRelatedResponse,
  MangoolsSerpItem,
  MangoolsSerpResponse,
} from "./types";

const BASE_URL = "https://api.mangools.com/v3";
const TIMEOUT_MS = 20_000;
const DEFAULT_BACKOFF_MS = 2000;
// Plancher du backoff 429 : au moins l'espacement du throttle (~500 ms). Évite
// qu'un `Retry-After: 0` relance la reprise instantanément après un rate-limit.
const MIN_BACKOFF_MS = 500;
// Plafond du backoff 429 : ce chemin est désormais synchrone côté UI (Task 9),
// un `Retry-After` abusif (ex. 3600 s) ne doit jamais suspendre la requête des
// dizaines de minutes — on borne la reprise à 15 s.
const MAX_BACKOFF_MS = 15_000;

/** Erreur unifiée du client. `message` n'inclut jamais le token. */
export class MangoolsApiError extends Error {
  readonly code: MangoolsError;
  readonly status: number;

  constructor(code: MangoolsError, status: number, message?: string) {
    super(message ?? `Mangools API error: ${code} (status ${status})`);
    this.name = "MangoolsApiError";
    this.code = code;
    this.status = status;
  }
}

function statusToCode(status: number): MangoolsError {
  if (status === 401 || status === 403) return "unauthorized";
  if (status === 402) return "quota_exceeded";
  if (status === 429) return "rate_limited";
  return "upstream_error";
}

/** Retry-After en secondes → ms. Absent/invalide ⇒ backoff par défaut. */
function backoffFromRetryAfter(header: string | null): number {
  if (header != null && header.trim() !== "") {
    const seconds = Number(header);
    if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
  }
  return DEFAULT_BACKOFF_MS;
}

export type MangoolsClientDeps = {
  fetch?: typeof fetch;
  throttle?: Throttle;
};

export type MangoolsClient = {
  relatedKeywords(args: {
    kw: string;
    locationId: number;
  }): Promise<MangoolsRelatedResponse>;
  serps(args: {
    kw: string;
    locationId: number;
    platformId?: number;
  }): Promise<MangoolsSerpResponse>;
  keywordImports(
    kws: string[],
    locationId: number,
  ): Promise<MangoolsKeywordItem[]>;
  searchLocations(query: string): Promise<MangoolsLocation[]>;
  lists(): Promise<MangoolsList[]>;
  list(listId: string): Promise<MangoolsListDetail>;
};

/** `location.code` normalisé en alpha-2 MAJUSCULES, `null` si absent ou vide. */
function countryOf(raw: Record<string, unknown>): string | null {
  const code = (raw.location as { code?: string } | undefined)?.code;
  return typeof code === "string" && code !== "" ? code.toUpperCase() : null;
}

/** Ne conserve que les champs déclarés de `MangoolsKeywordItem` (écarte les
 *  bruts non typés : svs, svn, ts, seo_ts…). */
function mapKeywordItem(raw: Record<string, unknown>): MangoolsKeywordItem {
  return {
    _id: raw._id as string,
    kw: raw.kw as string,
    lid: raw.lid as number,
    country: countryOf(raw),
    sv: (raw.sv ?? null) as number | null,
    seo: (raw.seo ?? null) as number | null,
    cpc: (raw.cpc ?? null) as number | null,
    ppc: (raw.ppc ?? null) as number | null,
    msv: (raw.msv ?? null) as [number, number, number][] | null,
  };
}

function mapSerpItem(raw: Record<string, unknown>): MangoolsSerpItem {
  return {
    type: raw.type as string,
    url: raw.url as string | undefined,
    title: raw.title as string | undefined,
    desc: raw.desc as string | undefined,
    serpRank: raw.serpRank as number | undefined,
    domain: raw.domain as string | undefined,
    m: raw.m as MangoolsSerpItem["m"],
  };
}

function mapLocation(raw: Record<string, unknown>): MangoolsLocation {
  return {
    _id: raw._id as number,
    label: raw.label as string,
    country_code: (raw.country_code ?? null) as string | null,
    target_type: raw.target_type as string,
  };
}

/** Enveloppe de liste. ATTENTION : `created_at`/`updated_at` sont en
 *  MILLISECONDES ici, alors que `ts`/`seo_ts` des mots-clés sont en SECONDES.
 *  La documentation publiée montre des secondes pour les deux — elle est
 *  fausse, vérifié sur un compte réel le 2026-07-28. */
function mapList(raw: Record<string, unknown>): MangoolsList {
  const ids = Array.isArray(raw.keyword_ids) ? raw.keyword_ids : [];
  return {
    id: raw._id as string,
    name: (raw.name as string) ?? "",
    keywordCount: ids.length,
    createdAt: new Date(Number(raw.created_at ?? 0)).toISOString(),
    updatedAt: new Date(
      Number(raw.updated_at ?? raw.created_at ?? 0),
    ).toISOString(),
  };
}

export function createMangoolsClient(
  token: string,
  deps: MangoolsClientDeps = {},
): MangoolsClient {
  const doFetch = deps.fetch ?? fetch;
  const throttle = deps.throttle ?? createThrottle();
  // Le backoff 429 réutilise le sleep du throttle → instantané en test (sleep
  // injecté) sans exposer un paramètre `sleep` supplémentaire sur le client.
  const sleep = throttle.sleep;

  async function callOnce(path: string, init?: RequestInit): Promise<Response> {
    return doFetch(`${BASE_URL}${path}`, {
      ...init,
      headers: { "x-access-token": token, ...init?.headers },
      signal: AbortSignal.timeout(TIMEOUT_MS),
    });
  }

  /** Un appel = un slot throttle. Sur 429 : UNE reprise, elle-même re-throttlée
   *  (nouveau `throttle.run`) après un backoff planché à `MIN_BACKOFF_MS`. Le
   *  premier slot est libéré avant le sleep, puis la reprise est remise en file :
   *  cela rafraîchit `lastStart`, garde la reprise espacée des appels suivants
   *  (elle ne court-circuite plus le throttle) et laisse la file avancer pendant
   *  le backoff. NB : imbriquer un `throttle.run` dans l'autre bloquerait la file
   *  (la reprise attendrait la fin du run qui l'attend). */
  async function request<T>(path: string, init?: RequestInit): Promise<T> {
    let res = await throttle.run(() => callOnce(path, init));
    if (res.status === 429) {
      const backoff = Math.min(
        Math.max(
          backoffFromRetryAfter(res.headers.get("retry-after")),
          MIN_BACKOFF_MS,
        ),
        MAX_BACKOFF_MS,
      );
      await sleep(backoff);
      res = await throttle.run(() => callOnce(path, init));
    }
    if (!res.ok) {
      throw new MangoolsApiError(statusToCode(res.status), res.status);
    }
    return (await res.json()) as T;
  }

  return {
    async relatedKeywords({ kw, locationId }) {
      const qs = new URLSearchParams({ kw, location_id: String(locationId) });
      const body = await request<{
        keywords?: Record<string, unknown>[];
        countKeywordsBeforeLimit?: number;
      }>(`/kwfinder/related-keywords?${qs}`);
      return {
        keywords: (body.keywords ?? []).map(mapKeywordItem),
        countKeywordsBeforeLimit: body.countKeywordsBeforeLimit ?? 0,
      };
    },

    async serps({ kw, locationId, platformId }) {
      const qs = new URLSearchParams({ kw, location_id: String(locationId) });
      if (platformId !== undefined) qs.set("platform_id", String(platformId));
      const body = await request<{
        kw?: string;
        location_id?: number;
        items?: Record<string, unknown>[];
      }>(`/serpchecker/serps?${qs}`);
      return {
        kw: body.kw ?? kw,
        location_id: body.location_id ?? locationId,
        items: (body.items ?? []).map(mapSerpItem),
      };
    },

    async keywordImports(kws, locationId) {
      const body = await request<{ keywords?: Record<string, unknown>[] }>(
        "/kwfinder/keyword-imports",
        {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({ keywords: kws, location_id: locationId }),
        },
      );
      return (body.keywords ?? []).map(mapKeywordItem);
    },

    async searchLocations(query) {
      const qs = new URLSearchParams({ query });
      const body = await request<Record<string, unknown>[]>(
        `/mangools/locations?${qs}`,
      );
      return (Array.isArray(body) ? body : []).map(mapLocation);
    },

    async lists() {
      const body = await request<Record<string, unknown>[]>("/kwfinder/lists");
      return (Array.isArray(body) ? body : []).map(mapList);
    },

    async list(listId) {
      const body = await request<{
        list?: Record<string, unknown>;
        keywords?: Record<string, unknown>[];
      }>(`/kwfinder/lists/${encodeURIComponent(listId)}`);
      const keywords = (body.keywords ?? []).map(mapKeywordItem);
      // Les pays viennent des mots-clés : l'enveloppe n'en porte aucun. Une
      // liste est mono-pays en pratique, mais rien ne le garantit côté API.
      const countries = [
        ...new Set(
          keywords.map((k) => k.country).filter((c): c is string => c !== null),
        ),
      ];
      return { list: mapList(body.list ?? {}), keywords, countries };
    },
  };
}
