// src/lib/billing/subscription-sync.ts — keep the local subscription row's period
// (the renew date) in sync with Stripe ON THE WEBHOOK, not only on a billing-page
// visit. The @better-auth/stripe plugin writes plan/status first; this refreshes
// the period + cancel state from the same event so "renews on <date>" is never
// stale after a renewal or plan change.
import "server-only";
import { eq } from "drizzle-orm";
import { revalidateTag } from "next/cache";
import type Stripe from "stripe";
import { subscription } from "@/lib/db/auth-schema";
import { db } from "@/lib/db/client";
import { syncMcpAddonFromEvent } from "./mcp-addon";
import { orgLimitsTag } from "./quotas";
import { syncOrgSeatsLimit } from "./seats-limit";
import { orgBillingTag } from "./status";

const SUBSCRIPTION_EVENTS = new Set([
  "customer.subscription.created",
  "customer.subscription.updated",
  "customer.subscription.deleted",
]);

export type SubscriptionPeriod = {
  status: string;
  periodStart: Date | null;
  periodEnd: Date | null;
  cancelAtPeriodEnd: boolean;
  billingInterval: string | null;
};

/** Extract the renew window + state from a Stripe subscription. `current_period_*`
 *  moved from the subscription onto the item in recent API versions — read the
 *  item first, fall back to the subscription. Pure (no I/O) so it's unit-tested. */
export function extractSubscriptionPeriod(
  sub: Stripe.Subscription,
): SubscriptionPeriod {
  const item = sub.items?.data?.[0];
  const itemAny = item as unknown as {
    current_period_start?: number;
    current_period_end?: number;
  };
  const subAny = sub as unknown as {
    current_period_start?: number;
    current_period_end?: number;
  };
  const ps = itemAny?.current_period_start ?? subAny.current_period_start;
  const pe = itemAny?.current_period_end ?? subAny.current_period_end;
  return {
    status: sub.status,
    periodStart: ps ? new Date(ps * 1000) : null,
    periodEnd: pe ? new Date(pe * 1000) : null,
    cancelAtPeriodEnd: sub.cancel_at_period_end ?? false,
    billingInterval: item?.price?.recurring?.interval ?? null,
  };
}

/**
 * Write one Stripe.Subscription's period/state into the matching local row
 * (by stripeSubscriptionId). The SINGLE source of truth for the Stripe→row
 * mapping: both the webhook path (syncSubscriptionFromEvent) and the God-Mode
 * "force resync" action (billing-actions.resyncOrgSubscriptionAction) go
 * through here, so the mapping is never duplicated. Idempotent and
 * plan-agnostic (the plugin owns `plan`). No-op when the subscription has no
 * id or no local row matches. Returns the org (referenceId) the row belongs
 * to, or null when nothing matched — callers use it to invalidate per-org
 * caches (QW2-A) without re-deriving the Stripe id → org mapping themselves.
 */
export async function syncSubscriptionRow(
  sub: Stripe.Subscription,
): Promise<string | null> {
  if (!sub?.id) return null;
  const period = extractSubscriptionPeriod(sub);
  const [row] = await db
    .update(subscription)
    .set({
      status: period.status,
      periodStart: period.periodStart,
      periodEnd: period.periodEnd,
      cancelAtPeriodEnd: period.cancelAtPeriodEnd,
      billingInterval: period.billingInterval,
    })
    .where(eq(subscription.stripeSubscriptionId, sub.id))
    .returning({ referenceId: subscription.referenceId });
  return row?.referenceId ?? null;
}

/**
 * On any subscription lifecycle event, refresh the local row's period/state by
 * stripeSubscriptionId. Idempotent and plan-agnostic (the plugin owns `plan`),
 * so it composes after the plugin's own handler — the @better-auth/stripe
 * plugin's own handler already wrote status/plan on the `subscription` row
 * BEFORE `onEvent` (and this) runs, so invalidating here also covers plan
 * changes it didn't touch (QW2-A: getOrgBillingInfo/getOrgQuotaLimits are
 * unstable_cache'd). No-op for non-subscription events or when no row matches
 * the Stripe id. Delegates the row write to syncSubscriptionRow (one mapping,
 * two callers).
 */
export async function syncSubscriptionFromEvent(
  event: Stripe.Event,
): Promise<void> {
  if (!SUBSCRIPTION_EVENTS.has(event.type)) return;
  const orgId = await syncSubscriptionRow(
    event.data.object as Stripe.Subscription,
  );
  if (orgId) {
    // MCP add-on is INCLUDED by plan (mcpMonthly > 0): reconcile the entitlement
    // right where the plan/status is synced. The @better-auth/stripe plugin has
    // already written the new plan to the `subscription` row above, and
    // resolveEffectivePlanForMcp derives the plan from THIS event's item
    // lookup_key — so an upgrade grants MCP and a cancellation/non-MCP plan
    // removes it, all in the same webhook pass (idempotent).
    // Best-effort: une activation MCP qui throw ne doit JAMAIS court-circuiter
    // les backstops en aval (surtout syncOrgSeatsLimit, plus critique). Le retry
    // Stripe et le prochain event réconcilieront l'entitlement (idempotent).
    try {
      await syncMcpAddonFromEvent(event, orgId);
    } catch (e) {
      console.error("[billing] échec sync add-on MCP (non bloquant):", e);
    }
    // Route Handler context (Stripe webhook → Better Auth's catch-all route),
    // not a Server Action — `updateTag` would throw here. `{ expire: 0 }` is
    // the documented immediate-expiry profile for webhooks/third-party
    // callers (revalidateTag.md).
    revalidateTag(orgBillingTag(orgId), { expire: 0 });
    revalidateTag(orgLimitsTag(orgId), { expire: 0 });
    // Sec audit A2 backstop: a plan change shifts the seats ceiling the
    // `member` insert trigger enforces — keep it fresh right on the webhook,
    // not just the cached read-path tags above.
    await syncOrgSeatsLimit(orgId);
  }
}
