// src/lib/seranking/integration-core.ts — implémentation clé BYO SE-Ranking
// (status + save/disconnect), avec DI (`deps`) pour les tests. `server-only`
// (PAS "use server") : ce module n'est jamais exposé au réseau, donc `deps`
// ne peut pas être injecté par un appelant malveillant. Les wrappers exportés
// par integration-actions.ts ("use server") n'exposent AUCUN paramètre
// `deps` — les arguments d'une Server Action sont contrôlés par le client,
// injecter `canManage`/`ping` depuis là contournerait la gate owner/admin ou
// stockerait une clé jamais validée (finding de sécurité Critical, Task 5).
import "server-only";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { getActiveOrgId } from "@/lib/active-org";
import { canManageOrgConnectors } from "@/lib/connectors/access";
import { encryptSecret } from "@/lib/connectors/crypto";
import { organization } from "@/lib/db/auth-schema";
import { db } from "@/lib/db/client";
import { isAddonEnabled } from "@/lib/mcp/entitlements";
import { SERANKING_BYOK_ADDON } from "./balance";

const PING_URL = "https://api.seranking.com/v1/account/subscription";
const PING_TIMEOUT_MS = 10_000;

export type SerankingIntegrationStatus = {
  /** Owner ou admin — peut save/disconnect la clé BYO (gate connecteurs org). */
  canManage: boolean;
  addonActive: boolean;
  /** Clé stockée — jamais la valeur elle-même (voir saveSerankingApiKey). */
  hasKey: boolean;
  /** units_left du dernier ping réussi ; toujours null ici (le status ne re-ping
   *  pas l'API à chaque chargement) — alimenté côté UI par le retour de save. */
  unitsLeft: number | null;
};

export type SaveSerankingApiKeyResult =
  | { ok: true; unitsLeft: number | null }
  | { ok: false; error: string };

type StatusDeps = {
  canManage?: () => Promise<boolean>;
  orgId?: () => Promise<string>;
};

type SaveDeps = StatusDeps & {
  /** Validation de la clé — défaut GET /account/subscription. Throw = clé
   *  refusée (non-2xx) ; résout avec `units_left` (ou null si absent du
   *  payload) sur succès. */
  ping?: (key: string) => Promise<number | null>;
};

/** Ping par défaut : GET /account/subscription, Authorization: Token <key>.
 *  Non-2xx (401/403/…) → throw (l'appelant traduit en « Clé refusée. »). */
async function defaultPing(key: string): Promise<number | null> {
  const res = await fetch(PING_URL, {
    headers: { Authorization: `Token ${key}` },
    signal: AbortSignal.timeout(PING_TIMEOUT_MS),
  });
  if (!res.ok) throw new Error(`SE-Ranking ping HTTP ${res.status}`);
  const json = (await res.json()) as {
    subscription_info?: { units_left?: number };
  };
  return json.subscription_info?.units_left ?? null;
}

/** Status pour la section Intégrations. La clé n'est JAMAIS exposée au
 *  client (`hasKey` seulement). `canManage` réutilise `canManageOrgConnectors`
 *  (owner|admin — la clé SE-Ranking est une clé d'API partagée par toute
 *  l'org, pas un accès personnel comme GSC). */
export async function getSerankingIntegrationStatus(
  deps?: StatusDeps,
): Promise<SerankingIntegrationStatus> {
  const canManage = deps?.canManage ?? canManageOrgConnectors;
  const orgId = deps?.orgId ?? getActiveOrgId;
  const org = await orgId();

  const [rows, addonActive, manage] = await Promise.all([
    db
      .select({ apiKey: organization.serankingApiKey })
      .from(organization)
      .where(eq(organization.id, org)),
    isAddonEnabled(org, SERANKING_BYOK_ADDON),
    canManage(),
  ]);

  return {
    canManage: manage,
    addonActive,
    hasKey: !!rows[0]?.apiKey,
    unitsLeft: null,
  };
}

/**
 * Save (ou, `key` null/vide, disconnect) la clé BYO SE-Ranking de l'org.
 * Gate : owner|admin (`canManageOrgConnectors`). La clé est TRIMÉE avant toute
 * décision — "" ou espaces-seulement = comme `null` (déconnexion), JAMAIS
 * stockée telle quelle (finding de review propagé de la Task 2). Déconnexion
 * autorisée même add-on inactif (geste inoffensif, sans coût — permet de vider
 * une clé orpheline après résiliation) ; l'écriture d'une clé, elle, exige
 * l'add-on actif ET une validation par ping AVANT tout stockage (jamais de clé
 * non vérifiée en base).
 */
export async function saveSerankingApiKey(
  key: string | null,
  deps?: SaveDeps,
): Promise<SaveSerankingApiKeyResult> {
  const canManage = deps?.canManage ?? canManageOrgConnectors;
  const orgId = deps?.orgId ?? getActiveOrgId;
  const ping = deps?.ping ?? defaultPing;

  if (!(await canManage())) {
    return {
      ok: false,
      error: "Action réservée au propriétaire ou à un administrateur.",
    };
  }

  const org = await orgId();
  const trimmed = key?.trim() || null;

  if (trimmed === null) {
    await db
      .update(organization)
      .set({ serankingApiKey: null })
      .where(eq(organization.id, org));
    try {
      // Requires a request/static-generation scope; skip outside one (e.g. tests).
      revalidatePath("/settings/integrations");
    } catch {}
    return { ok: true, unitsLeft: null };
  }

  if (!(await isAddonEnabled(org, SERANKING_BYOK_ADDON))) {
    return { ok: false, error: "Active d'abord l'add-on SE-Ranking." };
  }

  let unitsLeft: number | null;
  try {
    unitsLeft = await ping(trimmed);
  } catch {
    return { ok: false, error: "Clé SE-Ranking refusée." };
  }

  // Chiffrée au repos (AES-256-GCM, connectors/crypto). Les valeurs legacy en
  // clair restent lisibles côté consommation via decryptSecretOrLegacy
  // (balance.ts) jusqu'à la prochaine sauvegarde/rotation.
  await db
    .update(organization)
    .set({ serankingApiKey: encryptSecret(trimmed) })
    .where(eq(organization.id, org));
  try {
    revalidatePath("/settings/integrations");
  } catch {}
  return { ok: true, unitsLeft };
}
