<?php

namespace App\Services;

use App\Models\CustomerStatementEntry;
use App\Models\DailyCollection;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\LoanPayment;
use App\Models\LoanPaymentAllocation;
use App\Models\LoanStatusHistory;
use App\Models\RemotePaymentSubmission;
use App\Models\User;
use App\Services\CreditScore\CreditScoreRefreshCoordinator;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;

class LoanPaymentService
{
    public function __construct(
        private MoneyMath $money,
        private FinancialAccountingService $accounting,
        private LoanPenaltyService $penalties,
        private PaymentDeadlinePenaltyService $deadlinePenalties,
        private OverdueLoanService $overdue,
        private CreditScoreRefreshCoordinator $creditScores,
    ) {}

    public function post(Loan $loan, FinancialAccount $account, string $amount, CarbonInterface $date, string $method, ?string $reference, ?string $notes, User $user): LoanPayment
    {
        Gate::forUser($user)->authorize('receivePayment',$loan);

        return $this->postAuthorized($loan,$account,$amount,$date,$method,$reference,$notes,$user);
    }

    public function postVerifiedRemotePayment(RemotePaymentSubmission $submission, FinancialAccount $account, CarbonInterface $date, string $method, User $user): LoanPayment
    {
        Gate::forUser($user)->authorize('verify',$submission);
        $submission->loadMissing('loan');

        return $this->postAuthorized(
            $submission->loan,
            $account,
            (string) $submission->amount,
            $date,
            $method,
            $submission->transaction_reference,
            $submission->notes,
            $user,
        );
    }

    private function postAuthorized(Loan $loan, FinancialAccount $account, string $amount, CarbonInterface $date, string $method, ?string $reference, ?string $notes, User $user): LoanPayment
    {
        $amountMinor=$this->money->toMinor($amount);
        if($amountMinor<=0) throw ValidationException::withMessages(['amount'=>'Payment amount must be greater than zero.']);

        return DB::transaction(function () use ($loan,$account,$amountMinor,$date,$method,$reference,$notes,$user): LoanPayment {
            $lockedLoan=Loan::query()->with(['branch.business','customer'])->lockForUpdate()->findOrFail($loan->id);
            if(!$user->canAccessBranch($lockedLoan->branch_id)) throw ValidationException::withMessages(['loan'=>'You do not have access to this loan branch.']);
            if($lockedLoan->status!=='active') throw ValidationException::withMessages(['loan'=>'Payments can only be posted to an active loan.']);
            if($account->branch_id!==$lockedLoan->branch_id) throw ValidationException::withMessages(['financial_account_id'=>'The receiving account must belong to the same branch as the loan.']);

            $paymentAt=CarbonImmutable::instance($date);
            $this->deadlinePenalties->assessMissedSchedules($lockedLoan,$paymentAt->startOfDay());
            $this->deadlinePenalties->assessLatePayment($lockedLoan->fresh(),$paymentAt);
            $this->penalties->assess($lockedLoan->fresh(),$paymentAt->startOfDay());
            $lockedLoan->refresh();
            $installments=$lockedLoan->installments()->orderBy('installment_number')->lockForUpdate()->get();
            if($installments->isEmpty()) throw ValidationException::withMessages(['loan'=>'This loan has no active repayment schedule.']);

            $available=['principal'=>0,'interest'=>0,'fees'=>0,'penalties'=>0];
            foreach($installments as $installment){
                $available['principal']+=max(0,$this->money->toMinor((string)$installment->principal)-$this->money->toMinor((string)$installment->principal_paid));
                $available['interest']+=max(0,$this->money->toMinor((string)$installment->interest)-$this->money->toMinor((string)$installment->interest_paid));
                $available['fees']+=max(0,$this->money->toMinor((string)$installment->fees)-$this->money->toMinor((string)$installment->fees_paid));
                $available['penalties']+=max(0,$this->money->toMinor((string)$installment->penalty_due)-$this->money->toMinor((string)$installment->penalty_paid));
            }
            $totalAvailable=array_sum($available);
            if($totalAvailable<=0) throw ValidationException::withMessages(['amount'=>'This loan has no outstanding amount to collect.']);
            if($amountMinor>$totalAvailable) throw ValidationException::withMessages(['amount'=>'Payment cannot exceed the current outstanding loan balance of '.$this->money->fromMinor($totalAvailable).'.']);

            [$receiptNumber]=$this->nextReceiptNumber($lockedLoan,$date);
            $effectiveReference=$reference ?: $receiptNumber;
            $payment=LoanPayment::create([
                'receipt_number'=>$receiptNumber,'loan_id'=>$lockedLoan->id,'customer_id'=>$lockedLoan->customer_id,'branch_id'=>$lockedLoan->branch_id,
                'financial_account_id'=>$account->id,'amount'=>$this->money->fromMinor($amountMinor),'payment_date'=>$date,'method'=>$method,
                'reference'=>$effectiveReference,'notes'=>$notes,'received_by'=>$user->id,
            ]);

            $left=$amountMinor; $allocated=['principal'=>0,'interest'=>0,'fees'=>0,'penalties'=>0];
            foreach($installments as $installment){
                if($left<=0) break;
                $row=['principal'=>0,'interest'=>0,'fees'=>0,'penalties'=>0];
                foreach([
                    'penalties'=>['penalty_due','penalty_paid'],
                    'fees'=>['fees','fees_paid'],
                    'interest'=>['interest','interest_paid'],
                    'principal'=>['principal','principal_paid'],
                ] as $component=>[$dueField,$paidField]){
                    if($left<=0) break;
                    $due=$this->money->toMinor((string)$installment->{$dueField}); $paid=$this->money->toMinor((string)$installment->{$paidField}); $remaining=max(0,$due-$paid);
                    if($remaining===0) continue;
                    $take=min($left,$remaining); $row[$component]+=$take; $allocated[$component]+=$take; $left-=$take;
                    $installment->{$paidField}=$this->money->fromMinor($paid+$take);
                }
                $rowTotal=array_sum($row);
                if($rowTotal>0){
                    $paidTotal=$this->money->toMinor((string)$installment->principal_paid)+$this->money->toMinor((string)$installment->interest_paid)+$this->money->toMinor((string)$installment->fees_paid)+$this->money->toMinor((string)$installment->penalty_paid);
                    $dueTotal=$this->money->toMinor((string)$installment->amount_due)+$this->money->toMinor((string)$installment->penalty_due); $remainingTotal=max(0,$dueTotal-$paidTotal);
                    $installment->amount_paid=$this->money->fromMinor($paidTotal); $installment->remaining=$this->money->fromMinor($remainingTotal);
                    $installment->status=$remainingTotal===0?'paid':(CarbonImmutable::parse($installment->due_date)->startOfDay()->lessThan($paymentAt->startOfDay())?'overdue':'partial');
                    $installment->save();
                    LoanPaymentAllocation::create([
                        'loan_payment_id'=>$payment->id,'loan_installment_id'=>$installment->id,
                        'principal'=>$this->money->fromMinor($row['principal']),'interest'=>$this->money->fromMinor($row['interest']),
                        'fees'=>$this->money->fromMinor($row['fees']),'penalties'=>$this->money->fromMinor($row['penalties']),'total'=>$this->money->fromMinor($rowTotal),
                    ]);
                }
            }
            if($left!==0) throw ValidationException::withMessages(['amount'=>'The repayment schedule could not fully allocate this payment. No financial changes were saved.']);

            $payment->update([
                'principal_amount'=>$this->money->fromMinor($allocated['principal']),'interest_amount'=>$this->money->fromMinor($allocated['interest']),
                'fees_amount'=>$this->money->fromMinor($allocated['fees']),'penalties_amount'=>$this->money->fromMinor($allocated['penalties']),
            ]);
            $transaction=$this->accounting->postLoanPayment($payment,$account,$this->formatted($allocated),$date,$effectiveReference,$notes,$user);
            $payment->update(['financial_account_transaction_id'=>$transaction->id]);

            $remaining=[]; foreach($available as $component=>$minor) $remaining[$component]=$minor-$allocated[$component]; $newOutstanding=array_sum($remaining);
            $newPaid=$this->money->toMinor((string)($lockedLoan->total_paid ?? '0'))+$amountMinor;
            $loanUpdate=[
                'principal_outstanding'=>$this->money->fromMinor($remaining['principal']),'interest_outstanding'=>$this->money->fromMinor($remaining['interest']),
                'fees_outstanding'=>$this->money->fromMinor($remaining['fees']),'penalties_outstanding'=>$this->money->fromMinor($remaining['penalties']),
                'total_outstanding'=>$this->money->fromMinor($newOutstanding),'total_paid'=>$this->money->fromMinor($newPaid),'last_payment_at'=>$date,'updated_by'=>$user->id,
            ];
            if($newOutstanding===0){ $loanUpdate['status']='completed'; $loanUpdate['completed_at']=$date; }
            $from=$lockedLoan->status; $lockedLoan->update($loanUpdate);
            if($newOutstanding===0) LoanStatusHistory::create(['loan_id'=>$lockedLoan->id,'from_status'=>$from,'to_status'=>'completed','notes'=>'Loan fully repaid with receipt '.$receiptNumber.'.','changed_by'=>$user->id,'changed_at'=>now()]);

            CustomerStatementEntry::create([
                'customer_id'=>$lockedLoan->customer_id,'loan_id'=>$lockedLoan->id,'branch_id'=>$lockedLoan->branch_id,'entry_date'=>$date,
                'reference'=>$receiptNumber,'type'=>'loan_payment','description'=>'Loan payment '.$lockedLoan->loan_number,'debit'=>'0.00','credit'=>$this->money->fromMinor($amountMinor),
                'running_balance'=>$this->money->fromMinor($newOutstanding),'related_type'=>LoanPayment::class,'related_id'=>$payment->id,
            ]);
            $this->updateDailyCollection($lockedLoan->branch_id,$account->id,$date,$amountMinor,$allocated);
            $this->overdue->refreshLoan($lockedLoan->fresh(), $date);
            $this->creditScores->afterCommitForLoan($lockedLoan, $newOutstanding === 0 ? 'loan_completed' : 'loan_payment_posted');

            return $payment->fresh(['loan.customer','branch','financialAccount','receiver','allocations.installment','accountTransaction']);
        });
    }

    private function updateDailyCollection(int $branchId, int $accountId, CarbonInterface $date, int $amountMinor, array $allocated): void
    {
        $day=$date->format('Y-m-d');
        DB::table('daily_collections')->insertOrIgnore(['branch_id'=>$branchId,'financial_account_id'=>$accountId,'collection_date'=>$day,'amount'=>0,'principal'=>0,'interest'=>0,'fees'=>0,'penalties'=>0,'payments_count'=>0,'created_at'=>now(),'updated_at'=>now()]);
        $row=DailyCollection::query()->where('branch_id',$branchId)->where('financial_account_id',$accountId)->whereDate('collection_date',$day)->lockForUpdate()->firstOrFail();
        $row->update([
            'amount'=>$this->money->fromMinor($this->money->toMinor((string)$row->amount)+$amountMinor),
            'principal'=>$this->money->fromMinor($this->money->toMinor((string)$row->principal)+$allocated['principal']),
            'interest'=>$this->money->fromMinor($this->money->toMinor((string)$row->interest)+$allocated['interest']),
            'fees'=>$this->money->fromMinor($this->money->toMinor((string)$row->fees)+$allocated['fees']),
            'penalties'=>$this->money->fromMinor($this->money->toMinor((string)$row->penalties)+$allocated['penalties']),
            'payments_count'=>$row->payments_count+1,
        ]);
    }

    private function nextReceiptNumber(Loan $loan, CarbonInterface $date): array
    {
        $year=(int)$date->format('Y');
        DB::table('number_sequences')->insertOrIgnore(['type'=>'loan_receipt','branch_id'=>$loan->branch_id,'year'=>$year,'current_value'=>0,'created_at'=>now(),'updated_at'=>now()]);
        $row=DB::table('number_sequences')->where('type','loan_receipt')->where('branch_id',$loan->branch_id)->where('year',$year)->lockForUpdate()->first();
        $sequence=((int)$row->current_value)+1; DB::table('number_sequences')->where('id',$row->id)->update(['current_value'=>$sequence,'updated_at'=>now()]);
        $tag=strtoupper(preg_replace('/[^A-Z0-9]/','',str_replace('BR-','',$loan->branch->code)) ?: (string)$loan->branch_id);
        return ['REC-'.$tag.'-'.$year.'-'.str_pad((string)$sequence,6,'0',STR_PAD_LEFT)];
    }

    private function formatted(array $minor): array
    {
        return collect($minor)->map(fn(int $value): string=>$this->money->fromMinor($value))->all();
    }
}
