import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In } from 'typeorm';
import { Coupon, CouponStatus } from '../../domain/entities/coupon.entity';
import { Issuer } from '../../domain/entities/issuer.entity';
import {
  ICouponRepository,
  CouponListFilters,
  CouponAggregateResult,
  CouponStatusCount,
  CouponsByIssuerRow,
  CouponsByCategoryRow,
  CouponSoldAggregateRow,
  DiscountDistributionRow,
  RedemptionRateRow,
  OwnerSummary,
} from '../../domain/repositories/coupon.repository.interface';

@Injectable()
export class CouponRepository implements ICouponRepository {
  constructor(
    @InjectRepository(Coupon)
    private readonly repo: Repository<Coupon>,
    private readonly dataSource: DataSource,
  ) {}

  async findById(id: string): Promise<Coupon | null> {
    return this.repo.findOne({ where: { id } });
  }

  async create(data: Partial<Coupon>): Promise<Coupon> {
    const entity = this.repo.create(data);
    return this.repo.save(entity);
  }

  async save(coupon: Coupon): Promise<Coupon> {
    return this.repo.save(coupon);
  }

  async findAndCount(filters: CouponListFilters): Promise<[Coupon[], number]> {
    const { category, status, search, issuerId, page, limit } = filters;
    const qb = this.repo.createQueryBuilder('c');

    if (category) qb.andWhere('c.category = :category', { category });
    if (status) qb.andWhere('c.status = :status', { status });
    if (issuerId) qb.andWhere('c.issuer_id = :issuerId', { issuerId });
    if (search) {
      qb.andWhere('(c.name ILIKE :search OR c.description ILIKE :search)', {
        search: `%${search}%`,
      });
    }

    qb.orderBy('c.created_at', 'DESC')
      .skip((page - 1) * limit)
      .take(limit);

    return qb.getManyAndCount();
  }

  async findAndCountWithIssuerJoin(filters: CouponListFilters): Promise<[Coupon[], number]> {
    const { category, status, search, issuerId, page, limit } = filters;
    const qb = this.repo.createQueryBuilder('c');

    if (status) qb.andWhere('c.status = :status', { status });
    if (issuerId) qb.andWhere('c.issuer_id = :issuerId', { issuerId });
    if (category) qb.andWhere('c.category = :category', { category });
    if (search) {
      qb.andWhere('(c.name ILIKE :search OR c.description ILIKE :search)', {
        search: `%${search}%`,
      });
    }

    qb.orderBy('c.created_at', 'DESC')
      .skip((page - 1) * limit)
      .take(limit);

    const [coupons, total] = await qb.getManyAndCount();
    await this._attachIssuers(coupons);
    return [coupons, total];
  }

  async findByOwnerWithIssuerJoin(userId: string, filters: CouponListFilters): Promise<[Coupon[], number]> {
    const { status, page, limit } = filters;
    const qb = this.repo.createQueryBuilder('c');

    qb.andWhere('c.owner_user_id = :userId', { userId });
    if (status) qb.andWhere('c.status = :status', { status });

    qb.orderBy('c.created_at', 'DESC')
      .skip((page - 1) * limit)
      .take(limit);

    const [coupons, total] = await qb.getManyAndCount();
    await this._attachIssuers(coupons);
    return [coupons, total];
  }

  private async _attachIssuers(coupons: Coupon[]): Promise<void> {
    if (coupons.length === 0) return;
    const issuerIds = [...new Set(coupons.map((c) => c.issuerId))];
    const issuers = await this.dataSource.getRepository(Issuer).findBy({ id: In(issuerIds) });
    const issuerMap = new Map(issuers.map((i) => [i.id, i]));
    coupons.forEach((c) => { c.issuer = issuerMap.get(c.issuerId); });
  }

  async getOwnerSummary(userId: string): Promise<OwnerSummary> {
    const result = await this.repo
      .createQueryBuilder('c')
      .select([
        'COUNT(c.id) as "count"',
        'COALESCE(SUM(CAST(c.face_value AS numeric)), 0) as "totalFaceValue"',
        'COALESCE(SUM(CAST(c.face_value AS numeric) - COALESCE(CAST(c.current_price AS numeric), CAST(c.face_value AS numeric))), 0) as "totalSaved"',
      ])
      .where('c.owner_user_id = :userId', { userId })
      .getRawOne();

    return {
      count: Number(result.count) || 0,
      totalFaceValue: Number(result.totalFaceValue) || 0,
      totalSaved: Number(result.totalSaved) || 0,
    };
  }

  async updateStatus(id: string, status: string): Promise<Coupon> {
    const coupon = await this.repo.findOne({ where: { id } });
    if (!coupon) throw new NotFoundException('Coupon not found');
    coupon.status = status;
    return this.repo.save(coupon);
  }

  async purchaseWithLock(couponId: string, quantity: number): Promise<Coupon> {
    return this.dataSource.transaction(async (manager) => {
      const coupon = await manager.findOne(Coupon, {
        where: { id: couponId },
        lock: { mode: 'pessimistic_write' },
      });
      if (!coupon) throw new NotFoundException('Coupon not found');
      if (coupon.status !== CouponStatus.LISTED) {
        throw new BadRequestException('Coupon is not available');
      }
      if (coupon.remainingSupply < quantity) {
        throw new BadRequestException('Insufficient supply');
      }
      coupon.remainingSupply -= quantity;
      if (coupon.remainingSupply === 0) coupon.status = CouponStatus.SOLD;
      await manager.save(coupon);
      return coupon;
    });
  }

  async count(where?: Partial<Record<string, any>>): Promise<number> {
    return this.repo.count({ where: where as any });
  }

  // ---- Analytics aggregates ----

  async getAggregateStats(): Promise<CouponAggregateResult> {
    const result = await this.repo
      .createQueryBuilder('c')
      .select([
        'COUNT(c.id) as "totalCoupons"',
        'COALESCE(SUM(c.total_supply), 0) as "totalSupply"',
        'COALESCE(SUM(c.total_supply - c.remaining_supply), 0) as "totalSold"',
        'COALESCE(SUM(c.remaining_supply), 0) as "totalRemaining"',
      ])
      .getRawOne();

    return {
      totalCoupons: Number(result.totalCoupons) || 0,
      totalSupply: Number(result.totalSupply) || 0,
      totalSold: Number(result.totalSold) || 0,
      totalRemaining: Number(result.totalRemaining) || 0,
    };
  }

  async getStatusCounts(): Promise<CouponStatusCount[]> {
    const raw = await this.repo
      .createQueryBuilder('c')
      .select('c.status', 'status')
      .addSelect('COUNT(c.id)', 'count')
      .groupBy('c.status')
      .getRawMany();

    return raw.map((row) => ({
      status: row.status,
      count: Number(row.count),
    }));
  }

  async getCouponsByIssuer(): Promise<CouponsByIssuerRow[]> {
    const raw = await this.repo
      .createQueryBuilder('c')
      .select([
        'c.issuer_id as "issuerId"',
        'COUNT(c.id) as "couponCount"',
        'COALESCE(SUM(c.total_supply), 0) as "totalSupply"',
        'COALESCE(SUM(c.total_supply - c.remaining_supply), 0) as "totalSold"',
        'COALESCE(SUM(CAST(c.face_value AS numeric) * c.total_supply), 0) as "totalFaceValue"',
      ])
      .groupBy('c.issuer_id')
      .orderBy('"couponCount"', 'DESC')
      .getRawMany();

    return raw.map((row) => ({
      issuerId: row.issuerId,
      couponCount: Number(row.couponCount),
      totalSupply: Number(row.totalSupply),
      totalSold: Number(row.totalSold),
      totalFaceValue: Number(row.totalFaceValue),
    }));
  }

  async getCouponsByCategory(): Promise<CouponsByCategoryRow[]> {
    const raw = await this.repo
      .createQueryBuilder('c')
      .select([
        'c.category as "category"',
        'COUNT(c.id) as "couponCount"',
        'COALESCE(SUM(c.total_supply), 0) as "totalSupply"',
        'COALESCE(SUM(c.total_supply - c.remaining_supply), 0) as "totalSold"',
        'COALESCE(AVG(CAST(c.current_price AS numeric)), 0) as "avgPrice"',
      ])
      .groupBy('c.category')
      .orderBy('"couponCount"', 'DESC')
      .getRawMany();

    return raw.map((row) => ({
      category: row.category,
      couponCount: Number(row.couponCount),
      totalSupply: Number(row.totalSupply),
      totalSold: Number(row.totalSold),
      avgPrice: Math.round(Number(row.avgPrice) * 100) / 100,
    }));
  }

  async getTotalSold(): Promise<number> {
    const result = await this.repo
      .createQueryBuilder('c')
      .select('COALESCE(SUM(c.total_supply - c.remaining_supply), 0) as "totalSold"')
      .getRawOne();
    return Number(result.totalSold) || 0;
  }

  async getTotalSoldByStatuses(statuses: CouponStatus[]): Promise<number> {
    const result = await this.repo
      .createQueryBuilder('c')
      .select('COALESCE(SUM(c.total_supply - c.remaining_supply), 0) as "totalRedeemed"')
      .where('c.status IN (:...statuses)', { statuses })
      .getRawOne();
    return Number(result.totalRedeemed) || 0;
  }

  async getRedemptionRateTrend(): Promise<RedemptionRateRow[]> {
    const raw = await this.repo
      .createQueryBuilder('c')
      .select([
        "TO_CHAR(c.created_at, 'YYYY-MM') as \"month\"",
        'COALESCE(SUM(c.total_supply), 0) as "totalIssued"',
        'COALESCE(SUM(c.total_supply - c.remaining_supply), 0) as "totalSold"',
      ])
      .groupBy("TO_CHAR(c.created_at, 'YYYY-MM')")
      .orderBy('"month"', 'ASC')
      .getRawMany();

    return raw.map((row) => ({
      month: row.month,
      totalIssued: Number(row.totalIssued) || 0,
      totalSold: Number(row.totalSold) || 0,
    }));
  }

  async getDiscountDistribution(): Promise<DiscountDistributionRow[]> {
    const raw = await this.repo
      .createQueryBuilder('c')
      .select([
        `CASE
          WHEN CAST(c.face_value AS numeric) = 0 THEN 'N/A'
          WHEN (1 - CAST(c.current_price AS numeric) / CAST(c.face_value AS numeric)) * 100 < 0 THEN 'premium'
          WHEN (1 - CAST(c.current_price AS numeric) / CAST(c.face_value AS numeric)) * 100 < 10 THEN '0-10%'
          WHEN (1 - CAST(c.current_price AS numeric) / CAST(c.face_value AS numeric)) * 100 < 20 THEN '10-20%'
          WHEN (1 - CAST(c.current_price AS numeric) / CAST(c.face_value AS numeric)) * 100 < 30 THEN '20-30%'
          WHEN (1 - CAST(c.current_price AS numeric) / CAST(c.face_value AS numeric)) * 100 < 50 THEN '30-50%'
          ELSE '50%+'
        END as "range"`,
        'COUNT(c.id) as "count"',
        `COALESCE(AVG(
          CASE WHEN CAST(c.face_value AS numeric) > 0
            THEN (1 - CAST(c.current_price AS numeric) / CAST(c.face_value AS numeric)) * 100
            ELSE 0
          END
        ), 0) as "avgDiscount"`,
      ])
      .groupBy('"range"')
      .orderBy('"range"', 'ASC')
      .getRawMany();

    return raw.map((row) => ({
      range: row.range,
      count: Number(row.count),
      avgDiscount: Math.round(Number(row.avgDiscount) * 100) / 100,
    }));
  }

  // ---- Merchant analytics ----

  async getCouponSupplyAggregate(): Promise<{ totalCouponsIssued: number; totalCouponsSold: number }> {
    const result = await this.repo
      .createQueryBuilder('c')
      .select([
        'COALESCE(SUM(c.total_supply), 0) as "totalCouponsIssued"',
        'COALESCE(SUM(c.total_supply - c.remaining_supply), 0) as "totalCouponsSold"',
      ])
      .getRawOne();

    return {
      totalCouponsIssued: Number(result.totalCouponsIssued) || 0,
      totalCouponsSold: Number(result.totalCouponsSold) || 0,
    };
  }

  async getSoldPerIssuer(issuerIds: string[]): Promise<CouponSoldAggregateRow[]> {
    if (issuerIds.length === 0) return [];

    const raw = await this.repo
      .createQueryBuilder('c')
      .select([
        'c.issuer_id as "issuerId"',
        'COALESCE(SUM(c.total_supply - c.remaining_supply), 0) as "totalSold"',
      ])
      .where('c.issuer_id IN (:...ids)', { ids: issuerIds })
      .groupBy('c.issuer_id')
      .getRawMany();

    return raw.map((r) => ({
      issuerId: r.issuerId,
      totalSold: Number(r.totalSold),
    }));
  }

  async getRecentlySoldCoupons(limit: number): Promise<Coupon[]> {
    return this.repo
      .createQueryBuilder('c')
      .where('c.total_supply > c.remaining_supply')
      .orderBy('c.updated_at', 'DESC')
      .take(limit)
      .getMany();
  }
}
