// src/lib/billing/usage.ts — per-org usage counters for quota enforcement.
// Cheap indexed COUNTs, computed at the enforcement point (count-then-block).
// Spec: docs/superpowers/specs/2026-06-12-billing-quotas-design.md (3B).
import "server-only";
import { and, count, eq, sql } from "drizzle-orm";
import { invitation, member } from "@/lib/db/auth-schema";
import { db } from "@/lib/db/client";
import {
  backlinks,
  domainBacklinks,
  keywords,
  sites,
} from "@/lib/db/domain-schema";
import type { QuotaResource } from "./quotas";

/** Every counter here takes an optional `executor` (a `tx` from
 *  `db.transaction`, or the module `db` by default) — sec audit A2/P8: a
 *  quota-guarded write runs these counts INSIDE a `lockOrgForQuota`
 *  transaction (see quotas.ts `assertWithinQuotaTx`). A query on the plain
 *  `db` handle issued while the transaction already holds a connection asks
 *  the pool for a SECOND one; once concurrency reaches the pool size (default
 *  10; 1 on Vercel — src/lib/db/client.ts) every holder is waiting on a slot
 *  no other holder will release → deadlock. Passing `tx` through routes the
 *  count onto the SAME connection instead of requesting a second one. */
type Executor = Parameters<Parameters<typeof db.transaction>[0]>[0] | typeof db;

/**
 * Keyword quota is counted PER (keyword × source d'importation) : un mot-clé
 * importé depuis N sources consomme N unités (« 5 sources de 1000 kw = 5k »).
 * On somme la longueur du tableau `sources[]` de chaque ligne (déjà stocké, pas
 * de table ni de migration). Per-site → JOIN sites pour scoper par org.
 */
export async function countOrgKeywords(
  orgId: string,
  executor: Executor = db,
): Promise<number> {
  // greatest(..., 1): a keyword is always ≥ 1 unit (single-source keywords keep
  // the old count; only multi-source keywords cost more).
  const [row] = await executor
    .select({
      n: sql<number>`coalesce(sum(greatest(coalesce(array_length(${keywords.sources}, 1), 0), 1)), 0)`,
    })
    .from(keywords)
    .innerJoin(sites, eq(keywords.siteId, sites.id))
    .where(eq(sites.orgId, orgId));
  return Number(row?.n ?? 0);
}

/** A site IS a project. */
export async function countOrgProjects(
  orgId: string,
  executor: Executor = db,
): Promise<number> {
  const [row] = await executor
    .select({ n: count() })
    .from(sites)
    .where(eq(sites.orgId, orgId));
  return row?.n ?? 0;
}

/** Seats = current members + pending invitations (a sent invite reserves a seat). */
export async function countOrgSeats(
  orgId: string,
  executor: Executor = db,
): Promise<number> {
  const [members] = await executor
    .select({ n: count() })
    .from(member)
    .where(eq(member.organizationId, orgId));
  const [pending] = await executor
    .select({ n: count() })
    .from(invitation)
    .where(
      and(
        eq(invitation.organizationId, orgId),
        eq(invitation.status, "pending"),
      ),
    );
  return (members?.n ?? 0) + (pending?.n ?? 0);
}

/**
 * backlinkRows covers BOTH per-site row stores (decision per spec "verify at
 * impl"): `backlinks` (acquisition targeting — link_sources copies) AND
 * `domain_backlinks` (inbound profile import). Both grow with imports, so both
 * count against the same plan limit.
 */
export async function countOrgBacklinks(
  orgId: string,
  executor: Executor = db,
): Promise<number> {
  const [targeting] = await executor
    .select({ n: count() })
    .from(backlinks)
    .innerJoin(sites, eq(backlinks.siteId, sites.id))
    .where(eq(sites.orgId, orgId));
  const [profile] = await executor
    .select({ n: count() })
    .from(domainBacklinks)
    .innerJoin(sites, eq(domainBacklinks.siteId, sites.id))
    .where(eq(sites.orgId, orgId));
  return (targeting?.n ?? 0) + (profile?.n ?? 0);
}

/** Current usage for a quota resource — consumed by assertWithinQuota /
 *  assertWithinQuotaTx. Pass `executor` (a `tx`) when called from inside an
 *  already-open transaction — see the module doc above. */
export async function getUsage(
  orgId: string,
  resource: QuotaResource,
  executor: Executor = db,
): Promise<number> {
  switch (resource) {
    case "keywords":
      return countOrgKeywords(orgId, executor);
    case "projects":
      return countOrgProjects(orgId, executor);
    case "seats":
      return countOrgSeats(orgId, executor);
    case "backlinkRows":
      return countOrgBacklinks(orgId, executor);
    case "integrations":
      // No integrations registry yet (GSC mapping is per-site) — not an
      // enforcement point in 3B; keep at 0 until one exists.
      return 0;
    case "mcpMonthly":
      // TODO(3B-B2): monthly MCP metering — SUM(monthlyCount) over the org's
      // mcp_api_keys. The MCP route enforces its own check in B2;
      // assertWithinQuota is never called for mcpMonthly from app actions.
      return 0;
  }
}
