import { Injectable, NotFoundException } from "@nestjs/common";
import { CashMovementType, FinancialStatus } from "@prisma/client";
import { PrismaService } from "../../infra/prisma/prisma.service";
import { CreatePayableDto } from "./dto/create-payable.dto";
import { CreateReceivableDto } from "./dto/create-receivable.dto";
import { FinanceQueryDto } from "./dto/finance-query.dto";
import { PayEntryDto } from "./dto/pay-entry.dto";

/**
 * Parses a date string safely, treating date-only strings (YYYY-MM-DD)
 * as local noon to avoid UTC midnight timezone shift.
 * e.g. "2026-07-30" → 2026-07-30T12:00:00 (local) instead of 2026-07-30T00:00:00Z (UTC)
 */
function parseLocalDate(dateStr: string): Date {
  // If it already contains time info (e.g. ISO with 'T'), parse as-is
  if (dateStr.includes("T")) return new Date(dateStr);
  // Date-only: treat as local noon to avoid UTC midnight offset issues
  return new Date(`${dateStr}T12:00:00`);
}

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

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

    // Auto-correct OPEN/OVERDUE status based on dueDate before querying
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    await Promise.all([
      this.prisma.accountReceivable.updateMany({
        where: { companyId, status: FinancialStatus.OPEN, dueDate: { lt: today } },
        data: { status: FinancialStatus.OVERDUE },
      }),
      this.prisma.accountReceivable.updateMany({
        where: { companyId, status: FinancialStatus.OVERDUE, dueDate: { gte: today } },
        data: { status: FinancialStatus.OPEN },
      }),
    ]);

    const where: any = {
      companyId,
      ...(status ? { status } : {}),
      ...(search
        ? {
            OR: [
              { description: { contains: search, mode: "insensitive" } },
              { customer: { name: { contains: search, mode: "insensitive" } } },
            ],
          }
        : {}),
    };

    const [items, total] = await Promise.all([
      this.prisma.accountReceivable.findMany({
        where,
        orderBy: { dueDate: "asc" },
        skip,
        take: limit,
        include: { customer: true, rental: true },
      }),
      this.prisma.accountReceivable.count({ where }),
    ]);

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

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

    // Auto-correct OPEN/OVERDUE status based on dueDate before querying
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    await Promise.all([
      this.prisma.accountPayable.updateMany({
        where: { companyId, status: FinancialStatus.OPEN, dueDate: { lt: today } },
        data: { status: FinancialStatus.OVERDUE },
      }),
      this.prisma.accountPayable.updateMany({
        where: { companyId, status: FinancialStatus.OVERDUE, dueDate: { gte: today } },
        data: { status: FinancialStatus.OPEN },
      }),
    ]);

    const where: any = {
      companyId,
      ...(status ? { status } : {}),
      ...(search
        ? {
            OR: [
              { description: { contains: search, mode: "insensitive" } },
              { supplier: { name: { contains: search, mode: "insensitive" } } },
              { supplier: { tradeName: { contains: search, mode: "insensitive" } } },
            ],
          }
        : {}),
    };

    const [items, total] = await Promise.all([
      this.prisma.accountPayable.findMany({
        where,
        orderBy: { dueDate: "asc" },
        skip,
        take: limit,
        include: { supplier: true },
      }),
      this.prisma.accountPayable.count({ where }),
    ]);

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

  async getCashFlow(companyId: string, query: FinanceQueryDto) {
    const { search, page = 1, limit = 20 } = query;
    const skip = (page - 1) * limit;

    const where: any = {
      companyId,
      ...(search
        ? {
            OR: [
              { description: { contains: search, mode: "insensitive" } },
              { category: { contains: search, mode: "insensitive" } },
            ],
          }
        : {}),
    };

    const [items, total] = await Promise.all([
      this.prisma.cashMovement.findMany({
        where,
        orderBy: { occurredAt: "desc" },
        skip,
        take: limit,
      }),
      this.prisma.cashMovement.count({ where }),
    ]);

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

  async getStats(companyId: string) {
    const [receivables, payables] = await Promise.all([
      this.prisma.accountReceivable.findMany({ where: { companyId } }),
      this.prisma.accountPayable.findMany({ where: { companyId } }),
    ]);

    const totalReceivable = receivables
      .filter((r) => r.status === FinancialStatus.OPEN || r.status === FinancialStatus.OVERDUE)
      .reduce((acc, r) => acc + Number(r.amount), 0);

    const totalPayable = payables
      .filter((p) => p.status === FinancialStatus.OPEN || p.status === FinancialStatus.OVERDUE)
      .reduce((acc, p) => acc + Number(p.amount), 0);

    const overdueAmount = receivables
      .filter((r) => r.status === FinancialStatus.OVERDUE)
      .reduce((acc, r) => acc + Number(r.amount), 0);

    const netProjected = totalReceivable - totalPayable;

    return {
      totalReceivable,
      totalPayable,
      netProjected,
      overdueAmount,
    };
  }

  async createReceivable(companyId: string, dto: CreateReceivableDto) {
    return this.prisma.accountReceivable.create({
      data: {
        companyId,
        customerId: dto.customerId,
        rentalId: dto.rentalId,
        description: dto.description,
        amount: dto.amount,
        dueDate: parseLocalDate(dto.dueDate),
        status: dto.status || FinancialStatus.OPEN,
      },
      include: { customer: true },
    });
  }

  async createPayable(companyId: string, dto: CreatePayableDto) {
    return this.prisma.accountPayable.create({
      data: {
        companyId,
        supplierId: dto.supplierId,
        description: dto.description,
        amount: dto.amount,
        dueDate: parseLocalDate(dto.dueDate),
        status: dto.status || FinancialStatus.OPEN,
      },
      include: { supplier: true },
    });
  }

  async payReceivable(companyId: string, id: string, dto: PayEntryDto) {
    const rec = await this.prisma.accountReceivable.findFirst({
      where: { id, companyId },
    });

    if (!rec) throw new NotFoundException("Conta a receber não encontrada.");

    const amountPaid = dto.amountPaid ? Number(dto.amountPaid) : Number(rec.amount);
    const paidAtDate = dto.paidAt ? parseLocalDate(dto.paidAt) : new Date();
    const isPartial = amountPaid < Number(rec.amount);

    let updated;
    if (isPartial) {
      const remainingAmount = Number(rec.amount) - amountPaid;
      const newDueDate = dto.newDueDate ? parseLocalDate(dto.newDueDate) : rec.dueDate;

      // Recalculate status based on the new due date
      const now = new Date();
      now.setHours(0, 0, 0, 0);
      const dueDateOnly = new Date(newDueDate);
      dueDateOnly.setHours(0, 0, 0, 0);
      const newStatus = dueDateOnly >= now ? FinancialStatus.OPEN : FinancialStatus.OVERDUE;

      // Strip any previous "(Saldo Parcial)" suffix to get the base description
      const baseDescription = rec.description.replace(/ \(Saldo Parcial\)$/, "");

      // Update original record with the remaining balance and new due date
      updated = await this.prisma.accountReceivable.update({
        where: { id },
        data: {
          amount: remainingAmount,
          dueDate: newDueDate,
          status: newStatus,
          description: `${baseDescription} (Saldo Parcial)`,
        },
      });

      // Create a new PAID record for the amount that was actually received
      await this.prisma.accountReceivable.create({
        data: {
          companyId,
          customerId: rec.customerId,
          rentalId: rec.rentalId,
          description: `${baseDescription} (Baixa Parcial)`,
          amount: amountPaid,
          dueDate: paidAtDate,
          status: FinancialStatus.PAID,
          paidAt: paidAtDate,
        },
      });

      // Register in cash flow with exact timestamp of when payment was processed
      await this.prisma.cashMovement.create({
        data: {
          companyId,
          type: CashMovementType.INCOME,
          description: `Recebimento Parcial: ${baseDescription}`,
          amount: amountPaid,
          occurredAt: new Date(),
          category: "Locação",
        },
      });
    } else {
      updated = await this.prisma.accountReceivable.update({
        where: { id },
        data: {
          status: FinancialStatus.PAID,
          paidAt: paidAtDate,
        },
      });

      // Register in cash flow with exact timestamp of when payment was processed
      await this.prisma.cashMovement.create({
        data: {
          companyId,
          type: CashMovementType.INCOME,
          description: `Recebimento: ${rec.description}`,
          amount: rec.amount,
          occurredAt: new Date(),
          category: "Locação",
        },
      });
    }

    return updated;
  }

  async payPayable(companyId: string, id: string, dto: PayEntryDto) {
    const pay = await this.prisma.accountPayable.findFirst({
      where: { id, companyId },
    });

    if (!pay) throw new NotFoundException("Conta a pagar não encontrada.");

    const amountPaid = dto.amountPaid ? Number(dto.amountPaid) : Number(pay.amount);
    const paidAtDate = dto.paidAt ? parseLocalDate(dto.paidAt) : new Date();
    const isPartial = amountPaid < Number(pay.amount);

    let updated;
    if (isPartial) {
      const remainingAmount = Number(pay.amount) - amountPaid;
      const newDueDate = dto.newDueDate ? parseLocalDate(dto.newDueDate) : pay.dueDate;

      // Recalculate status based on the new due date
      const now = new Date();
      now.setHours(0, 0, 0, 0);
      const dueDateOnly = new Date(newDueDate);
      dueDateOnly.setHours(0, 0, 0, 0);
      const newStatus = dueDateOnly >= now ? FinancialStatus.OPEN : FinancialStatus.OVERDUE;

      // Strip any previous "(Saldo Parcial)" suffix to get the base description
      const baseDescription = pay.description.replace(/ \(Saldo Parcial\)$/, "");

      // Update original record with the remaining balance and new due date
      updated = await this.prisma.accountPayable.update({
        where: { id },
        data: {
          amount: remainingAmount,
          dueDate: newDueDate,
          status: newStatus,
          description: `${baseDescription} (Saldo Parcial)`,
        },
      });

      // Create a new PAID record for the amount that was actually paid
      await this.prisma.accountPayable.create({
        data: {
          companyId,
          supplierId: pay.supplierId,
          description: `${baseDescription} (Baixa Parcial)`,
          amount: amountPaid,
          dueDate: paidAtDate,
          status: FinancialStatus.PAID,
          paidAt: paidAtDate,
        },
      });

      // Register in cash flow with exact timestamp of when payment was processed
      await this.prisma.cashMovement.create({
        data: {
          companyId,
          type: CashMovementType.EXPENSE,
          description: `Pagamento Parcial: ${baseDescription}`,
          amount: amountPaid,
          occurredAt: new Date(),
          category: "Operacional",
        },
      });
    } else {
      updated = await this.prisma.accountPayable.update({
        where: { id },
        data: {
          status: FinancialStatus.PAID,
          paidAt: paidAtDate,
        },
      });

      // Register in cash flow with exact timestamp of when payment was processed
      await this.prisma.cashMovement.create({
        data: {
          companyId,
          type: CashMovementType.EXPENSE,
          description: `Pagamento: ${pay.description}`,
          amount: pay.amount,
          occurredAt: new Date(),
          category: "Operacional",
        },
      });
    }

    return updated;
  }
}
