/**
 * Subscription Service
 *
 * Business logic for subscription and license management.
 */

import { Injectable, Inject, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { v4 as uuidv4 } from 'uuid';
import {
  SubscriptionRepository,
  SUBSCRIPTION_REPOSITORY,
} from '@domain/ports/out/subscription.repository';
import { LicenseRepository, LICENSE_REPOSITORY } from '@domain/ports/out/license.repository';
import {
  OrganizationRepository,
  ORGANIZATION_REPOSITORY,
} from '@domain/ports/out/organization.repository';
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
import { StripePort, STRIPE_PORT } from '@domain/ports/out/stripe.port';
import { Subscription } from '@domain/entities/subscription.entity';
import { License } from '@domain/entities/license.entity';
import { SubscriptionPlan, SubscriptionPlanType } from '@domain/value-objects/subscription-plan.vo';
import { SubscriptionStatus } from '@domain/value-objects/subscription-status.vo';
import {
  NoLicensesAvailableException,
  LicenseAlreadyAssignedException,
} from '@domain/exceptions/subscription.exceptions';
import {
  CreateCheckoutSessionDto,
  CreatePortalSessionDto,
  SubscriptionOverviewResponseDto,
  CanInviteResponseDto,
  CheckoutSessionResponseDto,
  PortalSessionResponseDto,
  LicenseResponseDto,
  PlanDetailsDto,
  AllPlansResponseDto,
  SubscriptionPlanDto,
  SubscriptionStatusDto,
} from '../dto/subscription.dto';

@Injectable()
export class SubscriptionService {
  private readonly logger = new Logger(SubscriptionService.name);

  constructor(
    @Inject(SUBSCRIPTION_REPOSITORY)
    private readonly subscriptionRepository: SubscriptionRepository,
    @Inject(LICENSE_REPOSITORY)
    private readonly licenseRepository: LicenseRepository,
    @Inject(ORGANIZATION_REPOSITORY)
    private readonly organizationRepository: OrganizationRepository,
    @Inject(USER_REPOSITORY)
    private readonly userRepository: UserRepository,
    @Inject(STRIPE_PORT)
    private readonly stripeAdapter: StripePort,
    private readonly configService: ConfigService
  ) {}

  /**
   * Get subscription overview for an organization
   * ADMIN users always see a PLATINIUM plan with no expiration
   */
  async getSubscriptionOverview(
    organizationId: string,
    userRole?: string
  ): Promise<SubscriptionOverviewResponseDto> {
    const subscription = await this.getOrCreateSubscription(organizationId);
    const activeLicenses = await this.licenseRepository.findActiveBySubscriptionId(subscription.id);

    // Enrich licenses with user information
    const enrichedLicenses = await Promise.all(
      activeLicenses.map(async license => {
        const user = await this.userRepository.findById(license.userId);
        return this.mapLicenseToDto(license, user);
      })
    );

    // Count only non-ADMIN licenses for quota calculation
    // ADMIN users have unlimited licenses and don't count against the quota
    const usedLicenses = await this.licenseRepository.countActiveBySubscriptionIdExcludingAdmins(
      subscription.id
    );

    // ADMIN users always have PLATINIUM plan with no expiration
    const isAdmin = userRole === 'ADMIN';
    const effectivePlan = isAdmin ? SubscriptionPlan.platinium() : subscription.plan;
    const maxLicenses = effectivePlan.maxLicenses;
    const availableLicenses = effectivePlan.isUnlimited()
      ? -1
      : Math.max(0, maxLicenses - usedLicenses);

    return {
      id: subscription.id,
      organizationId: subscription.organizationId,
      plan: effectivePlan.value as SubscriptionPlanDto,
      planDetails: this.mapPlanToDto(effectivePlan),
      status: subscription.status.value as SubscriptionStatusDto,
      usedLicenses,
      maxLicenses,
      availableLicenses,
      cancelAtPeriodEnd: false,
      currentPeriodStart: isAdmin ? undefined : subscription.currentPeriodStart || undefined,
      currentPeriodEnd: isAdmin ? undefined : subscription.currentPeriodEnd || undefined,
      createdAt: subscription.createdAt,
      updatedAt: subscription.updatedAt,
      licenses: enrichedLicenses,
    };
  }

  /**
   * Get all available plans
   */
  getAllPlans(): AllPlansResponseDto {
    const plans = SubscriptionPlan.getAllPlans().map(plan => this.mapPlanToDto(plan));
    return { plans };
  }

  /**
   * Check if organization can invite more users
   * Note: ADMIN users don't count against the license quota and always have unlimited licenses
   */
  async canInviteUser(organizationId: string, userRole?: string): Promise<CanInviteResponseDto> {
    // ADMIN users always have unlimited invitations
    if (userRole === 'ADMIN') {
      return {
        canInvite: true,
        availableLicenses: -1,
        usedLicenses: 0,
        maxLicenses: -1,
        message: undefined,
      };
    }

    const subscription = await this.getOrCreateSubscription(organizationId);
    // Count only non-ADMIN licenses - ADMIN users have unlimited licenses
    const usedLicenses = await this.licenseRepository.countActiveBySubscriptionIdExcludingAdmins(
      subscription.id
    );

    const maxLicenses = subscription.maxLicenses;
    const canInvite =
      subscription.isActive() && (subscription.isUnlimited() || usedLicenses < maxLicenses);

    const availableLicenses = subscription.isUnlimited()
      ? -1
      : Math.max(0, maxLicenses - usedLicenses);

    let message: string | undefined;
    if (!subscription.isActive()) {
      message = 'Your subscription is not active. Please update your payment method.';
    } else if (!canInvite) {
      message = `You have reached the maximum number of users (${maxLicenses}) for your ${subscription.plan.name} plan. Upgrade to add more users.`;
    }

    return {
      canInvite,
      availableLicenses,
      usedLicenses,
      maxLicenses,
      message,
    };
  }

  /**
   * Create a Stripe Checkout session for subscription upgrade
   */
  async createCheckoutSession(
    organizationId: string,
    userId: string,
    dto: CreateCheckoutSessionDto
  ): Promise<CheckoutSessionResponseDto> {
    const organization = await this.organizationRepository.findById(organizationId);
    if (!organization) {
      throw new NotFoundException('Organization not found');
    }

    const user = await this.userRepository.findById(userId);
    if (!user) {
      throw new NotFoundException('User not found');
    }

    // Cannot checkout for FREE plan
    if (dto.plan === SubscriptionPlanDto.BRONZE) {
      throw new BadRequestException('Cannot create checkout session for Bronze plan');
    }

    const subscription = await this.getOrCreateSubscription(organizationId);

    const frontendUrl = this.configService.get<string>('FRONTEND_URL', 'http://localhost:3000');
    // Include {CHECKOUT_SESSION_ID} placeholder - Stripe replaces it with actual session ID
    const successUrl =
      dto.successUrl ||
      `${frontendUrl}/dashboard/settings/organization?success=true&session_id={CHECKOUT_SESSION_ID}`;
    const cancelUrl =
      dto.cancelUrl || `${frontendUrl}/dashboard/settings/organization?canceled=true`;

    const result = await this.stripeAdapter.createCheckoutSession({
      organizationId,
      organizationName: organization.name,
      email: user.email,
      plan: dto.plan as SubscriptionPlanType,
      billingInterval: dto.billingInterval as 'monthly' | 'yearly',
      successUrl,
      cancelUrl,
      customerId: subscription.stripeCustomerId || undefined,
    });

    this.logger.log(
      `Created checkout session for organization ${organizationId}, plan ${dto.plan}`
    );

    return {
      sessionId: result.sessionId,
      sessionUrl: result.sessionUrl,
    };
  }

  /**
   * Create a Stripe Customer Portal session
   */
  async createPortalSession(
    organizationId: string,
    dto: CreatePortalSessionDto
  ): Promise<PortalSessionResponseDto> {
    const subscription = await this.subscriptionRepository.findByOrganizationId(organizationId);

    if (!subscription?.stripeCustomerId) {
      throw new BadRequestException(
        'No Stripe customer found for this organization. Please complete a checkout first.'
      );
    }

    const frontendUrl = this.configService.get<string>('FRONTEND_URL', 'http://localhost:3000');
    const returnUrl = dto.returnUrl || `${frontendUrl}/dashboard/settings/organization`;

    const result = await this.stripeAdapter.createPortalSession({
      customerId: subscription.stripeCustomerId,
      returnUrl,
    });

    this.logger.log(`Created portal session for organization ${organizationId}`);

    return {
      sessionUrl: result.sessionUrl,
    };
  }

  /**
   * Sync subscription from Stripe
   * Useful when webhooks are not available (e.g., local development)
   * @param organizationId - The organization ID
   * @param sessionId - Optional Stripe checkout session ID (used after checkout completes)
   */
  async syncFromStripe(
    organizationId: string,
    sessionId?: string
  ): Promise<SubscriptionOverviewResponseDto> {
    let subscription = await this.subscriptionRepository.findByOrganizationId(organizationId);

    if (!subscription) {
      subscription = await this.getOrCreateSubscription(organizationId);
    }

    let stripeSubscriptionId = subscription.stripeSubscriptionId;
    let stripeCustomerId = subscription.stripeCustomerId;

    // If we have a session ID, ALWAYS retrieve the checkout session to get the latest subscription details
    // This is important for upgrades where Stripe may create a new subscription
    if (sessionId) {
      this.logger.log(
        `Retrieving checkout session ${sessionId} for organization ${organizationId}`
      );
      const checkoutSession = await this.stripeAdapter.getCheckoutSession(sessionId);

      if (checkoutSession) {
        this.logger.log(
          `Checkout session found: subscriptionId=${checkoutSession.subscriptionId}, customerId=${checkoutSession.customerId}, status=${checkoutSession.status}`
        );

        // Always use the subscription ID from the checkout session if available
        // This handles upgrades where a new subscription is created
        if (checkoutSession.subscriptionId) {
          stripeSubscriptionId = checkoutSession.subscriptionId;
        }
        if (checkoutSession.customerId) {
          stripeCustomerId = checkoutSession.customerId;
        }

        // Update subscription with customer ID if we got it from checkout session
        if (stripeCustomerId && !subscription.stripeCustomerId) {
          subscription = subscription.updateStripeCustomerId(stripeCustomerId);
        }
      } else {
        this.logger.warn(`Checkout session ${sessionId} not found`);
      }
    }

    if (!stripeSubscriptionId) {
      this.logger.log(`No Stripe subscription found for organization ${organizationId}`);
      // Return current subscription data without syncing
      return this.getSubscriptionOverview(organizationId);
    }

    // Get fresh data from Stripe
    const stripeData = await this.stripeAdapter.getSubscription(stripeSubscriptionId);

    if (!stripeData) {
      this.logger.warn(`Could not retrieve Stripe subscription ${stripeSubscriptionId}`);
      return this.getSubscriptionOverview(organizationId);
    }

    // Map the price ID to our plan
    const plan = this.stripeAdapter.mapPriceIdToPlan(stripeData.planId);
    let updatedSubscription = subscription;

    if (plan) {
      // Count only non-ADMIN licenses - ADMIN users have unlimited licenses
      const usedLicenses = await this.licenseRepository.countActiveBySubscriptionIdExcludingAdmins(
        subscription.id
      );
      const newPlan = SubscriptionPlan.create(plan);

      // Update plan
      updatedSubscription = updatedSubscription.updatePlan(newPlan, usedLicenses);
      this.logger.log(`Updated plan to ${plan} for organization ${organizationId}`);
    }

    // Update Stripe IDs if not already set
    if (!updatedSubscription.stripeCustomerId && stripeData.customerId) {
      updatedSubscription = updatedSubscription.updateStripeCustomerId(stripeData.customerId);
    }

    // Update Stripe subscription data
    updatedSubscription = updatedSubscription.updateStripeSubscription({
      stripeSubscriptionId: stripeData.subscriptionId,
      currentPeriodStart: stripeData.currentPeriodStart,
      currentPeriodEnd: stripeData.currentPeriodEnd,
      cancelAtPeriodEnd: stripeData.cancelAtPeriodEnd,
    });

    // Update status
    updatedSubscription = updatedSubscription.updateStatus(
      SubscriptionStatus.fromStripeStatus(stripeData.status)
    );

    await this.subscriptionRepository.save(updatedSubscription);

    this.logger.log(
      `Synced subscription for organization ${organizationId} from Stripe (plan: ${updatedSubscription.plan.value})`
    );

    return this.getSubscriptionOverview(organizationId);
  }

  /**
   * Handle Stripe webhook events
   */
  async handleStripeWebhook(payload: string | Buffer, signature: string): Promise<void> {
    const event = await this.stripeAdapter.constructWebhookEvent(payload, signature);

    this.logger.log(`Processing Stripe webhook event: ${event.type}`);

    switch (event.type) {
      case 'checkout.session.completed':
        await this.handleCheckoutCompleted(event.data.object);
        break;

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

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

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

      default:
        this.logger.log(`Unhandled Stripe event type: ${event.type}`);
    }
  }

  /**
   * Allocate a license to a user
   * Note: ADMIN users always get a license (unlimited) and don't count against the quota
   */
  async allocateLicense(userId: string, organizationId: string): Promise<License> {
    const subscription = await this.getOrCreateSubscription(organizationId);

    // Check if user already has a license
    const existingLicense = await this.licenseRepository.findByUserId(userId);
    if (existingLicense?.isActive()) {
      throw new LicenseAlreadyAssignedException(userId);
    }

    // Get the user to check if they're an ADMIN
    const user = await this.userRepository.findById(userId);
    const isAdmin = user?.role === 'ADMIN';

    // ADMIN users have unlimited licenses - skip quota check for them
    if (!isAdmin) {
      // Count only non-ADMIN licenses for quota check
      const usedLicenses = await this.licenseRepository.countActiveBySubscriptionIdExcludingAdmins(
        subscription.id
      );

      if (!subscription.canAllocateLicenses(usedLicenses)) {
        throw new NoLicensesAvailableException(
          organizationId,
          usedLicenses,
          subscription.maxLicenses
        );
      }
    }

    // If there's a revoked license, reactivate it
    if (existingLicense?.isRevoked()) {
      const reactivatedLicense = existingLicense.reactivate();
      return this.licenseRepository.save(reactivatedLicense);
    }

    // Create new license
    const license = License.create({
      id: uuidv4(),
      subscriptionId: subscription.id,
      userId,
    });

    const savedLicense = await this.licenseRepository.save(license);
    this.logger.log(`Allocated license ${savedLicense.id} to user ${userId} (isAdmin: ${isAdmin})`);

    return savedLicense;
  }

  /**
   * Revoke a user's license
   */
  async revokeLicense(userId: string): Promise<void> {
    const license = await this.licenseRepository.findByUserId(userId);
    if (!license) {
      this.logger.warn(`No license found for user ${userId}`);
      return;
    }

    if (license.isRevoked()) {
      this.logger.warn(`License for user ${userId} is already revoked`);
      return;
    }

    const revokedLicense = license.revoke();
    await this.licenseRepository.save(revokedLicense);

    this.logger.log(`Revoked license ${license.id} for user ${userId}`);
  }

  /**
   * Get or create a subscription for an organization
   */
  async getOrCreateSubscription(organizationId: string): Promise<Subscription> {
    let subscription = await this.subscriptionRepository.findByOrganizationId(organizationId);

    if (!subscription) {
      // Create FREE subscription for the organization
      subscription = Subscription.create({
        id: uuidv4(),
        organizationId,
        plan: SubscriptionPlan.bronze(),
      });

      subscription = await this.subscriptionRepository.save(subscription);
      this.logger.log(`Created Bronze subscription for organization ${organizationId}`);
    }

    return subscription;
  }

  // Private helper methods

  private async handleCheckoutCompleted(session: Record<string, unknown>): Promise<void> {
    const metadata = session.metadata as Record<string, string> | undefined;
    const organizationId = metadata?.organizationId;
    const customerId = session.customer as string;
    const subscriptionId = session.subscription as string;

    if (!organizationId || !customerId || !subscriptionId) {
      this.logger.warn('Checkout session missing required metadata');
      return;
    }

    // Get subscription details from Stripe
    const stripeSubscription = await this.stripeAdapter.getSubscription(subscriptionId);
    if (!stripeSubscription) {
      this.logger.error(`Could not retrieve Stripe subscription ${subscriptionId}`);
      return;
    }

    // Get or create our subscription
    let subscription = await this.getOrCreateSubscription(organizationId);

    // Map the price ID to our plan
    const plan = this.stripeAdapter.mapPriceIdToPlan(stripeSubscription.planId);
    if (!plan) {
      this.logger.error(`Unknown Stripe price ID: ${stripeSubscription.planId}`);
      return;
    }

    // Update subscription
    subscription = subscription.updateStripeCustomerId(customerId);
    subscription = subscription.updateStripeSubscription({
      stripeSubscriptionId: subscriptionId,
      currentPeriodStart: stripeSubscription.currentPeriodStart,
      currentPeriodEnd: stripeSubscription.currentPeriodEnd,
      cancelAtPeriodEnd: stripeSubscription.cancelAtPeriodEnd,
    });
    subscription = subscription.updatePlan(
      SubscriptionPlan.create(plan),
      await this.licenseRepository.countActiveBySubscriptionIdExcludingAdmins(subscription.id)
    );
    subscription = subscription.updateStatus(
      SubscriptionStatus.fromStripeStatus(stripeSubscription.status)
    );

    await this.subscriptionRepository.save(subscription);

    // Update organization status badge to match the plan
    await this.updateOrganizationBadge(organizationId, subscription.statusBadge);

    this.logger.log(`Updated subscription for organization ${organizationId} to plan ${plan}`);
  }

  private async handleSubscriptionUpdated(
    stripeSubscription: Record<string, unknown>
  ): Promise<void> {
    const subscriptionId = stripeSubscription.id as string;

    let subscription = await this.subscriptionRepository.findByStripeSubscriptionId(subscriptionId);

    if (!subscription) {
      this.logger.warn(`Subscription ${subscriptionId} not found in database`);
      return;
    }

    // Get fresh data from Stripe
    const stripeData = await this.stripeAdapter.getSubscription(subscriptionId);
    if (!stripeData) {
      this.logger.error(`Could not retrieve Stripe subscription ${subscriptionId}`);
      return;
    }

    // Map the price ID to our plan
    const plan = this.stripeAdapter.mapPriceIdToPlan(stripeData.planId);
    if (plan) {
      // Count only non-ADMIN licenses - ADMIN users have unlimited licenses
      const usedLicenses = await this.licenseRepository.countActiveBySubscriptionIdExcludingAdmins(
        subscription.id
      );
      const newPlan = SubscriptionPlan.create(plan);

      // Only update plan if it can accommodate current non-ADMIN users
      if (newPlan.canAccommodateUsers(usedLicenses)) {
        subscription = subscription.updatePlan(newPlan, usedLicenses);
      } else {
        this.logger.warn(`Cannot update to plan ${plan} - would exceed license limit`);
      }
    }

    subscription = subscription.updateStripeSubscription({
      stripeSubscriptionId: subscriptionId,
      currentPeriodStart: stripeData.currentPeriodStart,
      currentPeriodEnd: stripeData.currentPeriodEnd,
      cancelAtPeriodEnd: stripeData.cancelAtPeriodEnd,
    });
    subscription = subscription.updateStatus(
      SubscriptionStatus.fromStripeStatus(stripeData.status)
    );

    await this.subscriptionRepository.save(subscription);

    // Update organization status badge to match the plan
    if (subscription.organizationId) {
      await this.updateOrganizationBadge(subscription.organizationId, subscription.statusBadge);
    }

    this.logger.log(`Updated subscription ${subscriptionId}`);
  }

  private async handleSubscriptionDeleted(
    stripeSubscription: Record<string, unknown>
  ): Promise<void> {
    const subscriptionId = stripeSubscription.id as string;

    const subscription =
      await this.subscriptionRepository.findByStripeSubscriptionId(subscriptionId);

    if (!subscription) {
      this.logger.warn(`Subscription ${subscriptionId} not found in database`);
      return;
    }

    // Downgrade to FREE plan - count only non-ADMIN licenses
    const canceledSubscription = subscription
      .updatePlan(
        SubscriptionPlan.bronze(),
        await this.licenseRepository.countActiveBySubscriptionIdExcludingAdmins(subscription.id)
      )
      .updateStatus(SubscriptionStatus.canceled());

    await this.subscriptionRepository.save(canceledSubscription);

    // Reset organization badge to 'none' on cancellation
    if (subscription.organizationId) {
      await this.updateOrganizationBadge(subscription.organizationId, 'none');
    }

    this.logger.log(`Subscription ${subscriptionId} canceled, downgraded to Bronze`);
  }

  private async handlePaymentFailed(invoice: Record<string, unknown>): Promise<void> {
    const customerId = invoice.customer as string;

    const subscription = await this.subscriptionRepository.findByStripeCustomerId(customerId);

    if (!subscription) {
      this.logger.warn(`Subscription for customer ${customerId} not found`);
      return;
    }

    const updatedSubscription = subscription.updateStatus(SubscriptionStatus.pastDue());

    await this.subscriptionRepository.save(updatedSubscription);

    this.logger.log(`Subscription ${subscription.id} marked as past due due to payment failure`);
  }

  private mapLicenseToDto(
    license: License,
    user: { email: string; firstName: string; lastName: string; role: string } | null
  ): LicenseResponseDto {
    return {
      id: license.id,
      userId: license.userId,
      userEmail: user?.email || 'Unknown',
      userName: user ? `${user.firstName} ${user.lastName}` : 'Unknown User',
      userRole: user?.role || 'USER',
      status: license.status.value,
      assignedAt: license.assignedAt,
      revokedAt: license.revokedAt || undefined,
    };
  }

  private async updateOrganizationBadge(organizationId: string, badge: string): Promise<void> {
    try {
      const organization = await this.organizationRepository.findById(organizationId);
      if (organization) {
        organization.updateStatusBadge(badge as 'none' | 'silver' | 'gold' | 'platinium');
        await this.organizationRepository.save(organization);
        this.logger.log(`Updated status badge for organization ${organizationId} to ${badge}`);
      }
    } catch (error: any) {
      this.logger.error(`Failed to update organization badge: ${error?.message}`, error?.stack);
    }
  }

  private mapPlanToDto(plan: SubscriptionPlan): PlanDetailsDto {
    return {
      plan: plan.value as SubscriptionPlanDto,
      name: plan.name,
      maxLicenses: plan.maxLicenses,
      monthlyPriceEur: plan.monthlyPriceEur,
      yearlyPriceEur: plan.yearlyPriceEur,
      maxShipmentsPerYear: plan.maxShipmentsPerYear,
      commissionRatePercent: plan.commissionRatePercent,
      supportLevel: plan.supportLevel,
      statusBadge: plan.statusBadge,
      planFeatures: [...plan.planFeatures],
      features: [...plan.features],
    };
  }
}
