import { Inject, InternalServerErrorException } from '@nestjs/common';
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { type Plan } from '@prisma/client';
import { DomainException, NotFoundException, CacheService, CachePrefix, CacheTTL, PrismaService, LoggerService } from '@modules/shared';
import {
  SUBSCRIPTION_REPOSITORY,
  type ISubscriptionRepository,
} from '../../../domain/repositories/subscription.repository';
import { CheckQuotaQuery } from './check-quota.query';

export interface QuotaCheckResult {
  metric: string;
  limit: number | null;
  used: number;
  remaining: number | null;
  allowed: boolean;
}

const METRIC_TO_PLAN_FIELD: Record<string, keyof Plan> = {
  listings_created: 'maxListings',
  searches_saved: 'maxSavedSearches',
  analytics_queries: 'maxAnalyticsQueries',
  media_uploads: 'maxMediaUploads',
  featured_listings_promoted: 'featuredListingsQuota',
};

@QueryHandler(CheckQuotaQuery)
export class CheckQuotaHandler implements IQueryHandler<CheckQuotaQuery> {
  constructor(
    @Inject(SUBSCRIPTION_REPOSITORY)
    private readonly subscriptionRepo: ISubscriptionRepository,
    private readonly prisma: PrismaService,
    private readonly cache: CacheService,
    private readonly logger: LoggerService,
  ) {}

  async execute(query: CheckQuotaQuery): Promise<QuotaCheckResult> {
    try {
      const cacheKey = CacheService.buildKey(CachePrefix.USER_QUOTA, query.userId, query.metric);

      return this.cache.getOrSet(
        cacheKey,
        () => this.loadQuota(query.userId, query.metric),
        CacheTTL.USER_QUOTA,
        'quota',
      );
    } catch (error) {
      if (error instanceof DomainException) throw error;
      this.logger.error(
        `Failed to kiểm tra hạn mức sử dụng: ${error instanceof Error ? error.message : error}`,
        error instanceof Error ? error.stack : undefined,
        this.constructor.name,
      );
      throw new InternalServerErrorException('Không thể kiểm tra hạn mức sử dụng. Vui lòng thử lại sau.');
    }
  }

  private async loadQuota(userId: string, metric: string): Promise<QuotaCheckResult> {
    const subscription = await this.subscriptionRepo.findByUserId(userId);

    // No subscription = FREE tier defaults
    if (!subscription || !subscription.isActive()) {
      const freePlan = await this.prisma.plan.findFirst({
        where: { tier: 'FREE', isActive: true },
      });
      if (!freePlan) {
        return { metric, limit: 0, used: 0, remaining: 0, allowed: false };
      }
      return this.checkAgainstPlan(freePlan, metric, null);
    }

    const plan = await this.prisma.plan.findUnique({
      where: { id: subscription.planId },
    });
    if (!plan) {
      throw new NotFoundException('Plan', subscription.planId);
    }

    return this.checkAgainstPlan(plan, metric, subscription.id, subscription.currentPeriodStart, subscription.currentPeriodEnd);
  }

  private async checkAgainstPlan(
    plan: Plan,
    metric: string,
    subscriptionId: string | null,
    periodStart?: Date,
    periodEnd?: Date,
  ): Promise<QuotaCheckResult> {
    const planField = METRIC_TO_PLAN_FIELD[metric];

    // Unknown metric or unlimited
    if (!planField || plan[planField] === null) {
      return { metric, limit: null, used: 0, remaining: null, allowed: true };
    }

    const limit = plan[planField] as number;

    // Get current period usage (period-scoped to prevent stale reads)
    let used = 0;
    if (subscriptionId && periodStart && periodEnd) {
      const usageRecord = await this.prisma.usageRecord.findUnique({
        where: {
          subscriptionId_metric_periodStart_periodEnd: {
            subscriptionId,
            metric,
            periodStart,
            periodEnd,
          },
        },
      });
      used = usageRecord?.count ?? 0;
    } else if (subscriptionId) {
      // Fallback for free tier (no subscription period)
      const usageRecord = await this.prisma.usageRecord.findFirst({
        where: {
          subscriptionId,
          metric,
        },
        orderBy: { periodStart: 'desc' },
      });
      used = usageRecord?.count ?? 0;
    }

    const remaining = limit - used;

    return {
      metric,
      limit,
      used,
      remaining: Math.max(0, remaining),
      allowed: remaining > 0,
    };
  }
}
