/**
 * Block F4 — Billing + quotas.
 *
 * Plans live in `billing_plans` (seeded with free/pro/team/enterprise by
 * migration 0020). Each user has a row in `user_quotas` keyed by
 * `plan_slug` + running usage counters (storage, AI tokens, bandwidth).
 *
 *   getPlan(slug)                       — load a plan by slug
 *   getUserQuota(userId)                — row + plan join, initialises on first read
 *   listPlans()                         — admin UI
 *   setUserPlan(userId, slug, byId)     — admin override (audit-logged outside)
 *   bumpUsage(userId, field, delta)     — fire-and-forget counter increment
 *   checkQuota(userId, field, amount)   — boolean "allowed?" for pre-write gating
 *   repoCountForUser(userId)            — counts owned repos (enforced at create)
 *   resetIfCycleExpired(userId)         — flips cycleStart each month
 *
 * All helpers swallow DB errors (plan goes to "free" on failure) so billing is
 * never a hard dependency for the primary request path.
 */

import { and, eq, sql } from "drizzle-orm";
import { db } from "../db";
import {
  billingPlans,
  userQuotas,
  repositories,
  type BillingPlan,
  type UserQuota,
} from "../db/schema";

export const DEFAULT_PLAN_SLUG = "free";

/** Mirrors the seed rows in migration 0020 so billing works even pre-migration. */
export const FALLBACK_PLANS: Record<string, Omit<BillingPlan, "id" | "createdAt">> = {
  free: {
    slug: "free",
    name: "Free",
    priceCents: 0,
    repoLimit: 5,
    storageMbLimit: 500,
    aiTokensMonthly: 50_000,
    bandwidthGbMonthly: 5,
    privateRepos: false,
  },
  pro: {
    slug: "pro",
    name: "Pro",
    priceCents: 900,
    repoLimit: 50,
    storageMbLimit: 5_000,
    aiTokensMonthly: 500_000,
    bandwidthGbMonthly: 50,
    privateRepos: true,
  },
  team: {
    slug: "team",
    name: "Team",
    priceCents: 2900,
    repoLimit: 200,
    storageMbLimit: 20_000,
    aiTokensMonthly: 2_000_000,
    bandwidthGbMonthly: 200,
    privateRepos: true,
  },
  enterprise: {
    slug: "enterprise",
    name: "Enterprise",
    priceCents: 9900,
    repoLimit: 10_000,
    storageMbLimit: 500_000,
    aiTokensMonthly: 50_000_000,
    bandwidthGbMonthly: 5_000,
    privateRepos: true,
  },
};

export async function listPlans(): Promise<
  Array<Omit<BillingPlan, "id" | "createdAt">>
> {
  try {
    const rows = await db.select().from(billingPlans).orderBy(billingPlans.priceCents);
    if (rows.length > 0) return rows;
  } catch {
    // fall through
  }
  return Object.values(FALLBACK_PLANS);
}

export async function getPlan(
  slug: string
): Promise<Omit<BillingPlan, "id" | "createdAt">> {
  try {
    const [row] = await db
      .select()
      .from(billingPlans)
      .where(eq(billingPlans.slug, slug))
      .limit(1);
    if (row) return row;
  } catch {
    // fall through
  }
  return FALLBACK_PLANS[slug] || FALLBACK_PLANS.free;
}

export type QuotaField =
  | "storageMbUsed"
  | "aiTokensUsedThisMonth"
  | "bandwidthGbUsedThisMonth";

export interface QuotaView {
  planSlug: string;
  plan: Omit<BillingPlan, "id" | "createdAt">;
  usage: {
    storageMbUsed: number;
    aiTokensUsedThisMonth: number;
    bandwidthGbUsedThisMonth: number;
  };
  cycleStart: Date | null;
  /** Stripe linkage — populated once the user completes a checkout session. */
  stripeCustomerId: string | null;
  stripeSubscriptionId: string | null;
  stripeSubscriptionStatus: string | null;
  currentPeriodEnd: Date | null;
  percent: {
    storage: number;
    aiTokens: number;
    bandwidth: number;
  };
}

/** Loads the quota row, inserting a free-plan row on first read. */
export async function getUserQuota(userId: string): Promise<QuotaView> {
  let row: UserQuota | undefined;
  try {
    const [r] = await db
      .select()
      .from(userQuotas)
      .where(eq(userQuotas.userId, userId))
      .limit(1);
    row = r;
    if (!row) {
      await db
        .insert(userQuotas)
        .values({ userId, planSlug: DEFAULT_PLAN_SLUG })
        .onConflictDoNothing();
      const [r2] = await db
        .select()
        .from(userQuotas)
        .where(eq(userQuotas.userId, userId))
        .limit(1);
      row = r2;
    }
  } catch {
    // fall through
  }

  const planSlug = row?.planSlug || DEFAULT_PLAN_SLUG;
  const plan = await getPlan(planSlug);
  const usage = {
    storageMbUsed: row?.storageMbUsed || 0,
    aiTokensUsedThisMonth: row?.aiTokensUsedThisMonth || 0,
    bandwidthGbUsedThisMonth: row?.bandwidthGbUsedThisMonth || 0,
  };
  const pct = (used: number, limit: number) =>
    limit > 0 ? Math.min(100, Math.round((used / limit) * 100)) : 0;

  return {
    planSlug,
    plan,
    usage,
    cycleStart: (row?.cycleStart as Date | null) || null,
    stripeCustomerId: (row?.stripeCustomerId as string | null) || null,
    stripeSubscriptionId: (row?.stripeSubscriptionId as string | null) || null,
    stripeSubscriptionStatus:
      (row?.stripeSubscriptionStatus as string | null) || null,
    currentPeriodEnd: (row?.currentPeriodEnd as Date | null) || null,
    percent: {
      storage: pct(usage.storageMbUsed, plan.storageMbLimit),
      aiTokens: pct(usage.aiTokensUsedThisMonth, plan.aiTokensMonthly),
      bandwidth: pct(usage.bandwidthGbUsedThisMonth, plan.bandwidthGbMonthly),
    },
  };
}

export async function setUserPlan(
  userId: string,
  planSlug: string
): Promise<boolean> {
  try {
    await db
      .insert(userQuotas)
      .values({ userId, planSlug })
      .onConflictDoUpdate({
        target: userQuotas.userId,
        set: { planSlug, updatedAt: new Date() },
      });
    return true;
  } catch (err) {
    console.error("[billing] setUserPlan:", err);
    return false;
  }
}

/** Fire-and-forget counter bump. Returns the new value on success. */
export async function bumpUsage(
  userId: string,
  field: QuotaField,
  delta: number
): Promise<number | null> {
  if (!userId || delta === 0) return null;
  const column =
    field === "storageMbUsed"
      ? userQuotas.storageMbUsed
      : field === "aiTokensUsedThisMonth"
      ? userQuotas.aiTokensUsedThisMonth
      : userQuotas.bandwidthGbUsedThisMonth;
  try {
    await db
      .insert(userQuotas)
      .values({
        userId,
        planSlug: DEFAULT_PLAN_SLUG,
        [field]: delta,
      } as any)
      .onConflictDoUpdate({
        target: userQuotas.userId,
        set: {
          [field]: sql`${column} + ${delta}`,
          updatedAt: new Date(),
        } as any,
      });
    const [r] = await db
      .select({ n: column })
      .from(userQuotas)
      .where(eq(userQuotas.userId, userId))
      .limit(1);
    return Number(r?.n || 0);
  } catch (err) {
    console.error("[billing] bumpUsage:", err);
    return null;
  }
}

/** True if the user has budget left for an action costing `amount` against `field`. */
export async function checkQuota(
  userId: string,
  field: QuotaField,
  amount: number = 1
): Promise<boolean> {
  try {
    const { plan, usage } = await getUserQuota(userId);
    if (field === "storageMbUsed")
      return usage.storageMbUsed + amount <= plan.storageMbLimit;
    if (field === "aiTokensUsedThisMonth")
      return usage.aiTokensUsedThisMonth + amount <= plan.aiTokensMonthly;
    if (field === "bandwidthGbUsedThisMonth")
      return usage.bandwidthGbUsedThisMonth + amount <= plan.bandwidthGbMonthly;
    return true;
  } catch {
    return true; // fail-open on billing errors
  }
}

export async function repoCountForUser(userId: string): Promise<number> {
  try {
    const [r] = await db
      .select({ n: sql<number>`count(*)::int` })
      .from(repositories)
      .where(eq(repositories.ownerId, userId));
    return Number(r?.n || 0);
  } catch {
    return 0;
  }
}

/** True if creating another repo would exceed the plan's repoLimit. */
export async function wouldExceedRepoLimit(userId: string): Promise<boolean> {
  try {
    const [quota, count] = await Promise.all([
      getUserQuota(userId),
      repoCountForUser(userId),
    ]);
    return count >= quota.plan.repoLimit;
  } catch {
    return false;
  }
}

/** Resets monthly counters if >30 days have passed since cycleStart. */
export async function resetIfCycleExpired(userId: string): Promise<boolean> {
  try {
    const [row] = await db
      .select({ cycleStart: userQuotas.cycleStart })
      .from(userQuotas)
      .where(eq(userQuotas.userId, userId))
      .limit(1);
    if (!row?.cycleStart) return false;
    const age = Date.now() - new Date(row.cycleStart).getTime();
    if (age < 30 * 24 * 60 * 60 * 1000) return false;
    await db
      .update(userQuotas)
      .set({
        aiTokensUsedThisMonth: 0,
        bandwidthGbUsedThisMonth: 0,
        cycleStart: new Date(),
        updatedAt: new Date(),
      })
      .where(eq(userQuotas.userId, userId));
    return true;
  } catch {
    return false;
  }
}

export function formatPrice(cents: number): string {
  if (cents === 0) return "Free";
  return `$${(cents / 100).toFixed(2)}/mo`;
}

// ---------------------------------------------------------------------------
// AI quota hard-gate helpers
// ---------------------------------------------------------------------------

/**
 * Thrown by `assertAiQuota` when the user's AI token budget is exhausted.
 * Callers should catch this specifically and degrade gracefully — post a
 * comment, return a user-facing error, or skip silently — rather than letting
 * it bubble up as an unhandled exception.
 */
export class AiQuotaExceededError extends Error {
  readonly userId: string;
  readonly planSlug: string;
  constructor(userId: string, planSlug: string) {
    super(
      `AI quota exceeded for user ${userId} (plan: ${planSlug}). Upgrade at /settings/billing.`
    );
    this.name = "AiQuotaExceededError";
    this.userId = userId;
    this.planSlug = planSlug;
  }
}

interface QuotaCacheEntry {
  /** True = allowed, false = blocked. */
  allowed: boolean;
  /** Usage as a fraction of the limit (0–1). Used to emit the 90% warning. */
  fraction: number;
  planSlug: string;
  expiresAt: number;
}

/** Per-user TTL cache. A simple Map is fine — the process is single-replica. */
const _quotaCache = new Map<string, QuotaCacheEntry>();

/** Cache TTL in milliseconds. */
const QUOTA_CACHE_TTL_MS = 60_000;

/**
 * Check whether the user has AI token budget remaining, using an in-process
 * 60-second cache to avoid a DB round-trip on every AI call.
 *
 * - Returns `{ allowed: true }` when usage is under 100% of the plan limit.
 * - Returns `{ allowed: false }` when at or above the limit.
 * - Returns `{ allowed: true, nearLimit: true }` when usage is >= 90% but
 *   under 100%, so callers can log a warning.
 *
 * Fails **open** on any DB / billing error (same policy as `checkQuota`).
 */
export async function checkAiQuotaCached(
  userId: string
): Promise<{ allowed: boolean; nearLimit: boolean; planSlug: string }> {
  if (!userId) return { allowed: true, nearLimit: false, planSlug: "free" };

  const now = Date.now();
  const cached = _quotaCache.get(userId);
  if (cached && cached.expiresAt > now) {
    return {
      allowed: cached.allowed,
      nearLimit: cached.fraction >= 0.9 && cached.allowed,
      planSlug: cached.planSlug,
    };
  }

  try {
    const quota = await getUserQuota(userId);
    const limit = quota.plan.aiTokensMonthly;
    const used = quota.usage.aiTokensUsedThisMonth;
    const allowed = limit <= 0 || used < limit;
    const fraction = limit > 0 ? used / limit : 0;
    const entry: QuotaCacheEntry = {
      allowed,
      fraction,
      planSlug: quota.planSlug,
      expiresAt: now + QUOTA_CACHE_TTL_MS,
    };
    _quotaCache.set(userId, entry);
    return { allowed, nearLimit: fraction >= 0.9 && allowed, planSlug: quota.planSlug };
  } catch {
    // Fail open — billing is never a hard dependency for the primary path.
    return { allowed: true, nearLimit: false, planSlug: "free" };
  }
}

/**
 * Invalidate the cached quota entry for a user. Call after `bumpUsage` when
 * you want the next AI call to re-check immediately rather than waiting up to
 * 60 seconds.
 */
export function invalidateQuotaCache(userId: string): void {
  _quotaCache.delete(userId);
}

/**
 * Assert that the user has AI token quota remaining. Throws
 * `AiQuotaExceededError` if blocked; logs a warning (but proceeds) when
 * usage is 90-99% of the limit.
 *
 * Designed to be called at the top of every AI feature entry point.
 * Never throws on billing errors (fails open).
 */
export async function assertAiQuota(userId: string): Promise<void> {
  const { allowed, nearLimit, planSlug } = await checkAiQuotaCached(userId);
  if (nearLimit) {
    console.warn(
      `[billing] AI quota warning: user ${userId} (plan: ${planSlug}) is near their monthly AI token limit.`
    );
  }
  if (!allowed) {
    throw new AiQuotaExceededError(userId, planSlug);
  }
}
