import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Prisma, RentalStatus } from "@prisma/client";
import { PrismaService } from "../../infra/prisma/prisma.service";
import { CreateRentalDto } from "./dto/create-rental.dto";
import { RentalQueryDto } from "./dto/rental-query.dto";
import { UpdateRentalDto } from "./dto/update-rental.dto";

@Injectable()
export class RentalsService {
  constructor(private readonly prisma: PrismaService) {}

  private calculateDays(startDateStr: string, endDateStr: string): number {
    const start = new Date(startDateStr);
    const end = new Date(endDateStr);
    const diffTime = Math.abs(end.getTime() - start.getTime());
    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
    return Math.max(diffDays, 1);
  }

  /**
   * Tarifador Inteligente Degressivo:
   * Calcula o valor do item considerando Diária, Semanal e Mensal (Tabela do Equipamento).
   */
  private calculateItemTotal(
    days: number,
    quantity: number,
    inputDailyRate: number,
    eq?: { dailyRate: Prisma.Decimal; weeklyRate: Prisma.Decimal; monthlyRate: Prisma.Decimal } | null,
  ): number {
    const daily = eq ? Number(eq.dailyRate) : inputDailyRate;
    const weekly = eq ? Number(eq.weeklyRate) : 0;
    const monthly = eq ? Number(eq.monthlyRate) : 0;

    // Se a diária enviada foi customizada no form pelo usuário e difere do cadastro
    if (inputDailyRate > 0 && eq && Math.abs(inputDailyRate - daily) > 0.01) {
      return quantity * inputDailyRate * days;
    }

    if (days <= 0) return 0;

    // 1. Regra Mensal (>= 30 dias)
    if (days >= 30 && monthly > 0) {
      const fullMonths = Math.floor(days / 30);
      const remDays = days % 30;

      let remTotal = 0;
      if (remDays > 0) {
        if (weekly > 0 && remDays >= 7) {
          const remWeeks = Math.floor(remDays / 7);
          const remWeekDays = remDays % 7;
          remTotal = Math.min(remDays * daily, remWeeks * weekly + remWeekDays * daily, monthly);
        } else {
          remTotal = Math.min(remDays * daily, monthly);
        }
      }
      return quantity * (fullMonths * monthly + remTotal);
    }

    // 2. Regra Semanal (>= 7 dias e < 30 dias)
    if (days >= 7 && weekly > 0) {
      const fullWeeks = Math.floor(days / 7);
      const remDays = days % 7;
      const calculatedWeeklyTotal = fullWeeks * weekly + remDays * daily;
      const pureDailyTotal = days * daily;
      const capMonthly = monthly > 0 ? monthly : Infinity;

      return quantity * Math.min(pureDailyTotal, calculatedWeeklyTotal, capMonthly);
    }

    // 3. Regra por Diária simples (< 7 dias)
    const pureDailyTotal = days * daily;
    const capWeekly = weekly > 0 ? weekly : Infinity;
    const capMonthly = monthly > 0 ? monthly : Infinity;

    return quantity * Math.min(pureDailyTotal, capWeekly, capMonthly);
  }

  private async generateFinancialEntries(
    companyId: string,
    rental: {
      id: string;
      code: string;
      customerId: string;
      startDate: Date;
      total: number;
    },
    depositAmount: number,
    installmentsCount: number = 1,
    customInstallments?: { dueDate: string; amount: number }[],
  ) {
    if (depositAmount > 0) {
      const existingDeposit = await this.prisma.accountReceivable.findFirst({
        where: { rentalId: rental.id, description: { contains: "Sinal de Depósito" } },
      });

      if (!existingDeposit) {
        await this.prisma.accountReceivable.create({
          data: {
            companyId,
            customerId: rental.customerId,
            rentalId: rental.id,
            description: `Sinal de Depósito - ${rental.code}`,
            amount: depositAmount,
            dueDate: new Date(),
            paidAt: new Date(),
            status: "PAID",
          },
        });

        await this.prisma.cashMovement.create({
          data: {
            companyId,
            type: "INCOME",
            description: `Recebimento de Sinal de Depósito - ${rental.code}`,
            amount: depositAmount,
            occurredAt: new Date(),
            category: "Locação",
          },
        });
      }
    }

    const remainingTotal = Math.max(rental.total - depositAmount, 0);
    if (remainingTotal > 0) {
      await this.prisma.accountReceivable.deleteMany({
        where: {
          rentalId: rental.id,
          status: "OPEN",
          NOT: { description: { contains: "Sinal de Depósito" } },
        },
      });

      if (customInstallments && customInstallments.length > 0) {
        const count = customInstallments.length;
        for (let i = 0; i < count; i++) {
          const inst = customInstallments[i];
          const dueDatePart = inst.dueDate ? inst.dueDate.split("T")[0] : "";
          let dueDate: Date;
          if (/^\d{4}-\d{2}-\d{2}$/.test(dueDatePart)) {
            const [yr, mo, dy] = dueDatePart.split("-").map(Number);
            dueDate = new Date(Date.UTC(yr, mo - 1, dy));
          } else {
            dueDate = new Date(rental.startDate);
            dueDate.setDate(dueDate.getDate() + i * 30);
          }

          await this.prisma.accountReceivable.create({
            data: {
              companyId,
              customerId: rental.customerId,
              rentalId: rental.id,
              description: `Parcela ${i + 1}/${count} - Contrato ${rental.code}`,
              amount: Number(inst.amount) || 0,
              dueDate,
              status: "OPEN",
            },
          });
        }
      } else {
        const count = Math.max(installmentsCount, 1);
        const installmentValue = Number((remainingTotal / count).toFixed(2));

        for (let i = 0; i < count; i++) {
          const dueDate = new Date(rental.startDate);
          dueDate.setDate(dueDate.getDate() + i * 30);

          await this.prisma.accountReceivable.create({
            data: {
              companyId,
              customerId: rental.customerId,
              rentalId: rental.id,
              description: `Parcela ${i + 1}/${count} - Contrato ${rental.code}`,
              amount: i === count - 1 ? remainingTotal - installmentValue * (count - 1) : installmentValue,
              dueDate,
              status: "OPEN",
            },
          });
        }
      }
    }
  }

  async findAll(companyId: string, query: RentalQueryDto) {
    const { search, status, customerId, page = 1, limit = 20 } = query;
    const skip = (page - 1) * limit;

    const where: Prisma.RentalWhereInput = {
      companyId,
      ...(status ? { status } : {}),
      ...(customerId ? { customerId } : {}),
      ...(search
        ? {
            OR: [
              { code: { contains: search, mode: "insensitive" } },
              { customer: { name: { contains: search, mode: "insensitive" } } },
              { customer: { document: { contains: search, mode: "insensitive" } } },
            ],
          }
        : {}),
    };

    const [items, total] = await Promise.all([
      this.prisma.rental.findMany({
        where,
        orderBy: { createdAt: "desc" },
        skip,
        take: limit,
        include: {
          customer: { select: { id: true, name: true, document: true, phone: true } },
          items: {
            include: {
              equipment: { select: { id: true, code: true, assetTag: true, brand: true, model: true, dailyRate: true, weeklyRate: true, monthlyRate: true } },
            },
          },
          receivables: true,
        },
      }),
      this.prisma.rental.count({ where }),
    ]);

    const mappedItems = await Promise.all(
      items.map((r: any) => this.mapRentalWithDetails(r))
    );

    return {
      items: mappedItems,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit) || 1,
    };
  }

  private async mapRentalWithDetails(rental: any) {
    if (!rental) return rental;
    let receivables = rental.receivables || [];
    const depositEntry = receivables.find((r: any) =>
      r.description?.includes("Sinal de Depósito") || r.description?.includes("Sinal / Entrada")
    );
    const depositAmount = Number(rental.depositAmount) || (depositEntry ? Number(depositEntry.amount) : 0);

    const openReceivables = receivables.filter((r: any) =>
      !r.description?.includes("Sinal de Depósito") && !r.description?.includes("Sinal / Entrada")
    );

    const installmentsCount = openReceivables.length > 0
      ? openReceivables.length
      : (Number(rental.installmentsCount) || 1);

    let paymentMethod = rental.paymentMethod;
    if (!paymentMethod || paymentMethod === "A_VISTA") {
      if (depositAmount > 0 && installmentsCount > 0) {
        paymentMethod = "ENTRADA_E_PARCELAS";
      } else if (installmentsCount > 1) {
        paymentMethod = "PARCELADO";
      } else {
        paymentMethod = "A_VISTA";
      }
    }

    if (rental.status !== RentalStatus.QUOTE && receivables.length === 0 && (depositAmount > 0 || installmentsCount > 1)) {
      try {
        await this.generateFinancialEntries(
          rental.companyId,
          {
            id: rental.id,
            code: rental.code,
            customerId: rental.customerId,
            startDate: rental.startDate,
            total: Number(rental.total),
          },
          depositAmount,
          installmentsCount,
        );
        receivables = await this.prisma.accountReceivable.findMany({
          where: { rentalId: rental.id },
          orderBy: { dueDate: "asc" },
        });
      } catch (err) {
        // fallback to projected
      }
    }

    if (receivables.length === 0 && (depositAmount > 0 || installmentsCount > 1)) {
      const remainingTotal = Math.max(Number(rental.total) - depositAmount, 0);
      const count = Math.max(installmentsCount, 1);
      const installmentValue = Number((remainingTotal / count).toFixed(2));
      const projected: any[] = [];

      if (depositAmount > 0) {
        projected.push({
          id: `proj-dep-${rental.id}`,
          description: rental.status === "QUOTE" ? `Sinal / Entrada (Proposta)` : `Sinal de Depósito - ${rental.code}`,
          amount: depositAmount,
          dueDate: rental.startDate ? new Date(rental.startDate).toISOString() : new Date().toISOString(),
          status: rental.status === "QUOTE" ? "OPEN" : "PAID",
          paidAt: rental.status === "QUOTE" ? null : new Date().toISOString(),
        });
      }

      if (remainingTotal > 0) {
        for (let i = 0; i < count; i++) {
          const dueDate = rental.startDate ? new Date(rental.startDate) : new Date();
          dueDate.setDate(dueDate.getDate() + i * 30);
          projected.push({
            id: `proj-inst-${rental.id}-${i}`,
            description: rental.status === "QUOTE"
              ? `Parcela ${i + 1}/${count} - Contrato ${rental.code} (Proposta)`
              : `Parcela ${i + 1}/${count} - Contrato ${rental.code}`,
            amount: i === count - 1 ? remainingTotal - installmentValue * (count - 1) : installmentValue,
            dueDate: dueDate.toISOString(),
            status: "OPEN",
          });
        }
      }
      receivables = projected;
    }

    return {
      ...rental,
      depositAmount,
      paymentMethod,
      installmentsCount,
      receivables,
    };
  }

  async getStats(companyId: string) {
    const [total, inProgress, quote, finished, reserved, canceled, totalRevenueResult] = await Promise.all([
      this.prisma.rental.count({ where: { companyId } }),
      this.prisma.rental.count({ where: { companyId, status: RentalStatus.IN_PROGRESS } }),
      this.prisma.rental.count({ where: { companyId, status: RentalStatus.QUOTE } }),
      this.prisma.rental.count({ where: { companyId, status: RentalStatus.FINISHED } }),
      this.prisma.rental.count({ where: { companyId, status: RentalStatus.RESERVED } }),
      this.prisma.rental.count({ where: { companyId, status: RentalStatus.CANCELED } }),
      this.prisma.rental.aggregate({
        where: { companyId, status: { in: [RentalStatus.IN_PROGRESS, RentalStatus.FINISHED] } },
        _sum: { total: true },
      }),
    ]);

    return {
      total,
      inProgress,
      quote,
      finished,
      reserved,
      canceled,
      totalRevenue: Number(totalRevenueResult._sum.total || 0),
    };
  }

  async findOne(companyId: string, id: string) {
    const rental = await this.prisma.rental.findFirst({
      where: { id, companyId },
      include: {
        customer: true,
        items: {
          include: {
            equipment: true,
          },
        },
        receivables: {
          orderBy: { dueDate: "asc" },
        },
      },
    });

    if (!rental) {
      throw new NotFoundException("Contrato de locação não encontrado.");
    }

    return await this.mapRentalWithDetails(rental);
  }

  async create(companyId: string, dto: CreateRentalDto) {
    const days = this.calculateDays(dto.startDate, dto.endDate);

    const eqIds = dto.items.map((i) => i.equipmentId);
    const equipments = await this.prisma.equipment.findMany({
      where: { id: { in: eqIds }, companyId },
    });

    let subtotal = 0;
    const itemsData = dto.items.map((item) => {
      const eq = equipments.find((e) => e.id === item.equipmentId);
      const itemTotal = this.calculateItemTotal(days, item.quantity, item.dailyRate, eq);
      subtotal += itemTotal;
      return {
        equipmentId: item.equipmentId,
        quantity: item.quantity,
        dailyRate: item.dailyRate,
        total: itemTotal,
      };
    });

    const freight = dto.freight || 0;
    const discount = dto.discount || 0;
    const depositAmount = dto.depositAmount || 0;
    const total = Math.max(subtotal + freight - discount, 0);

    const count = await this.prisma.rental.count({ where: { companyId } });
    const prefix = dto.status === RentalStatus.QUOTE ? "ORC" : "LOC";
    const year = new Date().getFullYear();
    const code = `${prefix}-${year}-${String(count + 1).padStart(4, "0")}`;

    const rental = await this.prisma.rental.create({
      data: {
        companyId,
        customerId: dto.customerId,
        code,
        startDate: new Date(dto.startDate),
        endDate: new Date(dto.endDate),
        subtotal,
        freight,
        discount,
        depositAmount,
        paymentMethod: dto.paymentMethod || "A_VISTA",
        installmentsCount: dto.installmentsCount || 1,
        total,
        operator: dto.operator || null,
        notes: dto.notes || null,
        status: dto.status || RentalStatus.QUOTE,
        items: {
          createMany: {
            data: itemsData,
          },
        },
      },
      include: {
        customer: true,
        items: { include: { equipment: true } },
        receivables: true,
      },
    });

    if (rental.status === RentalStatus.QUOTE) {
      await this.prisma.accountReceivable.deleteMany({ where: { rentalId: rental.id } });
      await this.prisma.cashMovement.deleteMany({ where: { companyId, description: { contains: rental.code } } });
    } else {
      await this.generateFinancialEntries(
        companyId,
        {
          id: rental.id,
          code: rental.code,
          customerId: rental.customerId,
          startDate: rental.startDate,
          total: Number(rental.total),
        },
        depositAmount,
        dto.installmentsCount || 1,
        dto.installments,
      );
    }

    return this.findOne(companyId, rental.id);
  }

  async update(companyId: string, id: string, dto: UpdateRentalDto) {
    const currentRental = await this.findOne(companyId, id);

    const startDate = dto.startDate ? new Date(dto.startDate) : new Date(currentRental.startDate);
    const endDate = dto.endDate ? new Date(dto.endDate) : new Date(currentRental.endDate);
    const days = this.calculateDays(startDate.toISOString(), endDate.toISOString());

    let subtotal = Number(currentRental.subtotal);
    let itemsUpdateOp = undefined;

    if (dto.items && Array.isArray(dto.items)) {
      const eqIds = dto.items.map((i) => i.equipmentId);
      const equipments = await this.prisma.equipment.findMany({
        where: { id: { in: eqIds }, companyId },
      });

      subtotal = 0;
      const itemsData = dto.items.map((item) => {
        const eq = equipments.find((e) => e.id === item.equipmentId);
        const itemTotal = this.calculateItemTotal(days, item.quantity, item.dailyRate, eq);
        subtotal += itemTotal;
        return {
          equipmentId: item.equipmentId,
          quantity: item.quantity,
          dailyRate: item.dailyRate,
          total: itemTotal,
        };
      });

      await this.prisma.rentalItem.deleteMany({ where: { rentalId: id } });
      itemsUpdateOp = {
        createMany: { data: itemsData },
      };
    }

    const freight = dto.freight !== undefined ? dto.freight : Number(currentRental.freight);
    const discount = dto.discount !== undefined ? dto.discount : Number(currentRental.discount);
    const depositAmount = dto.depositAmount !== undefined ? dto.depositAmount : Number(currentRental.depositAmount);
    const total = Math.max(subtotal + freight - discount, 0);

    const rental = await this.prisma.rental.update({
      where: { id },
      data: {
        ...(dto.customerId ? { customerId: dto.customerId } : {}),
        startDate,
        endDate,
        subtotal,
        freight,
        discount,
        depositAmount,
        ...(dto.paymentMethod ? { paymentMethod: dto.paymentMethod } : {}),
        ...(dto.installmentsCount ? { installmentsCount: dto.installmentsCount } : {}),
        total,
        ...(dto.operator !== undefined ? { operator: dto.operator || null } : {}),
        ...(dto.notes !== undefined ? { notes: dto.notes || null } : {}),
        ...(dto.status ? { status: dto.status } : {}),
        ...(itemsUpdateOp ? { items: itemsUpdateOp } : {}),
      },
      include: {
        customer: true,
        items: { include: { equipment: true } },
        receivables: true,
      },
    });

    if (rental.status === RentalStatus.QUOTE) {
      await this.prisma.accountReceivable.deleteMany({ where: { rentalId: rental.id } });
      await this.prisma.cashMovement.deleteMany({ where: { companyId, description: { contains: rental.code } } });
    } else {
      await this.generateFinancialEntries(
        companyId,
        {
          id: rental.id,
          code: rental.code,
          customerId: rental.customerId,
          startDate: rental.startDate,
          total: Number(rental.total),
        },
        depositAmount,
        rental.installmentsCount || dto.installmentsCount || 1,
        dto.installments,
      );
    }

    return this.findOne(companyId, rental.id);
  }

  async rebuildFinancials(companyId: string, id: string) {
    const rental = await this.prisma.rental.findFirst({
      where: { id, companyId },
    });
    if (!rental) throw new NotFoundException("Locação não encontrada.");
    if (rental.status === RentalStatus.QUOTE) {
      return { message: "Orçamentos não geram lançamentos financeiros." };
    }

    const depositAmount = Number(rental.depositAmount) || 0;
    const installmentsCount = Number(rental.installmentsCount) || 1;

    // Delete all existing receivables and cash movements for this rental
    await this.prisma.accountReceivable.deleteMany({ where: { rentalId: id } });
    await this.prisma.cashMovement.deleteMany({
      where: { companyId, description: { contains: rental.code } },
    });

    await this.generateFinancialEntries(
      companyId,
      {
        id: rental.id,
        code: rental.code,
        customerId: rental.customerId,
        startDate: rental.startDate,
        total: Number(rental.total),
      },
      depositAmount,
      installmentsCount,
    );

    const receivables = await this.prisma.accountReceivable.findMany({
      where: { rentalId: id },
      orderBy: { dueDate: "asc" },
    });

    return { message: "Lançamentos financeiros recriados com sucesso.", receivables };
  }

  async rebuildAllFinancials(companyId: string) {
    const rentals = await this.prisma.rental.findMany({
      where: { companyId, status: { not: RentalStatus.QUOTE } },
    });

    const results: any[] = [];

    for (const rental of rentals) {
      const depositAmount = Number(rental.depositAmount) || 0;
      const installmentsCount = Number(rental.installmentsCount) || 1;

      if (depositAmount === 0 && installmentsCount <= 1) continue;

      // Check if existing receivables have wrong amounts
      const existingRecs = await this.prisma.accountReceivable.findMany({
        where: { rentalId: rental.id },
      });
      const existingTotal = existingRecs.reduce((sum, r) => sum + Number(r.amount), 0);
      const expectedTotal = Number(rental.total);

      // Rebuild if no receivables or total is off by more than 10%
      if (existingRecs.length === 0 || Math.abs(existingTotal - expectedTotal) > expectedTotal * 0.1) {
        await this.prisma.accountReceivable.deleteMany({ where: { rentalId: rental.id } });
        await this.prisma.cashMovement.deleteMany({
          where: { companyId, description: { contains: rental.code } },
        });

        await this.generateFinancialEntries(
          companyId,
          {
            id: rental.id,
            code: rental.code,
            customerId: rental.customerId,
            startDate: rental.startDate,
            total: Number(rental.total),
          },
          depositAmount,
          installmentsCount,
        );

        results.push({ code: rental.code, rebuilt: true, depositAmount, installmentsCount });
      } else {
        results.push({ code: rental.code, rebuilt: false, existingTotal });
      }
    }

    return { message: "Rebuild completo.", results };
  }

  async remove(companyId: string, id: string) {
    const rental = await this.findOne(companyId, id);

    // Delete associated financial receivables (AccountReceivable) for this contract
    await this.prisma.accountReceivable.deleteMany({
      where: { rentalId: id },
    });

    // Delete cash movements related to this contract (e.g., signal deposit)
    if (rental.code) {
      await this.prisma.cashMovement.deleteMany({
        where: {
          companyId,
          description: { contains: rental.code },
        },
      });
    }

    return this.prisma.rental.delete({
      where: { id },
    });
  }
}
