<?php

namespace App\Services;

use App\Models\CustomerStatementEntry;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\LoanProduct;
use App\Models\LoanStatusHistory;
use App\Models\LoanTopUp;
use App\Models\User;
use App\Services\Authorization\FinancialMakerCheckerService;
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 LoanTopUpService
{
    public function __construct(
        private MoneyMath $money,
        private LoanApplicationService $applications,
        private LoanPenaltyService $penalties,
        private LoanTopUpAccountingService $accounting,
        private CustomerStatementBalanceService $statements,
        private FinancialMakerCheckerService $makerChecker,
        private CreditScoreRefreshCoordinator $creditScores,
    ) {}

    /** @return array{paid_percentage:string,threshold:string,eligible:bool,scheduled_outstanding:string,current_outstanding:string} */
    public function eligibility(Loan $loan): array
    {
        $loan->loadMissing('branch.business');
        $total = $this->money->toMinor((string) $loan->total_repayment);
        $scheduledOutstanding = $this->money->toMinor((string) ($loan->principal_outstanding ?? '0'))
            + $this->money->toMinor((string) ($loan->interest_outstanding ?? '0'))
            + $this->money->toMinor((string) ($loan->fees_outstanding ?? '0'));
        $paidScheduled = max(0, $total - min($total, $scheduledOutstanding));
        $paidBasisPoints = $total > 0 ? $this->money->mulDivRound($paidScheduled, 10000, $total) : 0;
        $threshold = (string) ($loan->branch->business->top_up_eligibility_percentage ?? '70.00');
        $thresholdBasisPoints = intdiv($this->money->rateScaled($threshold), 10000);

        return [
            'paid_percentage'=>$this->formatBasisPoints($paidBasisPoints),
            'threshold'=>number_format((float) $threshold, 2, '.', ''),
            'eligible'=>$paidBasisPoints >= $thresholdBasisPoints,
            'scheduled_outstanding'=>$this->money->fromMinor($scheduledOutstanding),
            'current_outstanding'=>$this->money->fromMinor($this->money->toMinor((string) ($loan->total_outstanding ?? '0'))),
        ];
    }

    public function initiate(Loan $previousLoan, array $data, LoanProduct $product, User $user): Loan
    {
        if (! $user->canAccessBranch($previousLoan->branch_id)) {
            throw ValidationException::withMessages(['loan'=>'You do not have access to this loan branch.']);
        }
        Gate::forUser($user)->authorize('createTopUp',$previousLoan);
        if ($previousLoan->status !== 'active') {
            throw ValidationException::withMessages(['loan'=>'Only an active loan can be topped up.']);
        }
        if (Loan::query()->where('top_up_source_loan_id',$previousLoan->id)->where('status','!=','rejected')->exists()) {
            throw ValidationException::withMessages(['loan'=>'This loan already has an open or completed top-up replacement loan.']);
        }
        $previousLoan->loadMissing('branch.business');
        if ($product->business_id !== $previousLoan->branch->business_id || $product->status !== 'active') {
            throw ValidationException::withMessages(['loan_product_id'=>'The selected loan product is not available for this branch.']);
        }

        $currentOutstanding = $this->money->toMinor((string) $previousLoan->total_outstanding);
        if ($this->money->toMinor((string) $data['principal']) <= $currentOutstanding) {
            throw ValidationException::withMessages(['principal'=>'New principal must be greater than the current old-loan outstanding so the customer receives net top-up cash.']);
        }

        $eligibility = $this->eligibility($previousLoan);
        $override = (bool) ($data['override_eligibility'] ?? false);
        $overrideReason = trim((string) ($data['override_reason'] ?? ''));
        if (! $eligibility['eligible']) {
            if (! $override) {
                throw ValidationException::withMessages(['eligibility'=>'Customer has repaid '.$eligibility['paid_percentage'].'%; the configured top-up threshold is '.$eligibility['threshold'].'%.']);
            }
            if (! $user->can('loans.top-up.override')) {
                throw ValidationException::withMessages(['override_eligibility'=>'You are not authorized to override top-up eligibility.']);
            }
            if ($overrideReason === '') {
                throw ValidationException::withMessages(['override_reason'=>'An override reason is required when eligibility is bypassed.']);
            }
        } else {
            $override = false;
            $overrideReason = '';
        }

        $applicationData = $data;
        $applicationData['branch_id'] = $previousLoan->branch_id;
        $applicationData['customer_id'] = $previousLoan->customer_id;
        unset($applicationData['override_eligibility'],$applicationData['override_reason']);

        return DB::transaction(function () use ($previousLoan,$applicationData,$product,$user,$eligibility,$override,$overrideReason): Loan {
            $locked = Loan::query()->lockForUpdate()->findOrFail($previousLoan->id);
            if ($locked->status !== 'active') {
                throw ValidationException::withMessages(['loan'=>'The previous loan changed status before the top-up application could be created.']);
            }
            if (Loan::query()->where('top_up_source_loan_id',$locked->id)->where('status','!=','rejected')->exists()) {
                throw ValidationException::withMessages(['loan'=>'Another top-up application was created for this loan.']);
            }

            $newLoan = $this->applications->create($applicationData,$product,$user);
            $newLoan->update([
                'top_up_source_loan_id'=>$locked->id,
                'top_up_eligibility_percentage'=>$eligibility['threshold'],
                'top_up_paid_percentage_at_application'=>$eligibility['paid_percentage'],
                'top_up_override_used'=>$override,
                'top_up_override_reason'=>$override ? $overrideReason : null,
                'top_up_override_by'=>$override ? $user->id : null,
                'top_up_override_at'=>$override ? now() : null,
            ]);
            return $newLoan->fresh();
        });
    }

    public function execute(Loan $newLoan, FinancialAccount $account, CarbonInterface $date, ?string $reference, ?string $notes, User $user): LoanTopUp
    {
        if (! $newLoan->top_up_source_loan_id) {
            throw ValidationException::withMessages(['loan'=>'This loan is not a top-up replacement application.']);
        }

        return DB::transaction(function () use ($newLoan,$account,$date,$reference,$notes,$user): LoanTopUp {
            $ids = [$newLoan->id, (int) $newLoan->top_up_source_loan_id];
            sort($ids);
            $locked = Loan::query()->whereIn('id',$ids)->orderBy('id')->lockForUpdate()->get()->keyBy('id');
            $replacement = $locked->get($newLoan->id);
            $previous = $locked->get((int) $newLoan->top_up_source_loan_id);
            if (! $replacement || ! $previous) throw ValidationException::withMessages(['loan'=>'One of the linked top-up loans no longer exists.']);
            if (! $user->canAccessBranch($replacement->branch_id)) throw ValidationException::withMessages(['loan'=>'You do not have access to this loan branch.']);
            Gate::forUser($user)->authorize('executeTopUp',$replacement);
            $this->makerChecker->assertTopUpExecutionSeparated($replacement,$user);
            if ($replacement->status !== 'ready_for_disbursement') throw ValidationException::withMessages(['loan'=>'The replacement loan must be Ready for Disbursement before executing the top-up.']);
            if ($previous->status !== 'active') throw ValidationException::withMessages(['loan'=>'The previous loan must still be active when the top-up is executed.']);
            if ($replacement->branch_id !== $previous->branch_id || $replacement->customer_id !== $previous->customer_id) throw ValidationException::withMessages(['loan'=>'Top-up loans must belong to the same customer and branch.']);
            if ($account->branch_id !== $replacement->branch_id) throw ValidationException::withMessages(['financial_account_id'=>'The top-up disbursement account must belong to the same branch as the replacement loan.']);
            if (LoanTopUp::query()->where('new_loan_id',$replacement->id)->orWhere('previous_loan_id',$previous->id)->exists()) throw ValidationException::withMessages(['loan'=>'This top-up has already been executed.']);
            if ($replacement->disbursement()->exists()) throw ValidationException::withMessages(['loan'=>'A normally disbursed loan cannot be executed as a top-up.']);

            $replacement->loadMissing(['branch.business','documents','installments']);
            if ($replacement->branch->business->require_signed_contract_before_disbursement && ! $replacement->hasSignedContract()) {
                throw ValidationException::withMessages(['loan'=>'A signed contract is required before the top-up can be executed.']);
            }

            $this->penalties->assess($previous, CarbonImmutable::instance($date)->startOfDay());
            $previous->refresh();
            $components = [
                'principal'=>$this->money->toMinor((string) $previous->principal_outstanding),
                'interest'=>$this->money->toMinor((string) $previous->interest_outstanding),
                'fees'=>$this->money->toMinor((string) $previous->fees_outstanding),
                'penalties'=>$this->money->toMinor((string) $previous->penalties_outstanding),
            ];
            $previousOutstanding = array_sum($components);
            if ($previousOutstanding <= 0 || $previousOutstanding !== $this->money->toMinor((string) $previous->total_outstanding)) {
                throw ValidationException::withMessages(['loan'=>'The previous loan outstanding components do not reconcile.']);
            }

            $newPrincipal = $this->money->toMinor((string) $replacement->principal);
            if ($newPrincipal <= $previousOutstanding) throw ValidationException::withMessages(['principal'=>'The approved new principal no longer exceeds the current old-loan outstanding.']);
            $netCash = $newPrincipal - $previousOutstanding;

            $installments = $replacement->installments()->orderBy('installment_number')->lockForUpdate()->get();
            if ($installments->isEmpty()) throw ValidationException::withMessages(['loan'=>'The replacement repayment schedule is missing.']);
            $scheduleTotal = $installments->sum(fn ($item): int => $this->money->toMinor((string) $item->amount_due));
            if ($scheduleTotal !== $this->money->toMinor((string) $replacement->total_repayment)) throw ValidationException::withMessages(['loan'=>'The replacement repayment schedule does not reconcile with the loan total.']);

            $number = $this->nextNumber($replacement,$date);
            $effectiveReference = $reference ?: $number;
            $topUp = LoanTopUp::create([
                'top_up_number'=>$number,'branch_id'=>$replacement->branch_id,'previous_loan_id'=>$previous->id,'new_loan_id'=>$replacement->id,
                'financial_account_id'=>$account->id,'previous_outstanding'=>$this->money->fromMinor($previousOutstanding),
                'previous_principal'=>$this->money->fromMinor($components['principal']),'previous_interest'=>$this->money->fromMinor($components['interest']),
                'previous_fees'=>$this->money->fromMinor($components['fees']),'previous_penalties'=>$this->money->fromMinor($components['penalties']),
                'amount_settled_internally'=>$this->money->fromMinor($previousOutstanding),'new_principal'=>$replacement->principal,
                'new_interest'=>$replacement->interest,'new_fees'=>$replacement->fees,'new_total_repayment'=>$replacement->total_repayment,
                'cash_given_to_customer'=>$this->money->fromMinor($netCash),'top_up_date'=>$date,'reference'=>$effectiveReference,
                'notes'=>$notes,'executed_by'=>$user->id,
            ]);
            $topUp->load(['previousLoan','newLoan']);
            $transaction = $this->accounting->post($topUp,$account,collect($components)->map(fn (int $value): string=>$this->money->fromMinor($value))->all(),$date,$effectiveReference,$notes,$user);
            $topUp->update(['financial_account_transaction_id'=>$transaction->id]);

            foreach ($previous->installments()->orderBy('installment_number')->lockForUpdate()->get() as $installment) {
                $dueTotal = $this->money->toMinor((string) $installment->amount_due) + $this->money->toMinor((string) $installment->penalty_due);
                $installment->update([
                    'principal_paid'=>$installment->principal,'interest_paid'=>$installment->interest,'fees_paid'=>$installment->fees,
                    'penalty_paid'=>$installment->penalty_due,'amount_paid'=>$this->money->fromMinor($dueTotal),'remaining'=>'0.00','status'=>'paid',
                ]);
            }

            $previousPaid = $this->money->toMinor((string) ($previous->total_paid ?? '0')) + $previousOutstanding;
            $previous->update([
                'status'=>'closed_through_top_up','principal_outstanding'=>'0.00','interest_outstanding'=>'0.00','fees_outstanding'=>'0.00',
                'penalties_outstanding'=>'0.00','total_outstanding'=>'0.00','total_paid'=>$this->money->fromMinor($previousPaid),
                'overdue_principal'=>'0.00','overdue_interest'=>'0.00','overdue_fees'=>'0.00','overdue_penalties'=>'0.00','total_overdue'=>'0.00',
                'days_overdue'=>0,'arrears_bucket'=>null,'arrears_updated_at'=>now(),'last_payment_at'=>$date,'completed_at'=>$date,'updated_by'=>$user->id,
            ]);
            LoanStatusHistory::create([
                'loan_id'=>$previous->id,'from_status'=>'active','to_status'=>'closed_through_top_up',
                'notes'=>'Closed through top-up '.$number.' into '.$replacement->loan_number.'.','changed_by'=>$user->id,'changed_at'=>now(),
            ]);

            foreach ($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',
                ]);
            }
            $replacement->update([
                'status'=>'active','disbursed_at'=>$date,'disbursed_by'=>$user->id,'activated_at'=>now(),'schedule_activated_at'=>now(),
                'principal_outstanding'=>$replacement->principal,'interest_outstanding'=>$replacement->interest,'fees_outstanding'=>$replacement->fees,
                'penalties_outstanding'=>'0.00','total_outstanding'=>$replacement->total_repayment,'total_paid'=>'0.00',
                'overdue_principal'=>'0.00','overdue_interest'=>'0.00','overdue_fees'=>'0.00','overdue_penalties'=>'0.00','total_overdue'=>'0.00',
                'days_overdue'=>0,'arrears_bucket'=>null,'arrears_updated_at'=>now(),'last_payment_at'=>null,'completed_at'=>null,'updated_by'=>$user->id,
            ]);
            LoanStatusHistory::create([
                'loan_id'=>$replacement->id,'from_status'=>'ready_for_disbursement','to_status'=>'active',
                'notes'=>'Activated through top-up '.$number.'; net cash '.$this->money->fromMinor($netCash).'.','changed_by'=>$user->id,'changed_at'=>now(),
            ]);

            CustomerStatementEntry::create([
                'customer_id'=>$previous->customer_id,'loan_id'=>$previous->id,'branch_id'=>$previous->branch_id,'entry_date'=>$date,
                'reference'=>$number,'type'=>'top_up_internal_settlement','description'=>'Internally settled through top-up '.$number,
                'debit'=>'0.00','credit'=>$this->money->fromMinor($previousOutstanding),'running_balance'=>'0.00','related_type'=>LoanTopUp::class,'related_id'=>$topUp->id,
            ]);
            CustomerStatementEntry::create([
                'customer_id'=>$replacement->customer_id,'loan_id'=>$replacement->id,'branch_id'=>$replacement->branch_id,'entry_date'=>$date,
                'reference'=>$number,'type'=>'loan_top_up','description'=>'Top-up loan obligation '.$replacement->loan_number,
                'debit'=>$replacement->total_repayment,'credit'=>'0.00','running_balance'=>$replacement->total_repayment,'related_type'=>LoanTopUp::class,'related_id'=>$topUp->id,
            ]);
            $this->statements->recalculate($previous);
            $this->statements->recalculate($replacement);
            $this->creditScores->afterCommitForLoan($replacement, 'loan_top_up_executed');

            return $topUp->fresh(['previousLoan.customer','newLoan.customer','branch.business','financialAccount','executor','accountTransaction']);
        });
    }

    private function nextNumber(Loan $loan, CarbonInterface $date): string
    {
        $year = (int) $date->format('Y');
        DB::table('number_sequences')->insertOrIgnore(['type'=>'loan_top_up','branch_id'=>$loan->branch_id,'year'=>$year,'current_value'=>0,'created_at'=>now(),'updated_at'=>now()]);
        $row = DB::table('number_sequences')->where('type','loan_top_up')->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()]);
        $loan->loadMissing('branch');
        $tag = strtoupper(preg_replace('/[^A-Z0-9]/','',str_replace('BR-','',$loan->branch->code)) ?: (string) $loan->branch_id);
        return 'TOP-'.$tag.'-'.$year.'-'.str_pad((string) $sequence,6,'0',STR_PAD_LEFT);
    }

    private function formatBasisPoints(int $basisPoints): string
    {
        return intdiv($basisPoints,100).'.'.str_pad((string) ($basisPoints % 100),2,'0',STR_PAD_LEFT);
    }
}
