<?php

namespace App\Services;

use App\Models\CustomerStatementEntry;
use App\Models\Loan;
use App\Models\LoanInstallment;
use App\Models\LoanPenaltyCharge;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;

class PaymentDeadlinePenaltyService
{
    public function __construct(
        private MoneyMath $money,
        private CustomerStatementBalanceService $statements,
        private BranchContext $branchContext,
    ) {}

    public function assessLatePayment(Loan $loan, CarbonInterface $paidAt): int
    {
        return $this->branchContext->forBranch((int) $loan->branch_id, fn () => $this->assessLatePaymentWithinBranch($loan, $paidAt));
    }

    private function assessLatePaymentWithinBranch(Loan $loan, CarbonInterface $paidAt): int
    {
        if (! $this->enabled($loan)) return 0;

        $timezone = (string) config('app.timezone', 'Africa/Dar_es_Salaam');
        $at = CarbonImmutable::instance($paidAt)->setTimezone($timezone);
        $time = trim((string) $loan->payment_deadline_time);
        if ($time === '') return 0;

        $total = 0;
        $installments = $loan->installments()
            ->whereDate('due_date', $at->toDateString())
            ->orderBy('installment_number')
            ->lockForUpdate()
            ->get();

        foreach ($installments as $installment) {
            if ($installment->excuse()->exists()) continue;

            $deadline = CarbonImmutable::parse($installment->due_date->format('Y-m-d').' '.$time, $timezone);
            if (! $at->greaterThan($deadline)) continue;
            if (LoanPenaltyCharge::query()->where('loan_installment_id',$installment->id)->where('period_key','deadline-late')->exists()) continue;

            $baseMinor = $this->scheduledBaseMinorAt($installment, $at);
            if ($baseMinor <= 0) continue;

            $total += $this->charge(
                $loan,
                $installment,
                'deadline-late',
                'deadline_late',
                (string) $loan->deadline_late_penalty_type,
                (string) $loan->deadline_late_penalty_value,
                (string) $loan->deadline_late_penalty_settlement,
                $baseMinor,
                $at,
                0,
                'Payment posted after '.$deadline->format('H:i').' deadline',
            );
        }

        if ($total > 0) $this->statements->recalculate($loan->fresh());
        return $total;
    }

    public function assessMissedSchedules(Loan $loan, CarbonInterface $asOf): int
    {
        return $this->branchContext->forBranch((int) $loan->branch_id, fn () => $this->assessMissedSchedulesWithinBranch($loan, $asOf));
    }

    private function assessMissedSchedulesWithinBranch(Loan $loan, CarbonInterface $asOf): int
    {
        if (! $this->enabled($loan)) return 0;

        $timezone = (string) config('app.timezone', 'Africa/Dar_es_Salaam');
        $target = CarbonImmutable::instance($asOf)->setTimezone($timezone)->startOfDay();
        $total = 0;
        $installments = $loan->installments()->orderBy('installment_number')->lockForUpdate()->get();

        foreach ($installments as $installment) {
            $due = CarbonImmutable::parse($installment->due_date, $timezone)->startOfDay();
            if (! $due->lessThan($target)) continue;
            if ($installment->excuse()->exists()) continue;
            if (LoanPenaltyCharge::query()->where('loan_installment_id',$installment->id)->where('period_key','deadline-missed')->exists()) continue;

            $effective = $due->addDay();
            $baseMinor = $this->scheduledBaseMinorAt($installment, $effective);
            if ($baseMinor <= 0) continue;

            $total += $this->charge(
                $loan,
                $installment,
                'deadline-missed',
                'missed_schedule',
                (string) $loan->missed_schedule_penalty_type,
                (string) $loan->missed_schedule_penalty_value,
                (string) $loan->missed_schedule_penalty_settlement,
                $baseMinor,
                $effective,
                1,
                'Scheduled payment was not completed on '.$due->format('d M Y'),
            );
        }

        if ($total > 0) $this->statements->recalculate($loan->fresh());
        return $total;
    }

    private function enabled(Loan $loan): bool
    {
        $loan->loadMissing('branch.business');
        return (bool) $loan->branch->business->payment_deadline_penalties_enabled
            && (bool) $loan->payment_deadline_enabled;
    }

    private function charge(
        Loan $loan,
        LoanInstallment $installment,
        string $periodKey,
        string $reason,
        string $type,
        string $value,
        string $settlement,
        int $baseMinor,
        CarbonImmutable $effective,
        int $daysOverdue,
        string $description,
    ): int {
        if (! in_array($type, ['fixed','percentage'], true)) return 0;
        if (! in_array($settlement, ['add_to_loan','next_payment'], true)) $settlement = 'add_to_loan';

        $amountMinor = $type === 'fixed'
            ? $this->money->toMinor($value)
            : $this->money->percentageOf($baseMinor, $value);
        if ($amountMinor <= 0) return 0;

        $charge = LoanPenaltyCharge::create([
            'loan_id'=>$loan->id,
            'loan_installment_id'=>$installment->id,
            'branch_id'=>$loan->branch_id,
            'effective_date'=>$effective->toDateString(),
            'period_key'=>$periodKey,
            'penalty_type'=>$type,
            'penalty_value'=>$value,
            'base_amount'=>$this->money->fromMinor($baseMinor),
            'amount'=>$this->money->fromMinor($amountMinor),
            'days_overdue'=>$daysOverdue,
            'source'=>'payment_deadline',
            'penalty_reason'=>$reason,
            'settlement_mode'=>$settlement,
        ]);

        $newPenaltyDue = $this->money->toMinor((string) $installment->penalty_due) + $amountMinor;
        $newRemaining = $this->money->toMinor((string) $installment->remaining) + $amountMinor;
        $installment->update([
            'penalty_due'=>$this->money->fromMinor($newPenaltyDue),
            'remaining'=>$this->money->fromMinor($newRemaining),
            'status'=>$daysOverdue > 0 ? 'overdue' : $installment->status,
        ]);

        $loan->update([
            'penalties_outstanding'=>$this->money->fromMinor($this->money->toMinor((string) ($loan->penalties_outstanding ?? '0')) + $amountMinor),
            'total_outstanding'=>$this->money->fromMinor($this->money->toMinor((string) ($loan->total_outstanding ?? '0')) + $amountMinor),
        ]);

        CustomerStatementEntry::create([
            'customer_id'=>$loan->customer_id,
            'loan_id'=>$loan->id,
            'branch_id'=>$loan->branch_id,
            'entry_date'=>$effective,
            'reference'=>'PEN-'.str_pad((string) $charge->id, 8, '0', STR_PAD_LEFT),
            'type'=>'loan_penalty',
            'description'=>$description.' · '.($settlement === 'next_payment' ? 'pay with next payment' : 'added to loan balance'),
            'debit'=>$this->money->fromMinor($amountMinor),
            'credit'=>'0.00',
            'running_balance'=>'0.00',
            'related_type'=>LoanPenaltyCharge::class,
            'related_id'=>$charge->id,
        ]);

        return $amountMinor;
    }

    private function scheduledBaseMinorAt(LoanInstallment $installment, CarbonImmutable $effective): int
    {
        $paidPrincipal = 0; $paidInterest = 0; $paidFees = 0;
        $allocations = $installment->paymentAllocations()->with('payment')->get();

        foreach ($allocations as $allocation) {
            if (! $allocation->payment) continue;
            $paymentAt = CarbonImmutable::parse($allocation->payment->payment_date);
            if (! $paymentAt->lessThan($effective)) continue;
            $paidPrincipal += $this->money->toMinor((string) $allocation->principal);
            $paidInterest += $this->money->toMinor((string) $allocation->interest);
            $paidFees += $this->money->toMinor((string) $allocation->fees);
        }

        return max(0, $this->money->toMinor((string)$installment->principal)-$paidPrincipal)
            + max(0, $this->money->toMinor((string)$installment->interest)-$paidInterest)
            + max(0, $this->money->toMinor((string)$installment->fees)-$paidFees);
    }
}
