// src/lib/seranking/balance.ts — cœur billing SE-Ranking : résolution du mode
// d'accès (unlimited > byok > metered) et consommation atomique du solde de
// syncs. `server-only` (NOT "use server"): prend un orgId brut, aucune
// vérification de membership — les appelants (actions, sync.ts) résolvent
// l'org depuis la session AVANT d'appeler ici (même garde que
// src/lib/mcp/entitlements.ts).
import "server-only";
import { and, eq, sql } from "drizzle-orm";
import { decryptSecretOrLegacy } from "@/lib/connectors/crypto";
import { organization } from "@/lib/db/auth-schema";
import { db } from "@/lib/db/client";
import { isAddonEnabled } from "@/lib/mcp/entitlements";

export const SERANKING_BYOK_ADDON = "seranking_byok";

export type SerankingAccess =
  | { mode: "unlimited" }
  | { mode: "byok"; apiKey: string }
  | { mode: "metered"; balance: number };

/** Same executor duality as src/lib/billing/usage.ts's `Executor` — a `tx`
 *  from `db.transaction`, or the module `db` by default. Lets fulfillment
 *  webhooks (checkout.session.completed) credit the balance inside the same
 *  transaction as the idempotent purchase-row insert (mirror of
 *  pack-fulfillment.ts's fulfillMcpPack). */
type Executor = Parameters<Parameters<typeof db.transaction>[0]>[0] | typeof db;

/**
 * Resolves the org's SE-Ranking access mode. Resolution order (Global
 * Constraints du plan) : `unlimited` > `byok` (add-on actif ET clé
 * renseignée) > `metered`. Add-on actif SANS clé = metered (fallback sûr,
 * jamais de clé plateforme prêtée à une org byok en défaut).
 */
export async function getSerankingAccess(
  orgId: string,
): Promise<SerankingAccess> {
  const [org] = await db
    .select({
      unlimited: organization.serankingUnlimited,
      balance: organization.serankingSyncBalance,
      apiKey: organization.serankingApiKey,
    })
    .from(organization)
    .where(eq(organization.id, orgId));

  if (org?.unlimited) return { mode: "unlimited" };

  if (org?.apiKey && (await isAddonEnabled(orgId, SERANKING_BYOK_ADDON))) {
    // Stockée chiffrée depuis le fix connectors/crypto ; les clés d'avant le
    // fix (en clair) restent lisibles jusqu'à leur rotation.
    return { mode: "byok", apiKey: decryptSecretOrLegacy(org.apiKey) };
  }

  return { mode: "metered", balance: org?.balance ?? 0 };
}

/**
 * Atomic conditional decrement — no transaction, no advisory lock: the
 * `UPDATE ... WHERE balance >= 1 RETURNING id` itself is the atomicity
 * (Postgres serializes concurrent UPDATEs on the same row). Returns `true`
 * when a row was touched (1 sync consumed), `false` on insufficient balance
 * (0 rows touched, never goes negative). Called just BEFORE each API
 * request — no refund on downstream failure (a request sent is a sync due).
 */
export async function consumeSync(orgId: string): Promise<boolean> {
  const rows = await db
    .update(organization)
    .set({
      serankingSyncBalance: sql`${organization.serankingSyncBalance} - 1`,
    })
    .where(
      and(
        eq(organization.id, orgId),
        sql`${organization.serankingSyncBalance} >= 1`,
      ),
    )
    .returning({ id: organization.id });
  return rows.length > 0;
}

/** Credits `n` syncs onto the org's balance — used by pack fulfillment.
 *  `executor` optional (a `tx`) to run inside the fulfillment's own
 *  idempotent-insert transaction. */
export async function creditSyncs(
  orgId: string,
  n: number,
  executor: Executor = db,
): Promise<void> {
  await executor
    .update(organization)
    .set({
      serankingSyncBalance: sql`${organization.serankingSyncBalance} + ${n}`,
    })
    .where(eq(organization.id, orgId));
}
