// src/lib/mcp/entitlements.ts — org add-on entitlement lookups.
// `server-only` (NOT a "use server" module): these helpers take a raw orgId and
// perform NO membership check, so they must never be exposed as a Server Action.
// Callers (keys-actions, mcp/auth) resolve the org from the session/bearer token
// FIRST, then call in. Lives apart from keys-actions.ts precisely so it is not
// registered as a client-callable POST endpoint (cross-tenant IDOR otherwise).
// `unstable_cache` wrapping below does NOT change this: it's still a plain
// exported function taking a raw orgId, no "use server" directive, no
// session/cookie/header read inside — just a memoized DB read (QW2-A).
import "server-only";
import { and, eq } from "drizzle-orm";
import { unstable_cache } from "next/cache";
import { getOrgBillingStatus } from "@/lib/billing/status";
import { db } from "@/lib/db/client";
import { orgAddons } from "@/lib/db/domain-schema";

/** The add-on the MCP server gates on. Mirrors MCP_ADDON in src/lib/mcp/auth.ts
 *  — kept as a local literal rather than imported to avoid a require cycle
 *  (auth.ts already imports isAddonEnabled from here). The value is the frozen
 *  org_addons key, not a config knob. */
const MCP_ADDON = "checklist_mcp";

/** Cache tag for `isAddonEnabled(orgId, *)` — invalidated by setOrgAddonAction. */
export function orgAddonsTag(orgId: string): string {
  return `org-addons-${orgId}`;
}

async function fetchAddonEnabled(
  orgId: string,
  addon: string,
): Promise<boolean> {
  const rows = await db
    .select({ orgId: orgAddons.orgId })
    .from(orgAddons)
    .where(and(eq(orgAddons.orgId, orgId), eq(orgAddons.addon, addon)))
    .limit(1);
  return rows.length > 0;
}

/** True if an `org_addons` row exists for `(orgId, addon)`. Cached 30s,
 *  tagged per-org — the MCP chokepoint (resolveMcpOrg) calls this on EVERY
 *  request, and entitlement toggles are rare (super-admin god-mode / Stripe
 *  sync only), so a short TTL trades a few seconds of staleness for cutting a
 *  DB round-trip off every tool call (QW2-A). Invalidated immediately on toggle
 *  by setOrgAddonAction (src/lib/mcp/keys-actions.ts); the TTL is the fallback
 *  safety net for any write path that forgets to. */
async function hasAddonRow(orgId: string, addon: string): Promise<boolean> {
  return unstable_cache(
    fetchAddonEnabled,
    ["org-addon-enabled", orgId, addon],
    {
      tags: [orgAddonsTag(orgId)],
      revalidate: 30,
    },
  )(orgId, addon);
}

/** True if `addon` is enabled for `orgId`.
 *
 *  For every add-on the presence of an `org_addons` row grants access (paid
 *  plan sync + super-admin god-mode). For the MCP add-on ONLY, an org in an
 *  app-level trial ALSO gets access, derived from its billing status — the
 *  trial already carries MCP quota (quotas.ts: trialing → mcpMonthly > 0), so
 *  the entitlement is aligned with it. That trial access is COMPUTED, never
 *  stored: no row is posted, so it auto-expires when the trial lapses (nothing
 *  to clean up). The two reads compose two independently-cached, independently-
 *  tagged sources — `hasAddonRow` (orgAddonsTag) and `getOrgBillingStatus`
 *  (orgBillingTag) — so neither invalidation path is disturbed. Staleness of
 *  the trial signal is bounded by getOrgBillingStatus's own 30s TTL: an org
 *  whose trial just lapsed can keep MCP access for at most ~30s. Acceptable —
 *  same window the billing gate itself tolerates. */
export async function isAddonEnabled(
  orgId: string,
  addon: string,
): Promise<boolean> {
  if (await hasAddonRow(orgId, addon)) return true;
  if (addon === MCP_ADDON) {
    return (await getOrgBillingStatus(orgId)) === "trialing";
  }
  return false;
}
