<?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 with escalation) + utilities (% of base) + VAT.
     * Late fee is NOT added at generation — it is added at payment time if overdue.
     *
     * @param ?string $periodStart If provided, used to compute the escalated base for that month.
     */
    public static function generateInvoice(int $contractId, string $periodStart, string $periodEnd, string $dueDate, ?string $notes = null, ?string $periodStartForEscalation = 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, $periodStartForEscalation ?? $periodStart);
        $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).
     * Skips the first grace_period_months months (no rent charged).
     * Due date uses the contract's payment_due_day (default 5); capped to last day of month.
     * 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');
        }

        $gracePeriodMonths = (int) (is_object($contract) ? ($contract->grace_period_months ?? 0) : ($contract['grace_period_months'] ?? 0));
        $paymentDueDay     = (int) (is_object($contract) ? ($contract->payment_due_day     ?? 5) : ($contract['payment_due_day']     ?? 5));
        $paymentDueDay     = max(1, min(28, $paymentDueDay));

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

        $contractStart = new \DateTimeImmutable(date('Y-m-01', strtotime($startDate)));
        $current       = $contractStart;
        $end           = new \DateTimeImmutable($endDate);
        $created       = 0;
        $monthIndex    = 0;   // 0-based from contract start

        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');

            // Build due date: payment_due_day of this month, capped to last day
            $daysInMonth   = (int) $current->format('t');
            $effectiveDueDay = min($paymentDueDay, $daysInMonth);
            $dueDate = $current->format('Y-m-') . str_pad((string) $effectiveDueDay, 2, '0', STR_PAD_LEFT);

            // Skip grace period months
            if ($monthIndex >= $gracePeriodMonths && !in_array($periodStart, $existingStarts, true)) {
                self::generateInvoice($contractId, $periodStart, $periodEnd, $dueDate, null, $periodStart);
                $created++;
            }

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

        return $created;
    }

    /**
     * Calculate the monthly base amount for a specific period.
     *
     * If periodStart is provided and escalation_type is 'flat' or 'tiered':
     *   - base_monthly_year1 = (subtotal - discount_amount) / total_months
     *   - month_index = months between contract.start_date and periodStart (0-based)
     *   - return base_monthly_year1 × getMultiplierForMonth(monthIndex, ...)
     *
     * Otherwise: backward-compatible total_amount / months.
     *
     * @param ?string $periodStart ISO date of the invoice period start (e.g. '2025-01-01')
     */
    public static function calcBase(object|array $contract, ?string $periodStart = null): float
    {
        $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));
        }

        $escType = is_object($contract) ? ($contract->escalation_type ?? 'none') : ($contract['escalation_type'] ?? 'none');
        $escType = (string) $escType;

        if ($periodStart !== null && in_array($escType, ['flat', 'tiered'], true)) {
            // Year-1 base = net total before escalation / months
            $subtotal       = (float) (is_object($contract) ? ($contract->subtotal        ?? 0) : ($contract['subtotal']        ?? 0));
            $discountAmount = (float) (is_object($contract) ? ($contract->discount_amount ?? 0) : ($contract['discount_amount'] ?? 0));
            $baseMonthlyY1  = ($subtotal - $discountAmount) / max(1, $months);

            // month index: 0 = first month of contract
            $contractStart = new \DateTimeImmutable(date('Y-m-01', strtotime($startDate)));
            $periodDt      = new \DateTimeImmutable(date('Y-m-01', strtotime($periodStart)));
            $indexDiff     = $contractStart->diff($periodDt);
            $monthIndex    = ($indexDiff->y * 12) + $indexDiff->m;

            $escRate  = (float) (is_object($contract) ? ($contract->escalation_rate ?? 0) : ($contract['escalation_rate'] ?? 0));
            $tiersRaw = is_object($contract) ? ($contract->escalation_tiers_json ?? null) : ($contract['escalation_tiers_json'] ?? null);
            $tiers    = [];
            if ($tiersRaw && is_string($tiersRaw)) {
                $decoded = json_decode($tiersRaw, true);
                $tiers   = is_array($decoded) ? $decoded : [];
            }

            $multiplier = self::getMultiplierForMonth($monthIndex, $escType, $escRate, $tiers);
            return round($baseMonthlyY1 * $multiplier, 2);
        }

        // Backward-compatible path
        $totalAmount = (float) (is_object($contract) ? $contract->total_amount : ($contract['total_amount'] ?? 0));
        return round($totalAmount / max(1, $months), 2);
    }

    /**
     * Get the escalation multiplier for a given month index (0-based from contract start).
     *
     * For 'none'  : return 1.0
     * For 'flat'  : return (1 + rate/100)^yearIndex
     * For 'tiered': compound the tier rate for each prior year
     *
     * Tiers: [{months: N, annual_rate: R}, ...] — each tier covers N months from contract start.
     */
    public static function getMultiplierForMonth(int $monthIndex, string $escType, float $escRate, array $tiers): float
    {
        if ($escType === 'none' || $escType === '') {
            return 1.0;
        }

        $yearIndex = (int) floor($monthIndex / 12);   // 0 = year 1, no escalation yet

        if ($escType === 'flat') {
            return (1 + $escRate / 100) ** $yearIndex;
        }

        if ($escType === 'tiered') {
            // Compound a multiplier: for each year Y from 0 to yearIndex-1, apply rate of the
            // tier that year Y's start-month falls into.
            $compound = 1.0;
            for ($y = 0; $y < $yearIndex; $y++) {
                $yStartMonth   = $y * 12;
                $cumTierMonths = 0;
                $rateForYear   = 0.0;
                foreach ($tiers as $tier) {
                    $tMonths      = (int) ($tier['months'] ?? 12);
                    $cumTierMonths += $tMonths;
                    if ($yStartMonth < $cumTierMonths) {
                        $rateForYear = (float) ($tier['annual_rate'] ?? 0.00);
                        break;
                    }
                }
                $compound *= (1 + $rateForYear / 100);
            }
            return $compound;
        }

        return 1.0;
    }

    /**
     * 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
     *
     * For daily with bank rate:
     *   daily_rate = late_fee_bank_rate / 365
     *   fee = invoiceTotal × daily_rate × overdue_days
     *
     * For daily with flat rate (backward compat):
     *   fee = invoiceTotal × (late_fee_rate/100) × overdue_days
     *
     * For weekly/monthly: fee = invoiceTotal × (late_fee_rate/100) × periods
     */
    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));
        $lateFeeBankRate = (float) (is_object($contract) ? ($contract->late_fee_bank_rate ?? 0) : ($contract['late_fee_bank_rate'] ?? 0));

        if ($lateFeeType === 'none') {
            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;

        if ($lateFeeType === 'daily') {
            if ($lateFeeBankRate > 0) {
                // Bank annual rate → daily
                $dailyRate = $lateFeeBankRate / 100 / 365;
                return round($invoiceTotal * $dailyRate * $overdueDays, 2);
            }
            if ($lateFeeRate > 0) {
                return round($invoiceTotal * ($lateFeeRate / 100) * $overdueDays, 2);
            }
            return 0.00;
        }

        if ($lateFeeRate <= 0) {
            return 0.00;
        }

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

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