/**
 * Billing service for SentryAgent.ai AgentIdP.
 * Manages Stripe checkout sessions, webhook processing, and subscription status.
 */

import { Pool } from 'pg';
import Stripe from 'stripe';
import { TierService } from './TierService.js';
import { isTierName } from '../config/tiers.js';

/**
 * Current subscription status for a tenant.
 */
export interface ISubscriptionStatus {
  /** The tenant (organization) UUID. */
  tenantId: string;
  /** Subscription status: 'free', 'active', 'past_due', 'canceled', etc. */
  status: string;
  /** End of the current billing period, or null if on free tier. */
  currentPeriodEnd: Date | null;
  /** Stripe subscription ID, or null if on free tier. */
  stripeSubscriptionId: string | null;
}

/** DB row shape for tenant_subscriptions queries. */
interface ISubscriptionRow {
  status: string;
  current_period_end: Date | null;
  stripe_subscription_id: string | null;
}

/**
 * Service for managing Stripe billing integration.
 * Handles checkout session creation, webhook event processing,
 * and subscription status retrieval.
 */
export class BillingService {
  /**
   * @param pool - PostgreSQL connection pool.
   * @param stripe - Configured Stripe client instance.
   * @param tierService - Optional TierService. When provided, tier upgrades are applied
   *                      when a checkout.session.completed event carries tier metadata.
   */
  constructor(
    private readonly pool: Pool,
    private readonly stripe: Stripe,
    private readonly tierService: TierService | null = null,
  ) {}

  /**
   * Creates a Stripe Checkout Session for a tenant to subscribe.
   * Returns the URL the tenant should be redirected to complete payment.
   *
   * @param tenantId - The tenant UUID (used as client_reference_id).
   * @param successUrl - URL to redirect to on successful checkout.
   * @param cancelUrl - URL to redirect to if checkout is cancelled.
   * @returns The Stripe Checkout Session URL.
   * @throws Error if the session URL is not returned by Stripe.
   */
  async createCheckoutSession(
    tenantId: string,
    successUrl: string,
    cancelUrl: string,
  ): Promise<string> {
    const priceId = process.env['STRIPE_PRICE_ID'];

    const session = await this.stripe.checkout.sessions.create({
      mode: 'subscription',
      client_reference_id: tenantId,
      line_items: priceId
        ? [{ price: priceId, quantity: 1 }]
        : undefined,
      success_url: successUrl,
      cancel_url: cancelUrl,
    });

    if (!session.url) {
      throw new Error('Stripe did not return a checkout session URL.');
    }

    return session.url;
  }

  /**
   * Verifies and processes an incoming Stripe webhook event.
   * Handles subscription created, updated, and deleted events
   * by upserting the tenant_subscriptions table.
   *
   * @param rawBody - The raw request body buffer (required for signature verification).
   * @param sig - The value of the Stripe-Signature request header.
   * @param webhookSecret - The Stripe webhook endpoint secret (whsec_...).
   * @throws Error if the webhook signature is invalid.
   */
  async handleWebhookEvent(
    rawBody: Buffer,
    sig: string,
    webhookSecret: string,
  ): Promise<void> {
    const event = this.stripe.webhooks.constructEvent(rawBody, sig, webhookSecret);

    if (
      event.type === 'customer.subscription.created' ||
      event.type === 'customer.subscription.updated' ||
      event.type === 'customer.subscription.deleted'
    ) {
      const subscription = event.data.object as Stripe.Subscription;
      await this.upsertSubscription(subscription);
    }

    // ── Tier upgrade via checkout session ────────────────────────────────────
    // When a checkout session is completed and the session metadata contains
    // { orgId, targetTier }, apply the tier upgrade to the organizations table.
    if (event.type === 'checkout.session.completed') {
      const session = event.data.object as Stripe.Checkout.Session;
      await this.applyTierUpgradeIfPresent(session);
    }
  }

  /**
   * Returns the current subscription status for a tenant.
   * When no subscription row exists, returns a free-tier status.
   *
   * @param tenantId - The tenant UUID.
   * @returns The current ISubscriptionStatus.
   */
  async getSubscriptionStatus(tenantId: string): Promise<ISubscriptionStatus> {
    const result = await this.pool.query<ISubscriptionRow>(
      `SELECT status, current_period_end, stripe_subscription_id
       FROM tenant_subscriptions
       WHERE tenant_id = $1
       LIMIT 1`,
      [tenantId],
    );

    if (result.rows.length === 0) {
      return {
        tenantId,
        status: 'free',
        currentPeriodEnd: null,
        stripeSubscriptionId: null,
      };
    }

    const row = result.rows[0];
    return {
      tenantId,
      status: row.status,
      currentPeriodEnd: row.current_period_end ?? null,
      stripeSubscriptionId: row.stripe_subscription_id ?? null,
    };
  }

  /**
   * Applies a tier upgrade when the checkout session metadata contains
   * the required fields (`orgId` and `targetTier`).
   * Skips silently when metadata is absent, incomplete, or TierService is not wired.
   *
   * @param session - The completed Stripe Checkout Session.
   */
  private async applyTierUpgradeIfPresent(session: Stripe.Checkout.Session): Promise<void> {
    if (this.tierService === null) return;

    const metadata = session.metadata;
    if (!metadata) return;

    const orgId = metadata['orgId'];
    const targetTier = metadata['targetTier'];

    if (!orgId || !targetTier) return;
    if (!isTierName(targetTier)) return;

    await this.tierService.applyUpgrade(orgId, targetTier);
  }

  /**
   * Upserts a Stripe subscription into tenant_subscriptions.
   * Resolves the tenant from the subscription's customer.
   * Requires the Stripe customer metadata to contain a `tenant_id` field,
   * OR a checkout session client_reference_id to have been stored.
   * Falls back to fetching the customer record to find metadata.tenant_id.
   *
   * @param subscription - The Stripe Subscription object.
   */
  private async upsertSubscription(subscription: Stripe.Subscription): Promise<void> {
    // Fetch the customer to retrieve tenant_id from metadata
    const customerId =
      typeof subscription.customer === 'string'
        ? subscription.customer
        : subscription.customer.id;

    const customer = await this.stripe.customers.retrieve(customerId);
    if (customer.deleted) {
      return;
    }

    const tenantId = (customer as Stripe.Customer).metadata?.['tenant_id'];
    if (!tenantId) {
      // Cannot map to a tenant — skip gracefully
      return;
    }

    // billing_cycle_anchor gives the next billing date (used as period end proxy).
    // ended_at is populated when the subscription is canceled.
    const periodTimestamp = subscription.ended_at ?? subscription.billing_cycle_anchor;
    const currentPeriodEnd = new Date(periodTimestamp * 1000);
    const status = subscription.status;

    await this.pool.query(
      `INSERT INTO tenant_subscriptions
         (tenant_id, status, stripe_customer_id, stripe_subscription_id, current_period_end, updated_at)
       VALUES ($1, $2, $3, $4, $5, NOW())
       ON CONFLICT (tenant_id)
       DO UPDATE SET
         status                 = EXCLUDED.status,
         stripe_customer_id     = EXCLUDED.stripe_customer_id,
         stripe_subscription_id = EXCLUDED.stripe_subscription_id,
         current_period_end     = EXCLUDED.current_period_end,
         updated_at             = NOW()`,
      [tenantId, status, customerId, subscription.id, currentPeriodEnd],
    );
  }
}
