<?php

namespace App\Services;

use App\Models\Collateral;
use App\Models\CollateralMovement;
use App\Models\CustomerStatementEntry;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\LoanEarlySettlement;
use App\Models\LoanRestructuring;
use App\Models\LoanStatusHistory;
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 LoanEarlySettlementService
{
    public function __construct(
        private MoneyMath $money,
        private LoanPenaltyService $penalties,
        private LoanEarlySettlementAccountingService $accounting,
        private CustomerStatementBalanceService $statements,
        private CreditScoreRefreshCoordinator $creditScores,
    ) {}

    /** @return array{principal:string,interest:string,fees:string,penalties:string,subtotal:string,settlement_adjustment:string,final_amount:string} */
    public function quote(Loan $loan, CarbonInterface $date, string $adjustment, User $user): array
    {
        return DB::transaction(function () use ($loan,$date,$adjustment,$user): array {
            $locked = Loan::query()->lockForUpdate()->findOrFail($loan->id);
            $this->assertEligible($locked,$user);
            $this->penalties->assess($locked, CarbonImmutable::instance($date)->startOfDay());
            $locked->refresh();
            [$components,$subtotal] = $this->components($locked);
            $adjustmentMinor = $this->validateAdjustment($adjustment,$components,$subtotal,$user);
            return $this->formattedQuote($components,$subtotal,$adjustmentMinor);
        });
    }

    public function execute(Loan $loan, FinancialAccount $account, CarbonInterface $date, string $adjustment, ?string $adjustmentReason, ?string $reference, ?string $notes, User $user): LoanEarlySettlement
    {
        return DB::transaction(function () use ($loan,$account,$date,$adjustment,$adjustmentReason,$reference,$notes,$user): LoanEarlySettlement {
            $locked = Loan::query()->with(['branch','customer'])->lockForUpdate()->findOrFail($loan->id);
            $this->assertEligible($locked,$user);
            if ($account->branch_id !== $locked->branch_id) {
                throw ValidationException::withMessages(['financial_account_id'=>'The settlement receiving account must belong to the same branch as the loan.']);
            }
            if (LoanEarlySettlement::query()->where('loan_id',$locked->id)->exists()) {
                throw ValidationException::withMessages(['loan'=>'This loan has already been early-settled.']);
            }
            if (LoanRestructuring::query()->where('loan_id',$locked->id)->where('status','pending')->exists()) {
                throw ValidationException::withMessages(['loan'=>'Resolve the pending restructuring request before early settlement.']);
            }
            if ($locked->topUpReplacementLoans()->where('status','!=','rejected')->exists()) {
                throw ValidationException::withMessages(['loan'=>'Resolve the existing top-up replacement application before early settlement.']);
            }

            $this->penalties->assess($locked, CarbonImmutable::instance($date)->startOfDay());
            $locked->refresh();
            [$components,$subtotal] = $this->components($locked);
            $adjustmentMinor = $this->validateAdjustment($adjustment,$components,$subtotal,$user);
            if ($adjustmentMinor !== 0 && trim((string) $adjustmentReason) === '') {
                throw ValidationException::withMessages(['adjustment_reason'=>'A reason is required for a non-zero settlement adjustment.']);
            }
            $finalMinor = $subtotal + $adjustmentMinor;
            $number = $this->nextNumber($locked,$date);
            $effectiveReference = $reference ?: $number;

            $settlement = LoanEarlySettlement::create([
                'settlement_number'=>$number,'loan_id'=>$locked->id,'customer_id'=>$locked->customer_id,'branch_id'=>$locked->branch_id,
                'financial_account_id'=>$account->id,'principal_amount'=>$this->money->fromMinor($components['principal']),
                'interest_amount'=>$this->money->fromMinor($components['interest']),'fees_amount'=>$this->money->fromMinor($components['fees']),
                'penalties_amount'=>$this->money->fromMinor($components['penalties']),
                'settlement_adjustment'=>$this->money->fromSignedMinor($adjustmentMinor),'final_amount'=>$this->money->fromMinor($finalMinor),
                'adjustment_reason'=>$adjustmentMinor !== 0 ? trim((string) $adjustmentReason) : null,'settlement_date'=>$date,
                'reference'=>$effectiveReference,'notes'=>$notes,'settled_by'=>$user->id,
            ]);
            $transaction = $this->accounting->post(
                $settlement,$account,collect($components)->map(fn (int $value): string=>$this->money->fromMinor($value))->all(),
                $date,$effectiveReference,$notes,$user
            );
            $settlement->update(['financial_account_transaction_id'=>$transaction->id]);

            foreach ($locked->installments()->lockForUpdate()->get() as $installment) {
                $due = $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($due),'remaining'=>'0.00','status'=>'paid',
                ]);
            }

            $previousStatus = $locked->status;
            $actualPaid = $this->money->toMinor((string) ($locked->total_paid ?? '0')) + $finalMinor;
            $locked->update([
                'status'=>'fully_paid','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($actualPaid),'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'=>$locked->id,'from_status'=>$previousStatus,'to_status'=>'fully_paid',
                'notes'=>'Loan fully paid through early settlement '.$number.'.','changed_by'=>$user->id,'changed_at'=>now(),
            ]);

            if ($adjustmentMinor !== 0) {
                CustomerStatementEntry::create([
                    'customer_id'=>$locked->customer_id,'loan_id'=>$locked->id,'branch_id'=>$locked->branch_id,'entry_date'=>$date,'reference'=>$number,
                    'type'=>'early_settlement_adjustment','description'=>'Early settlement adjustment: '.trim((string) $adjustmentReason),
                    'debit'=>$adjustmentMinor > 0 ? $this->money->fromMinor($adjustmentMinor) : '0.00',
                    'credit'=>$adjustmentMinor < 0 ? $this->money->fromMinor(abs($adjustmentMinor)) : '0.00','running_balance'=>'0.00',
                    'related_type'=>LoanEarlySettlement::class,'related_id'=>$settlement->id,
                ]);
            }
            CustomerStatementEntry::create([
                'customer_id'=>$locked->customer_id,'loan_id'=>$locked->id,'branch_id'=>$locked->branch_id,'entry_date'=>$date,'reference'=>$number,
                'type'=>'loan_early_settlement','description'=>'Early settlement '.$number,'debit'=>'0.00','credit'=>$this->money->fromMinor($finalMinor),
                'running_balance'=>'0.00','related_type'=>LoanEarlySettlement::class,'related_id'=>$settlement->id,
            ]);
            $this->statements->recalculate($locked);

            $collaterals = Collateral::query()->whereIn('id',$locked->collaterals()->pluck('collaterals.id'))->orderBy('id')->lockForUpdate()->get();
            foreach ($collaterals as $collateral) {
                if (in_array($collateral->status,['ready_for_release','released','sold','returned'],true)) continue;
                $from = $collateral->status;
                $collateral->update(['status'=>'ready_for_release','updated_by'=>$user->id]);
                CollateralMovement::create([
                    'collateral_id'=>$collateral->id,'action'=>'loan_settled_ready_for_release','from_status'=>$from,'to_status'=>'ready_for_release',
                    'from_store_room'=>$collateral->store_room,'from_shelf'=>$collateral->shelf,'to_store_room'=>$collateral->store_room,'to_shelf'=>$collateral->shelf,
                    'notes'=>'Loan '.$locked->loan_number.' fully paid through early settlement '.$number.'. Ready for release.',
                    'moved_by'=>$user->id,'moved_at'=>now(),
                ]);
            }
            $this->creditScores->afterCommitForLoan($locked, 'loan_early_settlement');

            return $settlement->fresh(['loan.customer','branch.business','financialAccount','settler','accountTransaction']);
        });
    }

    private function assertEligible(Loan $loan, User $user): void
    {
        if (! $user->canAccessBranch($loan->branch_id)) throw ValidationException::withMessages(['loan'=>'You do not have access to this loan branch.']);
        Gate::forUser($user)->authorize('settle',$loan);
        if ($loan->status !== 'active') throw ValidationException::withMessages(['loan'=>'Only an active loan can be early-settled.']);
    }

    /** @return array{0:array{principal:int,interest:int,fees:int,penalties:int},1:int} */
    private function components(Loan $loan): array
    {
        $components = [
            'principal'=>$this->money->toMinor((string) ($loan->principal_outstanding ?? '0')),
            'interest'=>$this->money->toMinor((string) ($loan->interest_outstanding ?? '0')),
            'fees'=>$this->money->toMinor((string) ($loan->fees_outstanding ?? '0')),
            'penalties'=>$this->money->toMinor((string) ($loan->penalties_outstanding ?? '0')),
        ];
        $subtotal = array_sum($components);
        if ($subtotal <= 0 || $subtotal !== $this->money->toMinor((string) ($loan->total_outstanding ?? '0'))) {
            throw ValidationException::withMessages(['loan'=>'The current loan outstanding components do not reconcile.']);
        }
        return [$components,$subtotal];
    }

    private function validateAdjustment(string $adjustment, array $components, int $subtotal, User $user): int
    {
        try {
            $minor = $this->money->toSignedMinor($adjustment === '' ? '0.00' : $adjustment);
        } catch (\InvalidArgumentException $e) {
            throw ValidationException::withMessages(['settlement_adjustment'=>'Settlement adjustment must be a valid fixed-precision amount.']);
        }
        if ($minor !== 0 && ! $user->can('loans.settlement.adjust')) {
            throw ValidationException::withMessages(['settlement_adjustment'=>'You are not authorized to apply a settlement adjustment.']);
        }
        if ($minor < 0 && abs($minor) > ($components['interest'] + $components['fees'] + $components['penalties'])) {
            throw ValidationException::withMessages(['settlement_adjustment'=>'A settlement reduction cannot waive outstanding principal.']);
        }
        if ($subtotal + $minor <= 0) {
            throw ValidationException::withMessages(['settlement_adjustment'=>'Final settlement amount must be greater than zero.']);
        }
        return $minor;
    }

    private function formattedQuote(array $components, int $subtotal, int $adjustment): array
    {
        return [
            'principal'=>$this->money->fromMinor($components['principal']),'interest'=>$this->money->fromMinor($components['interest']),
            'fees'=>$this->money->fromMinor($components['fees']),'penalties'=>$this->money->fromMinor($components['penalties']),
            'subtotal'=>$this->money->fromMinor($subtotal),'settlement_adjustment'=>$this->money->fromSignedMinor($adjustment),
            'final_amount'=>$this->money->fromMinor($subtotal+$adjustment),
        ];
    }

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