<?php

namespace App\Services;

use App\Jobs\RefreshCustomerCreditScore;
use App\Models\CustomerStatementEntry;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\LoanDisbursement;
use App\Models\LoanStatusHistory;
use App\Models\User;
use App\Notifications\LoanDisbursedNotification;
use App\Services\Authorization\FinancialMakerCheckerService;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;
use Throwable;

class LoanDisbursementService
{
    public function __construct(
        private MoneyMath $money,
        private FinancialAccountingService $accounting,
        private ProcessingFeeSettlementService $processingFees,
        private FinancialMakerCheckerService $makerChecker,
    ) {}

    public function disburse(Loan $loan, FinancialAccount $account, string $amount, CarbonInterface $date, ?string $reference, ?string $notes, User $user, ?FinancialAccount $processingFeeAccount = null): LoanDisbursement
    {
        $disbursement = DB::transaction(function () use ($loan,$account,$amount,$date,$reference,$notes,$user,$processingFeeAccount): LoanDisbursement {
            $lockedLoan=Loan::query()->with(['branch.business','customer','installments','documents'])->lockForUpdate()->findOrFail($loan->id);
            if(!$user->canAccessBranch($lockedLoan->branch_id)) throw ValidationException::withMessages(['loan'=>'You do not have access to this loan branch.']);
            Gate::forUser($user)->authorize('disburse',$lockedLoan);
            $this->makerChecker->assertLoanDisbursementSeparated($lockedLoan,$user);
            if($lockedLoan->top_up_source_loan_id) throw ValidationException::withMessages(['loan'=>'Top-up replacement loans must be executed through the Loan Top-Up workflow so only net customer cash is disbursed.']);
            if($lockedLoan->status!=='ready_for_disbursement') throw ValidationException::withMessages(['loan'=>'Only loans that are Ready for Disbursement may be disbursed.']);
            if(LoanDisbursement::where('loan_id',$lockedLoan->id)->exists()) throw ValidationException::withMessages(['loan'=>'This loan has already been disbursed.']);
            if($lockedLoan->branch->business->require_signed_contract_before_disbursement && !$lockedLoan->hasSignedContract()) throw ValidationException::withMessages(['loan'=>'A signed contract is required before this loan can be disbursed.']);
            if($account->branch_id!==$lockedLoan->branch_id) throw ValidationException::withMessages(['financial_account_id'=>'The disbursement account must belong to the same branch as the loan.']);

            $principalMinor=$this->money->toMinor((string)$lockedLoan->principal);
            $processingMinor=$this->money->toMinor((string)$lockedLoan->processing_fee);
            $mode=$lockedLoan->processing_fee_payment_mode ?: 'financed';
            $expectedMinor=$mode==='deducted' ? $principalMinor-$processingMinor : $principalMinor;
            if($expectedMinor<=0) throw ValidationException::withMessages(['amount'=>'The processing fee leaves no amount available for disbursement.']);
            if($this->money->toMinor($amount)!==$expectedMinor) throw ValidationException::withMessages(['amount'=>'The disbursement amount must exactly match the amount due to the customer after processing-fee handling.']);
            if($mode==='cash' && $processingMinor>0 && !$processingFeeAccount) throw ValidationException::withMessages(['processing_fee_account_id'=>'Select the cash account that received the processing fee before posting this disbursement.']);

            if($lockedLoan->installments->isEmpty()) throw ValidationException::withMessages(['loan'=>'The approved repayment schedule is missing and the loan cannot be disbursed.']);
            $scheduleTotal=$lockedLoan->installments->sum(fn($i): int=>$this->money->toMinor((string)$i->amount_due));
            if($scheduleTotal!==$this->money->toMinor((string)$lockedLoan->total_repayment)) throw ValidationException::withMessages(['loan'=>'The approved repayment schedule does not reconcile with the loan total.']);

            [,,$number]=$this->nextNumber($lockedLoan,$date); $effectiveReference=$reference ?: $number;
            $disbursement=LoanDisbursement::create(['disbursement_number'=>$number,'loan_id'=>$lockedLoan->id,'branch_id'=>$lockedLoan->branch_id,'financial_account_id'=>$account->id,'amount'=>$this->money->fromMinor($expectedMinor),'disbursement_date'=>$date,'reference'=>$effectiveReference,'notes'=>$notes,'disbursed_by'=>$user->id]);
            $transaction=$this->accounting->postLoanDisbursement($disbursement,$account,$this->money->fromMinor($expectedMinor),$date,$effectiveReference,$notes,$user); $disbursement->update(['financial_account_transaction_id'=>$transaction->id]);

            if($mode==='deducted' && $processingMinor>0) {
                $this->processingFees->settleDeducted($disbursement,$lockedLoan->processing_fee,$date,$effectiveReference,$user);
            } elseif($mode==='cash' && $processingMinor>0) {
                $this->processingFees->collectCash($lockedLoan,$processingFeeAccount,$lockedLoan->processing_fee,$date,$effectiveReference,$user);
            }

            foreach($lockedLoan->installments as $installment){
                $installment->update(['principal_paid'=>'0.00','interest_paid'=>'0.00','fees_paid'=>'0.00','penalty_due'=>'0.00','penalty_paid'=>'0.00','amount_paid'=>'0.00','remaining'=>$installment->amount_due,'status'=>'upcoming']);
            }

            $from=$lockedLoan->status;
            $lockedLoan->update([
                'status'=>'active','disbursed_at'=>$date,'disbursed_by'=>$user->id,'activated_at'=>now(),'schedule_activated_at'=>now(),
                'principal_outstanding'=>$lockedLoan->principal,'interest_outstanding'=>$lockedLoan->interest,'fees_outstanding'=>$lockedLoan->fees,
                'penalties_outstanding'=>'0.00','total_outstanding'=>$lockedLoan->total_repayment,'total_paid'=>'0.00','last_payment_at'=>null,'completed_at'=>null,'updated_by'=>$user->id,
            ]);
            LoanStatusHistory::create(['loan_id'=>$lockedLoan->id,'from_status'=>$from,'to_status'=>'active','notes'=>'Loan disbursed via '.$account->name.' ('.$number.'). Processing fee: '.$lockedLoan->processingFeePaymentModeLabel().'.','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'=>$number,'type'=>'loan_disbursement','description'=>'Loan obligation '.$lockedLoan->loan_number,'debit'=>$lockedLoan->total_repayment,'credit'=>'0.00','running_balance'=>$lockedLoan->total_repayment,'related_type'=>LoanDisbursement::class,'related_id'=>$disbursement->id]);
            $lockedLoan->customer->notify(new LoanDisbursedNotification($disbursement->fresh('loan')));

            return $disbursement->fresh(['loan.customer','branch','financialAccount','user','accountTransaction']);
        });

        try {
            RefreshCustomerCreditScore::dispatch(
                (int) $disbursement->loan->customer_id,
                (int) $disbursement->branch_id,
                'loan_activated',
            );
        } catch (Throwable $exception) {
            report($exception);
        }

        return $disbursement;
    }

    private function nextNumber(Loan $loan, CarbonInterface $date): array
    {
        $year=(int)$date->format('Y');
        DB::table('number_sequences')->insertOrIgnore(['type'=>'loan_disbursement','branch_id'=>$loan->branch_id,'year'=>$year,'current_value'=>0,'created_at'=>now(),'updated_at'=>now()]);
        $row=DB::table('number_sequences')->where('type','loan_disbursement')->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 [$sequence,$year,'DSB-'.$tag.'-'.$year.'-'.str_pad((string)$sequence,6,'0',STR_PAD_LEFT)];
    }
}
