/**
 * PayPal Integration Service
 *
 * Handles PayPal subscription creation, webhook processing, and payment management
 * Uses @paypal/checkout-server-sdk for PayPal API integration
 */

// @ts-ignore - @paypal/checkout-server-sdk types not declared
import paypal from '@paypal/checkout-server-sdk';
import { PrismaClient, SubscriptionStatus, PaymentStatus, PaymentType, BillingCycle } from '@prisma/client';
import { logger } from '../utils/logger';
import { subscriptionEventsTotal, paymentsTotal, revenueTotal, trackError } from '../utils/metrics';
import { allocateCredits } from './creditService';
import { tierService } from './tierService';
import { config, getPayPalConfig } from '../config';
import { PayPalWebhookEvent, BillingCycle as BillingCycleType } from '../types';
import { calculatePeriodEnd } from '../utils/dateUtils';

const prisma = new PrismaClient();

/**
 * Initialize PayPal environment and client
 */
function getPayPalClient(): paypal.core.PayPalHttpClient {
  const paypalConfig = getPayPalConfig();

  const environment =
    paypalConfig.mode === 'live'
      ? new paypal.core.LiveEnvironment(paypalConfig.clientId, paypalConfig.clientSecret)
      : new paypal.core.SandboxEnvironment(paypalConfig.clientId, paypalConfig.clientSecret);

  return new paypal.core.PayPalHttpClient(environment);
}

/**
 * Map plan tier string to lowercase slug
 */
function mapPlanTier(tier: string): string {
  return tier.toLowerCase();
}

/**
 * Calculate the customer cost per character for a given plan tier and billing cycle.
 * Formula: subscription_price / credit_allocation
 *
 * @returns Cost per character, or 0 if tier not found or credit_allocation is 0
 */
async function calculateCustomerCostPerChar(planTier: string, billingCycle: BillingCycle): Promise<number> {
  const tier = await tierService.getTierBySlug(mapPlanTier(planTier), 'translate');
  if (!tier || tier.credit_allocation === 0) return 0;

  const price = billingCycle === BillingCycle.monthly
    ? Number(tier.monthly_price)
    : Number(tier.annual_price);

  return price / tier.credit_allocation;
}


/**
 * Create a PayPal subscription for a user
 *
 * @param userId - User UUID
 * @param planTier - Subscription plan tier (starter, professional, enterprise)
 * @param billingCycle - Billing cycle (monthly, annual)
 * @returns Approval URL for user to complete subscription
 */
export async function createSubscription(
  userId: string,
  planTier: string,
  billingCycle: BillingCycleType,
  customId?: string,
  plugin = 'translate'
): Promise<{ approvalUrl: string; subscriptionId: string }> {
  try {
    // Get user details
    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: { email: true },
    });

    if (!user) {
      throw new Error('User not found');
    }

    // Get PayPal plan ID
    const planId = await tierService.getPayPalPlanId(planTier, billingCycle, plugin);

    if (!planId) {
      throw new Error(`Invalid plan tier or billing cycle: ${planTier}/${billingCycle}`);
    }

    // Create subscription request
    const request = new paypal.subscriptions.SubscriptionsCreateRequest();
    request.requestBody({
      plan_id: planId,
      subscriber: {
        email_address: user.email,
      },
      application_context: {
        brand_name: 'translate.press.zone',
        locale: 'en-US',
        shipping_preference: 'NO_SHIPPING',
        user_action: 'SUBSCRIBE_NOW',
        return_url: `${config.frontendUrl}/subscription/success`,
        cancel_url: `${config.frontendUrl}/subscription/cancel`,
      },
      // custom_id carries our server-side checkout-session reference when present,
      // so the webhook handler can look up plugin / site_url from the session row
      // without trusting anything in the payload itself.
      custom_id: customId ?? userId,
    });

    // Execute request
    const client = getPayPalClient();
    const response = await client.execute(request);

    // Extract approval URL
    const approvalLink = response.result.links?.find((link: any) => link.rel === 'approve');

    if (!approvalLink) {
      throw new Error('No approval URL returned from PayPal');
    }

    logger.info('PayPal subscription created', {
      userId,
      planTier,
      billingCycle,
      subscriptionId: response.result.id,
    });

    // Track metrics
    subscriptionEventsTotal.inc({ event_type: 'subscription_created' });

    return {
      approvalUrl: approvalLink.href,
      subscriptionId: response.result.id,
    };
  } catch (error) {
    logger.error('Failed to create PayPal subscription', {
      userId,
      planTier,
      billingCycle,
      error,
    });
    trackError('paypal', 'subscription_creation_failed');
    throw error;
  }
}

/**
 * Verify PayPal webhook signature
 *
 * @param payload - Webhook payload as string
 * @param headers - Webhook headers
 * @returns True if signature is valid
 */
export async function verifyWebhookSignature(
  payload: string,
  headers: Record<string, string>
): Promise<boolean> {
  try {
    const transmissionId = headers['paypal-transmission-id'];
    const transmissionTime = headers['paypal-transmission-time'];
    const certUrl = headers['paypal-cert-url'];
    const authAlgo = headers['paypal-auth-algo'];
    const transmissionSig = headers['paypal-transmission-sig'];

    if (!transmissionId || !transmissionTime || !certUrl || !authAlgo || !transmissionSig) {
      logger.warn('Missing PayPal webhook signature headers');
      return false;
    }

    // Create verification request
    const request = new paypal.notifications.WebhooksVerifySignatureRequest();
    request.requestBody({
      transmission_id: transmissionId,
      transmission_time: transmissionTime,
      cert_url: certUrl,
      auth_algo: authAlgo,
      transmission_sig: transmissionSig,
      webhook_id: config.paypalWebhookId,
      webhook_event: JSON.parse(payload),
    });

    // Verify signature
    const client = getPayPalClient();
    const response = await client.execute(request);

    const isValid = response.result.verification_status === 'SUCCESS';

    if (!isValid) {
      logger.warn('PayPal webhook signature verification failed', {
        verificationStatus: response.result.verification_status,
      });
    }

    return isValid;
  } catch (error) {
    logger.error('Failed to verify PayPal webhook signature', { error });
    trackError('paypal', 'webhook_verification_failed');
    return false;
  }
}

/**
 * Handle PayPal webhook events
 *
 * @param payload - Webhook event payload
 * @param headers - Webhook headers
 */
export async function handleWebhookEvent(
  payload: PayPalWebhookEvent,
  headers: Record<string, string>
): Promise<void> {
  try {
    // Verify webhook signature
    const isValid = await verifyWebhookSignature(JSON.stringify(payload), headers);

    if (!isValid) {
      logger.warn('Rejecting PayPal webhook with invalid signature', {
        eventId: payload.id,
        eventType: payload.event_type,
      });
      throw new Error('Invalid webhook signature');
    }

    logger.info('Processing PayPal webhook event', {
      eventId: payload.id,
      eventType: payload.event_type,
      subscriptionId: payload.resource.id,
    });

    // Track metrics
    subscriptionEventsTotal.inc({ event_type: payload.event_type });

    // Handle different event types
    switch (payload.event_type) {
      case 'BILLING.SUBSCRIPTION.ACTIVATED':
        await handleSubscriptionActivated(payload);
        break;

      case 'PAYMENT.SALE.COMPLETED':
        await handlePaymentCompleted(payload);
        break;

      case 'BILLING.SUBSCRIPTION.CANCELLED':
        await handleSubscriptionCancelled(payload);
        break;

      case 'BILLING.SUBSCRIPTION.SUSPENDED':
        await handleSubscriptionSuspended(payload);
        break;

      case 'BILLING.SUBSCRIPTION.EXPIRED':
        await handleSubscriptionExpired(payload);
        break;

      case 'BILLING.SUBSCRIPTION.UPDATED':
        await handleSubscriptionUpdated(payload);
        break;

      default:
        logger.info('Unhandled PayPal webhook event type', {
          eventType: payload.event_type,
        });
    }
  } catch (error) {
    logger.error('Failed to handle PayPal webhook event', {
      eventId: payload.id,
      eventType: payload.event_type,
      error,
    });
    trackError('paypal', 'webhook_processing_failed');
    throw error;
  }
}

/**
 * Handle BILLING.SUBSCRIPTION.ACTIVATED event
 */
async function handleSubscriptionActivated(payload: PayPalWebhookEvent): Promise<void> {
  try {
    const subscriptionId = payload.resource.id;
    const userId = payload.resource.subscriber?.payer_id || (payload as any).custom_id;

    if (!userId) {
      throw new Error('User ID not found in webhook payload');
    }

    // Extract plan information
    const planId = payload.resource.plan_id;
    if (!planId) {
      throw new Error('Missing plan_id in subscription data');
    }
    const { planTier, billingCycle } = await parsePlanId(planId);

    // Calculate period dates
    const currentPeriodStart = new Date();
    const currentPeriodEnd = calculatePeriodEnd(currentPeriodStart, billingCycle);

    // Calculate customer cost per token for this subscription
    const customerCostPerChar = await calculateCustomerCostPerChar(planTier, billingCycle);

    // Create or update subscription in database
    await prisma.subscription.upsert({
      where: { user_id_plugin: { user_id: userId, plugin: 'translate' } },
      create: {
        user_id: userId,
        plugin: 'translate',
        plan_tier: planTier,
        billing_cycle: billingCycle,
        status: SubscriptionStatus.active,
        paypal_subscription_id: subscriptionId,
        current_period_start: currentPeriodStart,
        current_period_end: currentPeriodEnd,
        cancel_at_period_end: false,
        customer_cost_per_char: customerCostPerChar,
      },
      update: {
        plan_tier: planTier,
        billing_cycle: billingCycle,
        status: SubscriptionStatus.active,
        paypal_subscription_id: subscriptionId,
        current_period_start: currentPeriodStart,
        current_period_end: currentPeriodEnd,
        cancel_at_period_end: false,
        customer_cost_per_char: customerCostPerChar,
      },
    });

    // Allocate credits for the subscription tier
    const creditAmount = await tierService.getCreditAllocation(mapPlanTier(planTier), 'translate');
    await allocateCredits(
      userId,
      creditAmount,
      `Subscription activated: ${planTier} (${billingCycle})`,
      undefined
    );

    logger.info('Subscription activated', {
      userId,
      subscriptionId,
      planTier,
      billingCycle,
      creditsAllocated: creditAmount,
    });
  } catch (error) {
    logger.error('Failed to handle subscription activation', {
      subscriptionId: payload.resource.id,
      error,
    });
    throw error;
  }
}

/**
 * Handle PAYMENT.SALE.COMPLETED event
 */
async function handlePaymentCompleted(payload: PayPalWebhookEvent): Promise<void> {
  try {
    const paymentId = payload.resource.id;
    const subscriptionId = (payload.resource as any).billing_agreement_id;
    const amount = parseFloat((payload.resource as any).amount?.total || '0');
    const currency = (payload.resource as any).amount?.currency || 'USD';

    // Find subscription
    const subscription = await prisma.subscription.findUnique({
      where: { paypal_subscription_id: subscriptionId },
    });

    if (!subscription) {
      logger.warn('Subscription not found for payment', { subscriptionId, paymentId });
      return;
    }

    // Create payment record
    const payment = await prisma.payment.create({
      data: {
        user_id: subscription.user_id,
        paypal_payment_id: paymentId,
        amount,
        currency,
        status: PaymentStatus.completed,
        type: PaymentType.subscription_payment,
        subscription_id: subscription.id,
      },
    });

    // Update subscription period
    const newPeriodStart = new Date();
    const newPeriodEnd = calculatePeriodEnd(newPeriodStart, subscription.billing_cycle);

    // Recalculate customer cost per token on renewal
    const customerCostPerChar = await calculateCustomerCostPerChar(
      subscription.plan_tier,
      subscription.billing_cycle
    );

    await prisma.subscription.update({
      where: { id: subscription.id },
      data: {
        current_period_start: newPeriodStart,
        current_period_end: newPeriodEnd,
        status: SubscriptionStatus.active,
        customer_cost_per_char: customerCostPerChar,
      },
    });

    // Allocate credits for renewal
    const creditAmount = await tierService.getCreditAllocation(
      mapPlanTier(subscription.plan_tier),
      subscription.plugin
    );
    await allocateCredits(
      subscription.user_id,
      creditAmount,
      `Subscription renewed: ${subscription.plan_tier} (${subscription.billing_cycle})`,
      payment.id
    );

    // Track metrics
    paymentsTotal.inc({ status: 'completed', type: 'subscription_payment' });
    revenueTotal.inc(
      { tier: subscription.plan_tier, cycle: subscription.billing_cycle },
      amount
    );

    logger.info('Payment completed', {
      userId: subscription.user_id,
      paymentId,
      subscriptionId,
      amount,
      currency,
      creditsAllocated: creditAmount,
    });
  } catch (error) {
    logger.error('Failed to handle payment completion', {
      paymentId: payload.resource.id,
      error,
    });
    throw error;
  }
}

/**
 * Handle BILLING.SUBSCRIPTION.CANCELLED event
 */
async function handleSubscriptionCancelled(payload: PayPalWebhookEvent): Promise<void> {
  try {
    const subscriptionId = payload.resource.id;

    // Update subscription status
    const subscription = await prisma.subscription.update({
      where: { paypal_subscription_id: subscriptionId },
      data: {
        status: SubscriptionStatus.cancelled,
        cancel_at_period_end: true,
      },
    });

    logger.info('Subscription cancelled', {
      userId: subscription.user_id,
      subscriptionId,
    });
  } catch (error) {
    logger.error('Failed to handle subscription cancellation', {
      subscriptionId: payload.resource.id,
      error,
    });
    throw error;
  }
}

/**
 * Handle BILLING.SUBSCRIPTION.SUSPENDED event
 */
async function handleSubscriptionSuspended(payload: PayPalWebhookEvent): Promise<void> {
  try {
    const subscriptionId = payload.resource.id;

    // Update subscription status
    const subscription = await prisma.subscription.update({
      where: { paypal_subscription_id: subscriptionId },
      data: {
        status: SubscriptionStatus.suspended,
      },
    });

    logger.info('Subscription suspended', {
      userId: subscription.user_id,
      subscriptionId,
    });
  } catch (error) {
    logger.error('Failed to handle subscription suspension', {
      subscriptionId: payload.resource.id,
      error,
    });
    throw error;
  }
}

/**
 * Handle BILLING.SUBSCRIPTION.EXPIRED event
 */
async function handleSubscriptionExpired(payload: PayPalWebhookEvent): Promise<void> {
  try {
    const subscriptionId = payload.resource.id;

    // Update subscription status
    const subscription = await prisma.subscription.update({
      where: { paypal_subscription_id: subscriptionId },
      data: {
        status: SubscriptionStatus.cancelled,
      },
    });

    logger.info('Subscription expired', {
      userId: subscription.user_id,
      subscriptionId,
    });
  } catch (error) {
    logger.error('Failed to handle subscription expiration', {
      subscriptionId: payload.resource.id,
      error,
    });
    throw error;
  }
}

/**
 * Handle BILLING.SUBSCRIPTION.UPDATED event
 */
async function handleSubscriptionUpdated(payload: PayPalWebhookEvent): Promise<void> {
  try {
    const subscriptionId = payload.resource.id;
    const planId = payload.resource.plan_id;

    if (!planId) {
      logger.warn('No plan ID in subscription update', { subscriptionId });
      return;
    }

    const { planTier, billingCycle } = await parsePlanId(planId);

    // Recalculate customer cost per token for the new plan
    const customerCostPerChar = await calculateCustomerCostPerChar(planTier, billingCycle);

    // Update subscription
    const subscription = await prisma.subscription.update({
      where: { paypal_subscription_id: subscriptionId },
      data: {
        plan_tier: planTier,
        billing_cycle: billingCycle,
        customer_cost_per_char: customerCostPerChar,
      },
    });

    logger.info('Subscription updated', {
      userId: subscription.user_id,
      subscriptionId,
      planTier,
      billingCycle,
      customerCostPerChar,
    });
  } catch (error) {
    logger.error('Failed to handle subscription update', {
      subscriptionId: payload.resource.id,
      error,
    });
    throw error;
  }
}

/**
 * Cancel a subscription
 *
 * @param userId - User UUID
 * @param subscriptionId - PayPal subscription ID
 * @param reason - Cancellation reason
 */
export async function cancelSubscription(
  userId: string,
  subscriptionId: string,
  reason?: string
): Promise<void> {
  try {
    // Verify subscription belongs to user
    const subscription = await prisma.subscription.findFirst({
      where: {
        user_id: userId,
        paypal_subscription_id: subscriptionId,
      },
    });

    if (!subscription) {
      throw new Error('Subscription not found or does not belong to user');
    }

    // Cancel subscription in PayPal
    const request = new paypal.subscriptions.SubscriptionsCancelRequest(subscriptionId);
    request.requestBody({
      reason: reason || 'User requested cancellation',
    });

    const client = getPayPalClient();
    await client.execute(request);

    // Update subscription status
    await prisma.subscription.update({
      where: { id: subscription.id },
      data: {
        status: SubscriptionStatus.cancelled,
        cancel_at_period_end: true,
      },
    });

    logger.info('Subscription cancelled', {
      userId,
      subscriptionId,
      reason,
    });

    // Track metrics
    subscriptionEventsTotal.inc({ event_type: 'subscription_cancelled' });
  } catch (error) {
    logger.error('Failed to cancel subscription', {
      userId,
      subscriptionId,
      error,
    });
    trackError('paypal', 'subscription_cancellation_failed');
    throw error;
  }
}

/**
 * Parse PayPal plan ID to extract tier and billing cycle.
 * Uses database-backed config (via getPayPalConfig) for plan ID lookups.
 *
 * @throws Error if plan ID is not recognized
 */
export async function parsePlanId(planId: string, plugin = 'translate'): Promise<{
  planTier: string;
  billingCycle: BillingCycle;
}> {
  const tiers = await tierService.getTiersByPlugin(plugin);
  for (const tier of tiers) {
    if (tier.paypal_plan_monthly === planId) {
      return { planTier: tier.slug, billingCycle: BillingCycle.monthly };
    }
    if (tier.paypal_plan_annual === planId) {
      return { planTier: tier.slug, billingCycle: BillingCycle.annual };
    }
  }

  throw new Error(`Unknown PayPal plan ID: ${planId}. Ensure the tier plan is configured.`);
}

/**
 * Custom request class for refunding a legacy billing-agreement sale via the
 * PayPal v1 REST API (POST /v1/payments/sale/{saleId}/refund).
 *
 * The @paypal/checkout-server-sdk only exposes the Orders-v2 CapturesRefundRequest
 * (/v2/payments/captures/{id}/refund). Subscription payments use the older
 * Billing Agreements flow where the stored paypal_payment_id is a *sale* ID, not
 * a capture ID. We replicate the same request-object shape the SDK client expects.
 */
class SaleRefundRequest {
  path: string;
  verb: string;
  body: any;
  headers: Record<string, string>;

  constructor(saleId: string) {
    this.path = `/v1/payments/sale/${encodeURIComponent(saleId)}/refund`;
    this.verb = 'POST';
    this.body = null;
    this.headers = { 'Content-Type': 'application/json' };
  }

  requestBody(body: any): this {
    this.body = body;
    return this;
  }
}

/**
 * Refund a PayPal payment (sale) by its sale ID.
 *
 * Uses the PayPal v1 Billing Agreements sale-refund endpoint because
 * paypal_payment_id stores a *sale* ID from PAYMENT.SALE.COMPLETED events
 * (not a v2 capture ID). The @paypal/checkout-server-sdk does not expose
 * a sale-refund request, so we use the same request-object protocol the
 * SDK HTTP client expects.
 *
 * @param params.paypalPaymentId - The sale ID stored in Payment.paypal_payment_id
 * @param params.amount          - Optional partial-refund amount (omit for full refund)
 * @param params.currency        - Currency code (required when amount is provided)
 * @param params.reason          - Optional reason note passed to PayPal
 * @returns PayPal refund ID
 */
export async function refundPayment(params: {
  paypalPaymentId: string;
  amount?: number;
  currency?: string;
  reason?: string;
}): Promise<{ refund_id: string }> {
  const { paypalPaymentId, amount, currency, reason } = params;

  try {
    const request = new SaleRefundRequest(paypalPaymentId);

    const body: Record<string, any> = {};
    if (amount !== undefined && amount > 0) {
      body.amount = {
        total: amount.toFixed(2),
        currency: currency ?? 'USD',
      };
    }
    if (reason) {
      body.description = reason;
    }
    request.requestBody(body);

    const client = getPayPalClient();
    const response = await client.execute(request);

    const refundId: string = response.result?.id ?? response.result?.refund_id ?? '';

    logger.info('PayPal sale refunded successfully', {
      paypalPaymentId,
      refundId,
      amount,
      currency,
    });

    return { refund_id: refundId };
  } catch (error) {
    logger.error('Failed to refund PayPal payment', {
      paypalPaymentId,
      amount,
      currency,
      error,
    });
    trackError('paypal', 'refund_failed');
    throw error;
  }
}

/**
 * Get subscription details from PayPal
 *
 * @param subscriptionId - PayPal subscription ID
 * @returns Subscription details
 */
export async function getSubscriptionDetails(subscriptionId: string): Promise<any> {
  try {
    const request = new paypal.subscriptions.SubscriptionsGetRequest(subscriptionId);
    const client = getPayPalClient();
    const response = await client.execute(request);

    return response.result;
  } catch (error) {
    logger.error('Failed to get subscription details', {
      subscriptionId,
      error,
    });
    throw error;
  }
}
