/**
 * Stripe Webhook Service
 *
 * Service for processing Stripe webhook events
 */

import { Repository, DataSource } from 'typeorm';
import { StripeEvent, TenantSubscription } from '../entities/index.js';
import { logger } from '../../../shared/utils/logger.js';

// Stripe event types we handle
export type StripeEventType =
  | 'customer.subscription.created'
  | 'customer.subscription.updated'
  | 'customer.subscription.deleted'
  | 'customer.subscription.trial_will_end'
  | 'invoice.payment_succeeded'
  | 'invoice.payment_failed'
  | 'invoice.upcoming'
  | 'payment_intent.succeeded'
  | 'payment_intent.payment_failed'
  | 'checkout.session.completed';

export interface StripeWebhookPayload {
  id: string;
  type: string;
  api_version?: string;
  data: {
    object: Record<string, any>;
    previous_attributes?: Record<string, any>;
  };
  created: number;
  livemode: boolean;
}

export interface ProcessResult {
  success: boolean;
  eventId: string;
  message: string;
  error?: string;
}

export class StripeWebhookService {
  private eventRepository: Repository<StripeEvent>;
  private subscriptionRepository: Repository<TenantSubscription>;

  constructor(private dataSource: DataSource) {
    this.eventRepository = dataSource.getRepository(StripeEvent);
    this.subscriptionRepository = dataSource.getRepository(TenantSubscription);
  }

  /**
   * Process an incoming Stripe webhook event
   */
  async processWebhook(payload: StripeWebhookPayload): Promise<ProcessResult> {
    const { id: stripeEventId, type: eventType, api_version, data } = payload;

    // Check for duplicate event
    const existing = await this.eventRepository.findOne({
      where: { stripeEventId },
    });

    if (existing) {
      if (existing.processed) {
        return {
          success: true,
          eventId: existing.id,
          message: 'Event already processed',
        };
      }
      // Retry processing
      return this.retryProcessing(existing);
    }

    // Store the event
    const event = this.eventRepository.create({
      stripeEventId,
      eventType,
      apiVersion: api_version,
      data,
      processed: false,
      retryCount: 0,
    });

    await this.eventRepository.save(event);

    logger.info('Stripe webhook received', { stripeEventId, eventType });

    // Process the event
    try {
      await this.handleEvent(event, data.object);

      // Mark as processed
      event.processed = true;
      event.processedAt = new Date();
      await this.eventRepository.save(event);

      logger.info('Stripe webhook processed', { stripeEventId, eventType });

      return {
        success: true,
        eventId: event.id,
        message: 'Event processed successfully',
      };
    } catch (error) {
      const errorMessage = (error as Error).message;

      event.errorMessage = errorMessage;
      event.retryCount += 1;
      await this.eventRepository.save(event);

      logger.error('Stripe webhook processing failed', {
        stripeEventId,
        eventType,
        error: errorMessage,
      });

      return {
        success: false,
        eventId: event.id,
        message: 'Event processing failed',
        error: errorMessage,
      };
    }
  }

  /**
   * Handle specific event types
   */
  private async handleEvent(event: StripeEvent, object: Record<string, any>): Promise<void> {
    const eventType = event.eventType as StripeEventType;

    switch (eventType) {
      case 'customer.subscription.created':
        await this.handleSubscriptionCreated(object);
        break;

      case 'customer.subscription.updated':
        await this.handleSubscriptionUpdated(object);
        break;

      case 'customer.subscription.deleted':
        await this.handleSubscriptionDeleted(object);
        break;

      case 'customer.subscription.trial_will_end':
        await this.handleTrialWillEnd(object);
        break;

      case 'invoice.payment_succeeded':
        await this.handlePaymentSucceeded(object);
        break;

      case 'invoice.payment_failed':
        await this.handlePaymentFailed(object);
        break;

      case 'checkout.session.completed':
        await this.handleCheckoutCompleted(object);
        break;

      default:
        logger.warn('Unhandled Stripe event type', { eventType });
    }
  }

  /**
   * Handle subscription created
   */
  private async handleSubscriptionCreated(subscription: Record<string, any>): Promise<void> {
    const customerId = subscription.customer;
    const stripeSubscriptionId = subscription.id;
    const status = this.mapStripeStatus(subscription.status);

    // Find tenant by Stripe customer ID
    const existing = await this.subscriptionRepository.findOne({
      where: { stripeCustomerId: customerId },
    });

    if (existing) {
      existing.stripeSubscriptionId = stripeSubscriptionId;
      existing.status = status;
      existing.currentPeriodStart = new Date(subscription.current_period_start * 1000);
      existing.currentPeriodEnd = new Date(subscription.current_period_end * 1000);

      if (subscription.trial_end) {
        existing.trialEnd = new Date(subscription.trial_end * 1000);
      }

      await this.subscriptionRepository.save(existing);

      logger.info('Subscription created/linked', {
        tenantId: existing.tenantId,
        stripeSubscriptionId,
      });
    }
  }

  /**
   * Handle subscription updated
   */
  private async handleSubscriptionUpdated(subscription: Record<string, any>): Promise<void> {
    const stripeSubscriptionId = subscription.id;
    const status = this.mapStripeStatus(subscription.status);

    const existing = await this.subscriptionRepository.findOne({
      where: { stripeSubscriptionId },
    });

    if (existing) {
      existing.status = status;
      existing.currentPeriodStart = new Date(subscription.current_period_start * 1000);
      existing.currentPeriodEnd = new Date(subscription.current_period_end * 1000);

      if (subscription.cancel_at_period_end) {
        existing.cancelAtPeriodEnd = true;
      }

      if (subscription.canceled_at) {
        existing.cancelledAt = new Date(subscription.canceled_at * 1000);
      }

      await this.subscriptionRepository.save(existing);

      logger.info('Subscription updated', {
        tenantId: existing.tenantId,
        stripeSubscriptionId,
        status,
      });
    }
  }

  /**
   * Handle subscription deleted (cancelled)
   */
  private async handleSubscriptionDeleted(subscription: Record<string, any>): Promise<void> {
    const stripeSubscriptionId = subscription.id;

    const existing = await this.subscriptionRepository.findOne({
      where: { stripeSubscriptionId },
    });

    if (existing) {
      existing.status = 'cancelled';
      existing.cancelledAt = new Date();

      await this.subscriptionRepository.save(existing);

      logger.info('Subscription cancelled', {
        tenantId: existing.tenantId,
        stripeSubscriptionId,
      });
    }
  }

  /**
   * Handle trial will end (send notification)
   */
  private async handleTrialWillEnd(subscription: Record<string, any>): Promise<void> {
    const stripeSubscriptionId = subscription.id;
    const trialEnd = new Date(subscription.trial_end * 1000);

    const existing = await this.subscriptionRepository.findOne({
      where: { stripeSubscriptionId },
    });

    if (existing) {
      // TODO: Send notification to tenant
      logger.info('Trial ending soon', {
        tenantId: existing.tenantId,
        trialEnd,
      });
    }
  }

  /**
   * Handle payment succeeded
   */
  private async handlePaymentSucceeded(invoice: Record<string, any>): Promise<void> {
    const customerId = invoice.customer;
    const amountPaid = invoice.amount_paid;
    const invoiceId = invoice.id;

    const subscription = await this.subscriptionRepository.findOne({
      where: { stripeCustomerId: customerId },
    });

    if (subscription) {
      // Update last payment info
      subscription.lastPaymentAt = new Date();
      subscription.lastPaymentAmount = amountPaid / 100; // Stripe amounts are in cents
      subscription.status = 'active';

      await this.subscriptionRepository.save(subscription);

      logger.info('Payment succeeded', {
        tenantId: subscription.tenantId,
        invoiceId,
        amount: amountPaid / 100,
      });
    }
  }

  /**
   * Handle payment failed
   */
  private async handlePaymentFailed(invoice: Record<string, any>): Promise<void> {
    const customerId = invoice.customer;
    const invoiceId = invoice.id;
    const attemptCount = invoice.attempt_count;

    const subscription = await this.subscriptionRepository.findOne({
      where: { stripeCustomerId: customerId },
    });

    if (subscription) {
      subscription.status = 'past_due';
      await this.subscriptionRepository.save(subscription);

      // TODO: Send payment failed notification
      logger.warn('Payment failed', {
        tenantId: subscription.tenantId,
        invoiceId,
        attemptCount,
      });
    }
  }

  /**
   * Handle checkout session completed
   */
  private async handleCheckoutCompleted(session: Record<string, any>): Promise<void> {
    const customerId = session.customer;
    const subscriptionId = session.subscription;
    const metadata = session.metadata || {};
    const tenantId = metadata.tenant_id;

    if (tenantId) {
      // Link Stripe customer to tenant
      const subscription = await this.subscriptionRepository.findOne({
        where: { tenantId },
      });

      if (subscription) {
        subscription.stripeCustomerId = customerId;
        subscription.stripeSubscriptionId = subscriptionId;
        subscription.status = 'active';

        await this.subscriptionRepository.save(subscription);

        logger.info('Checkout completed', {
          tenantId,
          customerId,
          subscriptionId,
        });
      }
    }
  }

  /**
   * Map Stripe subscription status to our status
   */
  private mapStripeStatus(stripeStatus: string): 'active' | 'trial' | 'past_due' | 'cancelled' | 'suspended' {
    const statusMap: Record<string, 'active' | 'trial' | 'past_due' | 'cancelled' | 'suspended'> = {
      active: 'active',
      trialing: 'trial',
      past_due: 'past_due',
      canceled: 'cancelled',
      unpaid: 'past_due',
      incomplete: 'suspended',
      incomplete_expired: 'cancelled',
    };

    return statusMap[stripeStatus] || 'suspended';
  }

  /**
   * Retry processing a failed event
   */
  async retryProcessing(event: StripeEvent): Promise<ProcessResult> {
    if (event.retryCount >= 5) {
      return {
        success: false,
        eventId: event.id,
        message: 'Max retries exceeded',
        error: event.errorMessage || 'Unknown error',
      };
    }

    try {
      await this.handleEvent(event, event.data.object);

      event.processed = true;
      event.processedAt = new Date();
      event.errorMessage = undefined;
      await this.eventRepository.save(event);

      return {
        success: true,
        eventId: event.id,
        message: 'Event processed on retry',
      };
    } catch (error) {
      event.errorMessage = (error as Error).message;
      event.retryCount += 1;
      await this.eventRepository.save(event);

      return {
        success: false,
        eventId: event.id,
        message: 'Retry failed',
        error: event.errorMessage,
      };
    }
  }

  /**
   * Get failed events for retry
   */
  async getFailedEvents(limit: number = 100): Promise<StripeEvent[]> {
    return this.eventRepository.find({
      where: {
        processed: false,
      },
      order: { createdAt: 'ASC' },
      take: limit,
    });
  }

  /**
   * Get event by Stripe event ID
   */
  async findByStripeEventId(stripeEventId: string): Promise<StripeEvent | null> {
    return this.eventRepository.findOne({
      where: { stripeEventId },
    });
  }

  /**
   * Get recent events
   */
  async getRecentEvents(options?: {
    limit?: number;
    eventType?: string;
    processed?: boolean;
  }): Promise<StripeEvent[]> {
    const query = this.eventRepository.createQueryBuilder('event');

    if (options?.eventType) {
      query.andWhere('event.eventType = :eventType', { eventType: options.eventType });
    }

    if (options?.processed !== undefined) {
      query.andWhere('event.processed = :processed', { processed: options.processed });
    }

    return query
      .orderBy('event.createdAt', 'DESC')
      .take(options?.limit || 50)
      .getMany();
  }
}
