import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import { CreateFundCallDto } from '../dto/create-fund-call.dto';

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

  constructor(private readonly prisma: PrismaService) {}

  /**
   * Generate a fund call:
   * - splits totalAmount by repartitionKey (GENERALE uses tantiemesGeneral of each lot,
   *   SPECIALE uses LotTantieme quotas for the key)
   * - creates one FundCallItem per lot
   * - (notification queuing comes in later phase)
   */
  async create(dto: CreateFundCallDto) {
    const complex = await this.prisma.complex.findUnique({
      where: { id: dto.complexId },
      include: { repartitionKeys: true },
    });
    if (!complex) throw new NotFoundException('Complex not found');

    const fiscalYear = await this.prisma.fiscalYear.findUnique({
      where: { id: dto.fiscalYearId },
    });
    if (!fiscalYear || fiscalYear.complexId !== dto.complexId) {
      throw new BadRequestException('Invalid fiscal year');
    }

    if (!dto.allocations?.length) {
      throw new BadRequestException('At least one allocation required');
    }

    const totalAmount = dto.allocations.reduce((sum, a) => sum + a.amount, 0);

    return this.prisma.$transaction(async (tx) => {
      const fundCall = await tx.fundCall.create({
        data: {
          complexId: dto.complexId,
          fiscalYearId: dto.fiscalYearId,
          label: dto.label,
          period: dto.period,
          totalAmount: new Prisma.Decimal(totalAmount),
          dueDate: new Date(dto.dueDate),
          status: 'ISSUED',
          issuedAt: new Date(),
        },
      });

      const lots = await tx.lot.findMany({
        where: { complexId: dto.complexId },
        include: { tantiemes: true },
      });

      const itemsByLot = new Map<string, number>();

      for (const alloc of dto.allocations) {
        const key = await tx.repartitionKey.findUnique({
          where: { id: alloc.repartitionKeyId },
          include: { scopeSpatialUnit: true },
        });
        if (!key || key.complexId !== dto.complexId) {
          throw new BadRequestException(`Invalid repartition key ${alloc.repartitionKeyId}`);
        }

        // Determine eligible lots for the key
        let eligibleLots = lots;
        if (key.type === 'SPECIALE' && key.scopeSpatialUnitId) {
          // Recursive sub-tree under scope
          const scopeIds = await this.collectSubtreeUnitIds(tx, key.scopeSpatialUnitId);
          eligibleLots = lots.filter((l) => scopeIds.has(l.spatialUnitId));
        }

        // Compute share basis
        let totalQuota = 0;
        const quotas = new Map<string, number>();
        for (const lot of eligibleLots) {
          let q = 0;
          if (key.type === 'GENERALE') {
            q = lot.tantiemesGeneral;
          } else {
            const lt = lot.tantiemes.find((t) => t.repartitionKeyId === key.id);
            q = lt?.quota ?? 0;
          }
          quotas.set(lot.id, q);
          totalQuota += q;
        }

        if (totalQuota === 0) {
          // fallback to equal split
          const perLot = alloc.amount / eligibleLots.length;
          eligibleLots.forEach((l) => {
            itemsByLot.set(l.id, (itemsByLot.get(l.id) ?? 0) + perLot);
          });
        } else {
          for (const lot of eligibleLots) {
            const share = ((quotas.get(lot.id) ?? 0) / totalQuota) * alloc.amount;
            itemsByLot.set(lot.id, (itemsByLot.get(lot.id) ?? 0) + share);
          }
        }
      }

      const items = await Promise.all(
        Array.from(itemsByLot.entries()).map(([lotId, amount]) =>
          tx.fundCallItem.create({
            data: {
              fundCallId: fundCall.id,
              lotId,
              amount: new Prisma.Decimal(Math.round(amount * 100) / 100),
              status: 'DUE',
            },
          }),
        ),
      );

      this.logger.log(`Fund call ${fundCall.id} issued with ${items.length} items`);

      return { ...fundCall, itemsCount: items.length };
    });
  }

  private async collectSubtreeUnitIds(
    tx: Prisma.TransactionClient,
    rootId: string,
  ): Promise<Set<string>> {
    const ids = new Set<string>([rootId]);
    let frontier = [rootId];
    while (frontier.length) {
      const children = await tx.spatialUnit.findMany({
        where: { parentId: { in: frontier } },
        select: { id: true },
      });
      frontier = children.map((c) => c.id).filter((id) => !ids.has(id));
      frontier.forEach((id) => ids.add(id));
    }
    return ids;
  }

  findByComplex(complexId: string) {
    return this.prisma.fundCall.findMany({
      where: { complexId },
      include: { _count: { select: { items: true } } },
      orderBy: { createdAt: 'desc' },
    });
  }

  async findOne(id: string) {
    const fc = await this.prisma.fundCall.findUnique({
      where: { id },
      include: {
        items: { include: { lot: true, payments: true } },
      },
    });
    if (!fc) throw new NotFoundException();
    return fc;
  }

  async getResidentBalance(userId: string) {
    const items = await this.prisma.fundCallItem.findMany({
      where: {
        lot: { residents: { some: { userId, endDate: null } } },
      },
      include: {
        fundCall: true,
        lot: true,
        payments: { where: { status: 'SUCCEEDED' } },
      },
      orderBy: { createdAt: 'desc' },
    });

    const total = items.reduce((s, i) => s + Number(i.amount), 0);
    const paid = items.reduce((s, i) => s + Number(i.paidAmount), 0);
    return { total, paid, balance: total - paid, items };
  }
}
