import Stripe from 'stripe';
import { stripe } from './stripe-client';
import { createAdminClient } from '@/shared/db/supabase-client';
import type { HandledStripeEvent, SubscriptionStatus } from './types';
import { syncEntitlements } from '@/shared/entitlements/resolver';

// --- ASA GENERATED START ---
// Idempotent Stripe webhook handler for payments-basic module.
//
// Research-validated patterns:
// 1. Raw body for signature verification (await request.text())
// 2. Idempotent processing (event ID dedup via webhook_events table)
// 3. Eager state sync (always fetch current state from Stripe API, don't trust payload)
// 4. Return 200 for unrecognized events (prevent Stripe retry loops)
// 5. Correct dunning: past_due = still has access
// --- ASA GENERATED END ---

// --- USER CODE START ---

const HANDLED_EVENTS: HandledStripeEvent[] = [
  'checkout.session.completed',
  'customer.subscription.created',
  'customer.subscription.updated',
  'customer.subscription.deleted',
  'invoice.paid',
  'invoice.payment_failed',
];

/**
 * Process incoming Stripe webhook event.
 *
 * IMPORTANT implementation notes:
 * - Uses raw body (string) for signature verification
 * - Checks idempotency before processing (event ID in webhook_events table)
 * - Always fetches current state from Stripe API (never trusts webhook payload alone)
 * - Returns 200 for all events (including unhandled) to prevent Stripe retries
 */
export async function handleWebhook(
  rawBody: string,
  signature: string,
): Promise<{ status: number; message: string }> {
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

  // 1. Verify signature using RAW body (not parsed JSON)
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(rawBody, signature, webhookSecret);
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    return { status: 400, message: `Webhook signature verification failed: ${message}` };
  }

  // 2. Idempotency check — skip if already processed
  const supabase = createAdminClient();
  const { data: existing } = await supabase
    .from('webhook_events')
    .select('event_id')
    .eq('event_id', event.id)
    .single();

  if (existing) {
    return { status: 200, message: `Event ${event.id} already processed` };
  }

  // 3. Record event as processing (before handling)
  await supabase.from('webhook_events').insert({
    event_id: event.id,
    event_type: event.type,
    processed_at: new Date().toISOString(),
  });

  // 4. Handle known events (fetch current state from Stripe, don't trust payload)
  if (HANDLED_EVENTS.includes(event.type as HandledStripeEvent)) {
    try {
      await processEvent(event, supabase);
    } catch (err) {
      console.error(`[ASA Webhook] Error processing ${event.type}:`, err);
      // Still return 200 — retrying won't help if our logic failed
    }
  }

  // 5. Return 200 for ALL events (including unrecognized)
  // Returning 400 for unrecognized events causes Stripe retry loops
  return { status: 200, message: `Event ${event.id} processed` };
}

/**
 * Process a specific Stripe event by fetching current state from Stripe API.
 *
 * PATTERN: Webhooks are SIGNALS, not source of truth.
 * Always fetch the latest canonical state from Stripe before updating DB.
 */
async function processEvent(event: Stripe.Event, supabase: any): Promise<void> {
  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      if (session.subscription && session.customer) {
        // Fetch current subscription state from Stripe (not from webhook payload)
        const subscription = await stripe.subscriptions.retrieve(
          session.subscription as string,
        );
        await upsertSubscription(supabase, subscription, session.customer as string);
      }
      break;
    }

    case 'customer.subscription.created':
    case 'customer.subscription.updated':
    case 'customer.subscription.deleted': {
      const subEvent = event.data.object as Stripe.Subscription;
      // Fetch current state from Stripe API (eager sync)
      const subscription = await stripe.subscriptions.retrieve(subEvent.id);
      await upsertSubscription(
        supabase,
        subscription,
        subscription.customer as string,
      );
      break;
    }

    case 'invoice.paid':
    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice;
      if (invoice.subscription) {
        const subscription = await stripe.subscriptions.retrieve(
          invoice.subscription as string,
        );
        await upsertSubscription(
          supabase,
          subscription,
          subscription.customer as string,
        );
      }
      break;
    }
  }
}

/**
 * Upsert subscription state in database.
 *
 * Maps Stripe subscription status to our state machine:
 * - active, past_due, trialing → access granted
 * - canceled, unpaid, incomplete_expired → access revoked
 */
async function upsertSubscription(
  supabase: any,
  subscription: Stripe.Subscription,
  stripeCustomerId: string,
): Promise<void> {
  // Find user by stripe_customer_id
  const { data: existingSub } = await supabase
    .from('subscriptions')
    .select('id, user_id')
    .eq('stripe_customer_id', stripeCustomerId)
    .single();

  const status = mapStripeStatus(subscription.status);
  const plan = subscription.status === 'canceled' ? 'free' : 'pro';

  const subscriptionData = {
    stripe_customer_id: stripeCustomerId,
    stripe_subscription_id: subscription.id,
    plan,
    status,
    current_period_end: subscription.current_period_end
      ? new Date(subscription.current_period_end * 1000).toISOString()
      : null,
    cancel_at_period_end: subscription.cancel_at_period_end ?? false,
    updated_at: new Date().toISOString(),
  };

  if (existingSub) {
    await supabase
      .from('subscriptions')
      .update(subscriptionData)
      .eq('id', existingSub.id);

    // Sync entitlements based on new plan
    await syncEntitlements(existingSub.user_id, plan);
  }
  // If no existing subscription found, the checkout handler should have created it
}

function mapStripeStatus(stripeStatus: string): SubscriptionStatus {
  const statusMap: Record<string, SubscriptionStatus> = {
    active: 'active',
    past_due: 'past_due',
    trialing: 'trialing',
    incomplete: 'incomplete',
    canceled: 'canceled',
    unpaid: 'unpaid',
    incomplete_expired: 'canceled',
  };
  return statusMap[stripeStatus] ?? 'canceled';
}

// --- USER CODE END ---
