<?php
declare(strict_types=1);

namespace App\Modules\Rentals\Services;

use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Rentals\Models\RentalContract;
use App\Modules\Rentals\Models\RentalInvoice;

final class RentalInvoiceService
{
    /**
     * Generate a single monthly invoice for a contract.
     *
     * Calculates: base (monthly share) + utilities (% of base) + VAT (1% of base+utilities).
     * Late fee is NOT added at generation — it is added at payment time if overdue.
     */
    public static function generateInvoice(int $contractId, string $periodStart, string $periodEnd, string $dueDate, ?string $notes = null): object
    {
        $contract = RentalContract::find($contractId);
        if (!$contract) {
            throw new \RuntimeException('Contract not found');
        }

        $status = is_object($contract) ? $contract->status : ($contract['status'] ?? '');
        if (!in_array($status, ['approved', 'active'], true)) {
            throw new \RuntimeException('Cannot generate invoice for a contract that is not approved or active');
        }

        $base       = self::calcBase($contract);
        $utils      = self::calcUtilities($contract, $base);
        $vat        = self::calcVat($contract, $base + $utils);
        $total      = round($base + $utils + $vat, 2);
        $entityId   = (int) (is_object($contract) ? $contract->entity_id : ($contract['entity_id'] ?? 0));

        $invoice = RentalInvoice::create([
            'invoice_number'   => RentalInvoice::generateNumber(),
            'contract_id'      => $contractId,
            'entity_id'        => $entityId,
            'period_start'     => $periodStart,
            'period_end'       => $periodEnd,
            'due_date'         => $dueDate,
            'base_amount'      => $base,
            'utilities_amount' => $utils,
            'vat_amount'       => $vat,
            'late_fee_amount'  => 0.00,
            'total_amount'     => $total,
            'status'           => 'unpaid',
            'notes'            => $notes,
        ]);

        Logger::info('Rental invoice generated', [
            'invoice_id'  => $invoice->id,
            'contract_id' => $contractId,
            'total'       => $total,
        ]);

        return $invoice;
    }

    /**
     * Mark an invoice as paid and apply late fee if applicable.
     *
     * Late fee is calculated from due_date to paid_at based on contract late_fee_type/rate.
     * Journal entry is dispatched via event.
     */
    public static function markPaid(int $invoiceId, int $paymentId, string $paidAt): void
    {
        $invoice = RentalInvoice::find($invoiceId);
        if (!$invoice) {
            throw new \RuntimeException('Invoice not found');
        }

        $currentStatus = is_object($invoice) ? $invoice->status : ($invoice['status'] ?? '');
        if ($currentStatus === 'paid') {
            throw new \RuntimeException('Invoice is already paid');
        }

        $contractId = (int) (is_object($invoice) ? $invoice->contract_id : ($invoice['contract_id'] ?? 0));
        $contract   = RentalContract::find($contractId);

        $lateFee = $contract ? self::calcLateFee($invoice, $contract, $paidAt) : 0.00;

        $baseAmount  = (float) (is_object($invoice) ? $invoice->base_amount       : ($invoice['base_amount']       ?? 0));
        $utilsAmount = (float) (is_object($invoice) ? $invoice->utilities_amount  : ($invoice['utilities_amount']  ?? 0));
        $vatAmount   = (float) (is_object($invoice) ? $invoice->vat_amount        : ($invoice['vat_amount']        ?? 0));
        $newTotal    = round($baseAmount + $utilsAmount + $vatAmount + $lateFee, 2);

        $invoice->update([
            'status'          => 'paid',
            'paid_at'         => $paidAt,
            'payment_id'      => $paymentId,
            'late_fee_amount' => $lateFee,
            'total_amount'    => $newTotal,
        ]);

        EventBus::dispatch('rental.invoice_paid', [
            'invoice_id'      => $invoiceId,
            'contract_id'     => $contractId,
            'payment_id'      => $paymentId,
            'base_amount'     => $baseAmount,
            'utilities_amount'=> $utilsAmount,
            'vat_amount'      => $vatAmount,
            'late_fee_amount' => $lateFee,
            'total_amount'    => $newTotal,
            'paid_at'         => $paidAt,
        ]);

        Logger::info('Rental invoice paid', [
            'invoice_id' => $invoiceId,
            'total'      => $newTotal,
            'late_fee'   => $lateFee,
        ]);
    }

    /**
     * Generate all monthly invoices for a contract from start_date to end_date.
     *
     * Skips months that already have an invoice (by checking period_start overlap).
     * Due date defaults to the last day of each period.
     * Returns count of newly created invoices.
     */
    public static function bulkGenerateInvoices(int $contractId): int
    {
        $contract = RentalContract::find($contractId);
        if (!$contract) {
            throw new \RuntimeException('Contract not found');
        }

        $status = is_object($contract) ? $contract->status : ($contract['status'] ?? '');
        if (!in_array($status, ['approved', 'active'], true)) {
            throw new \RuntimeException('Cannot generate invoices for a contract that is not approved or active');
        }

        $startDate = is_object($contract) ? ($contract->start_date ?? '') : ($contract['start_date'] ?? '');
        $endDate   = is_object($contract) ? ($contract->end_date   ?? '') : ($contract['end_date']   ?? '');

        if (!$startDate || !$endDate) {
            throw new \RuntimeException('Contract is missing start_date or end_date');
        }

        // Load existing invoices to avoid duplicates
        $existing = RentalInvoice::getForContract($contractId);
        $existingStarts = array_column($existing, 'period_start');

        $current  = new \DateTimeImmutable(date('Y-m-01', strtotime($startDate)));
        $end      = new \DateTimeImmutable($endDate);
        $created  = 0;

        while ($current <= $end) {
            $periodStart = $current->format('Y-m-d');

            // Last day of the current month (or contract end, whichever is earlier)
            $monthEnd = new \DateTimeImmutable($current->format('Y-m-t'));
            $periodEnd = $monthEnd > $end ? $end->format('Y-m-d') : $monthEnd->format('Y-m-d');

            // Skip if already exists
            if (!in_array($periodStart, $existingStarts, true)) {
                self::generateInvoice($contractId, $periodStart, $periodEnd, $periodEnd);
                $created++;
            }

            $current = $current->modify('first day of next month');
        }

        return $created;
    }

    /**
     * Calculate the monthly base amount (contract total / number of months).
     */
    public static function calcBase(object|array $contract): float
    {
        $totalAmount = (float) (is_object($contract) ? $contract->total_amount : ($contract['total_amount'] ?? 0));
        $startDate   = is_object($contract) ? ($contract->start_date ?? '') : ($contract['start_date'] ?? '');
        $endDate     = is_object($contract) ? ($contract->end_date   ?? '') : ($contract['end_date']   ?? '');

        $months = 1;
        if ($startDate && $endDate) {
            $diff   = (new \DateTimeImmutable($startDate))->diff(new \DateTimeImmutable($endDate));
            $months = max(1, ($diff->y * 12) + $diff->m + ($diff->d > 0 ? 1 : 0));
        }

        return round($totalAmount / $months, 2);
    }

    /**
     * Calculate utilities surcharge on the monthly base.
     */
    public static function calcUtilities(object|array $contract, float $base): float
    {
        $pct = (float) (is_object($contract) ? $contract->utilities_percentage : ($contract['utilities_percentage'] ?? 0));
        return round($base * ($pct / 100), 2);
    }

    /**
     * Calculate VAT (default 1%) on (base + utilities).
     */
    public static function calcVat(object|array $contract, float $taxableAmount): float
    {
        $pct = (float) (is_object($contract) ? $contract->vat_percentage : ($contract['vat_percentage'] ?? 1.00));
        return round($taxableAmount * ($pct / 100), 2);
    }

    /**
     * Calculate late fee based on contract late_fee_type and days overdue.
     *
     * late_fee_type: none | daily | weekly | monthly
     * late_fee_rate: % of invoice total per period.
     *
     * Examples:
     *   daily   2%  → 2% of total per overdue day
     *   weekly  5%  → 5% of total per overdue week (or fraction)
     *   monthly 10% → 10% of total per overdue month (or fraction)
     */
    public static function calcLateFee(object|array $invoice, object|array $contract, string $paidAt): float
    {
        $lateFeeType = is_object($contract) ? ($contract->late_fee_type ?? 'none') : ($contract['late_fee_type'] ?? 'none');
        $lateFeeRate = (float) (is_object($contract) ? ($contract->late_fee_rate ?? 0) : ($contract['late_fee_rate'] ?? 0));

        if ($lateFeeType === 'none' || $lateFeeRate <= 0) {
            return 0.00;
        }

        $dueDate     = is_object($invoice) ? ($invoice->due_date ?? '') : ($invoice['due_date'] ?? '');
        $invoiceBase = (float) (is_object($invoice) ? ($invoice->base_amount ?? 0) : ($invoice['base_amount'] ?? 0));
        $invoiceUtils = (float) (is_object($invoice) ? ($invoice->utilities_amount ?? 0) : ($invoice['utilities_amount'] ?? 0));
        $invoiceVat   = (float) (is_object($invoice) ? ($invoice->vat_amount ?? 0) : ($invoice['vat_amount'] ?? 0));
        $invoiceTotal = $invoiceBase + $invoiceUtils + $invoiceVat;

        if (!$dueDate || $invoiceTotal <= 0) {
            return 0.00;
        }

        $due  = new \DateTimeImmutable($dueDate);
        $paid = new \DateTimeImmutable($paidAt);

        if ($paid <= $due) {
            return 0.00;
        }

        $overdueDays = (int) $due->diff($paid)->days;

        $periods = match ($lateFeeType) {
            'daily'   => $overdueDays,
            'weekly'  => (int) ceil($overdueDays / 7),
            'monthly' => (int) ceil($overdueDays / 30),
            default   => 0,
        };

        return round($invoiceTotal * ($lateFeeRate / 100) * $periods, 2);
    }
}
