<?php

namespace App\Services;

use App\Models\CustomerStatementEntry;
use App\Models\Loan;
use App\Models\LoanInstallment;
use App\Models\LoanRestructuring;
use App\Models\LoanStatusHistory;
use App\Models\User;
use App\Services\Authorization\FinancialMakerCheckerService;
use App\Services\CreditScore\CreditScoreRefreshCoordinator;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;

class LoanRestructuringService
{
    public function __construct(
        private MoneyMath $money,
        private OverdueLoanService $overdue,
        private FinancialMakerCheckerService $makerChecker,
        private CreditScoreRefreshCoordinator $creditScores,
    ) {}

    /** @return array{previous_schedule:list<array<string,mixed>>,new_schedule:list<array<string,mixed>>,scheduled_outstanding:string,penalties:string,total_outstanding:string} */
    public function preview(Loan $loan, string $scheduleText, User $user): array
    {
        if (! $user->canAccessBranch($loan->branch_id)) throw ValidationException::withMessages(['loan'=>'You do not have access to this loan branch.']);
        Gate::forUser($user)->authorize('restructure',$loan);
        if ($loan->status !== 'active') throw ValidationException::withMessages(['loan'=>'Only an active loan can be restructured.']);
        $loan->loadMissing('installments');
        [$components,$scheduled,$total] = $this->components($loan);
        $newSchedule = $this->buildSchedule($scheduleText,$components,$scheduled,CarbonImmutable::today());
        return [
            'previous_schedule'=>$this->snapshot($loan->installments),'new_schedule'=>$newSchedule,
            'scheduled_outstanding'=>$this->money->fromMinor($scheduled),'penalties'=>$this->money->fromMinor($components['penalties']),
            'total_outstanding'=>$this->money->fromMinor($total),
        ];
    }

    public function request(Loan $loan, string $scheduleText, string $reason, User $user): LoanRestructuring
    {
        return DB::transaction(function () use ($loan,$scheduleText,$reason,$user): LoanRestructuring {
            $locked = Loan::query()->lockForUpdate()->findOrFail($loan->id);
            if (! $user->canAccessBranch($locked->branch_id)) throw ValidationException::withMessages(['loan'=>'You do not have access to this loan branch.']);
            Gate::forUser($user)->authorize('restructure',$locked);
            if ($locked->status !== 'active') throw ValidationException::withMessages(['loan'=>'Only an active loan can be restructured.']);
            if (LoanRestructuring::query()->where('loan_id',$locked->id)->where('status','pending')->exists()) {
                throw ValidationException::withMessages(['loan'=>'This loan already has a pending restructuring request.']);
            }
            if ($locked->earlySettlement()->exists()) throw ValidationException::withMessages(['loan'=>'An early-settled loan cannot be restructured.']);
            if ($locked->topUpReplacementLoans()->where('status','!=','rejected')->exists()) {
                throw ValidationException::withMessages(['loan'=>'Resolve the existing top-up replacement application before restructuring.']);
            }

            $installments = $locked->installments()->lockForUpdate()->get();
            if ($installments->isEmpty()) throw ValidationException::withMessages(['loan'=>'This loan has no active repayment schedule to restructure.']);
            [$components,$scheduled,$total] = $this->components($locked);
            if ($scheduled <= 0) throw ValidationException::withMessages(['loan'=>'This loan has no scheduled principal, interest or fees left to restructure.']);
            $previous = $this->snapshot($installments);
            $new = $this->buildSchedule($scheduleText,$components,$scheduled,CarbonImmutable::today());
            $version = max(1,(int)($locked->current_schedule_version ?? 1));
            $number = $this->nextNumber($locked);

            return LoanRestructuring::create([
                'restructuring_number'=>$number,'loan_id'=>$locked->id,'branch_id'=>$locked->branch_id,
                'previous_schedule_version'=>$version,'new_schedule_version'=>$version+1,
                'principal_outstanding'=>$this->money->fromMinor($components['principal']),'interest_outstanding'=>$this->money->fromMinor($components['interest']),
                'fees_outstanding'=>$this->money->fromMinor($components['fees']),'penalties_outstanding'=>$this->money->fromMinor($components['penalties']),
                'total_outstanding'=>$this->money->fromMinor($total),'previous_schedule'=>$previous,'new_schedule'=>$new,
                'reason'=>trim($reason),'status'=>'pending','requested_by'=>$user->id,'requested_at'=>now(),
            ]);
        });
    }

    public function approve(LoanRestructuring $restructuring, User $user, ?string $comments = null): LoanRestructuring
    {
        return DB::transaction(function () use ($restructuring,$user,$comments): LoanRestructuring {
            $record = LoanRestructuring::query()->with('branch.business')->lockForUpdate()->findOrFail($restructuring->id);
            if (! $user->canAccessBranch($record->branch_id)) throw ValidationException::withMessages(['restructuring'=>'You do not have access to this branch.']);
            Gate::forUser($user)->authorize('decide',$record);
            $this->makerChecker->assertRestructuringDecisionSeparated($record,$user);
            if ($record->status !== 'pending') throw ValidationException::withMessages(['restructuring'=>'Only a pending restructuring request can be approved.']);
            $loan = Loan::query()->lockForUpdate()->findOrFail($record->loan_id);
            if ($loan->status !== 'active') throw ValidationException::withMessages(['loan'=>'The loan is no longer active.']);
            if ((int)$loan->current_schedule_version !== (int)$record->previous_schedule_version) {
                throw ValidationException::withMessages(['restructuring'=>'The repayment schedule changed after this proposal. Create a new restructuring request.']);
            }

            [$components,, $total] = $this->components($loan);
            $expected = [
                'principal'=>$this->money->toMinor((string)$record->principal_outstanding),'interest'=>$this->money->toMinor((string)$record->interest_outstanding),
                'fees'=>$this->money->toMinor((string)$record->fees_outstanding),'penalties'=>$this->money->toMinor((string)$record->penalties_outstanding),
            ];
            if ($components !== $expected || $total !== $this->money->toMinor((string)$record->total_outstanding)) {
                throw ValidationException::withMessages(['restructuring'=>'The loan balance changed after this proposal. Create a new restructuring request using the current balance.']);
            }

            $active = $loan->installments()->lockForUpdate()->get();
            if ($this->canonicalSnapshot($this->snapshot($active)) !== $this->canonicalSnapshot($record->previous_schedule ?? [])) {
                throw ValidationException::withMessages(['restructuring'=>'The active repayment schedule changed after this proposal. Create a new restructuring request.']);
            }
            $newSchedule = $record->new_schedule;
            $today = CarbonImmutable::today();
            foreach ($newSchedule as $row) {
                if (! CarbonImmutable::parse($row['due_date'])->startOfDay()->greaterThan($today)) {
                    throw ValidationException::withMessages(['restructuring'=>'A proposed repayment date is no longer in the future. Update the restructuring request.']);
                }
            }

            LoanInstallment::query()->where('loan_id',$loan->id)->where('is_active',true)->update(['is_active'=>false,'updated_at'=>now()]);
            foreach ($newSchedule as $row) {
                LoanInstallment::create([
                    'loan_id'=>$loan->id,'schedule_version'=>$record->new_schedule_version,'is_active'=>true,'loan_restructuring_id'=>$record->id,
                    'installment_number'=>$row['installment_number'],'principal'=>$row['principal'],'interest'=>$row['interest'],'fees'=>$row['fees'],
                    'amount_due'=>$row['amount_due'],'due_date'=>$row['due_date'],'principal_paid'=>'0.00','interest_paid'=>'0.00','fees_paid'=>'0.00',
                    'penalty_due'=>$row['penalty_due'],'penalty_paid'=>'0.00','amount_paid'=>'0.00','remaining'=>$row['remaining'],'status'=>'upcoming',
                ]);
            }

            $loan->update(['current_schedule_version'=>$record->new_schedule_version,'schedule_activated_at'=>now(),'updated_by'=>$user->id]);
            $record->update(['status'=>'approved','approved_by'=>$user->id,'approved_at'=>now(),'decision_comments'=>$comments]);
            LoanStatusHistory::create([
                'loan_id'=>$loan->id,'from_status'=>'active','to_status'=>'active','notes'=>'Repayment schedule restructured under '.$record->restructuring_number.'.',
                'changed_by'=>$user->id,'changed_at'=>now(),
            ]);
            CustomerStatementEntry::create([
                'customer_id'=>$loan->customer_id,'loan_id'=>$loan->id,'branch_id'=>$loan->branch_id,'entry_date'=>now(),
                'reference'=>$record->restructuring_number,'type'=>'loan_restructuring','description'=>'Repayment schedule restructured: '.$record->reason,
                'debit'=>'0.00','credit'=>'0.00','running_balance'=>$loan->total_outstanding,'related_type'=>LoanRestructuring::class,'related_id'=>$record->id,
            ]);
            $this->overdue->refreshLoan($loan->fresh(), now());
            $this->creditScores->afterCommitForLoan($loan, 'loan_restructured');

            return $record->fresh(['loan.customer','branch','requester','approver','documents.uploader']);
        });
    }

    public function reject(LoanRestructuring $restructuring, User $user, string $comments): LoanRestructuring
    {
        return DB::transaction(function () use ($restructuring,$user,$comments): LoanRestructuring {
            $record = LoanRestructuring::query()->with('branch.business')->lockForUpdate()->findOrFail($restructuring->id);
            if (! $user->canAccessBranch($record->branch_id)) throw ValidationException::withMessages(['restructuring'=>'You do not have access to this branch.']);
            Gate::forUser($user)->authorize('decide',$record);
            $this->makerChecker->assertRestructuringDecisionSeparated($record,$user);
            if ($record->status !== 'pending') throw ValidationException::withMessages(['restructuring'=>'Only a pending restructuring request can be rejected.']);
            $record->update(['status'=>'rejected','rejected_by'=>$user->id,'rejected_at'=>now(),'decision_comments'=>trim($comments)]);
            return $record->fresh(['loan.customer','branch','requester','rejecter','documents.uploader']);
        });
    }

    /** @return array{0:array{principal:int,interest:int,fees:int,penalties:int},1:int,2: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')),
        ];
        $scheduled = $components['principal']+$components['interest']+$components['fees'];
        $total = $scheduled+$components['penalties'];
        if ($total <= 0 || $total !== $this->money->toMinor((string)($loan->total_outstanding ?? '0'))) {
            throw ValidationException::withMessages(['loan'=>'The current loan outstanding components do not reconcile.']);
        }
        return [$components,$scheduled,$total];
    }

    /** @return list<array<string,mixed>> */
    private function buildSchedule(string $text, array $components, int $scheduledTotal, CarbonImmutable $today): array
    {
        $lines = collect(preg_split('/\r\n|\r|\n/',trim($text)))->map(fn($line)=>trim((string)$line))->filter()->values();
        if ($lines->isEmpty()) throw ValidationException::withMessages(['new_schedule'=>'Enter at least one new repayment line as YYYY-MM-DD | amount.']);
        if ($lines->count() > 365) throw ValidationException::withMessages(['new_schedule'=>'A restructuring schedule cannot exceed 365 installments.']);

        $rows = []; $amountTotal = 0; $lastDate = null;
        foreach ($lines as $index=>$line) {
            $parts = array_map('trim',preg_split('/\|/', $line, 2));
            if (count($parts)!==2 || !preg_match('/^\d{4}-\d{2}-\d{2}$/',$parts[0])) {
                throw ValidationException::withMessages(['new_schedule'=>'Each line must use YYYY-MM-DD | amount. Invalid line '.($index+1).'.']);
            }
            try { $date = CarbonImmutable::createFromFormat('Y-m-d',$parts[0])->startOfDay(); }
            catch (\Throwable) { throw ValidationException::withMessages(['new_schedule'=>'Invalid repayment date on line '.($index+1).'.']); }
            if ($date->format('Y-m-d') !== $parts[0] || ! $date->greaterThan($today)) {
                throw ValidationException::withMessages(['new_schedule'=>'Every new repayment date must be a valid future date.']);
            }
            if ($lastDate && ! $date->greaterThan($lastDate)) throw ValidationException::withMessages(['new_schedule'=>'New repayment dates must be strictly increasing.']);
            try { $amount = $this->money->toMinor($parts[1]); }
            catch (\Throwable) { throw ValidationException::withMessages(['new_schedule'=>'Invalid installment amount on line '.($index+1).'.']); }
            if ($amount <= 0) throw ValidationException::withMessages(['new_schedule'=>'Every installment amount must be greater than zero.']);
            $rows[]=['installment_number'=>$index+1,'due_date'=>$date->toDateString(),'amount_minor'=>$amount];
            $amountTotal += $amount; $lastDate = $date;
        }
        if ($amountTotal !== $scheduledTotal) {
            throw ValidationException::withMessages(['new_schedule'=>'New installment amounts must total exactly '.$this->money->fromMinor($scheduledTotal).'.']);
        }

        $remaining = ['fees'=>$components['fees'],'interest'=>$components['interest'],'principal'=>$components['principal']];
        $result = [];
        foreach ($rows as $index=>$row) {
            $capacity = $row['amount_minor']; $allocated=['principal'=>0,'interest'=>0,'fees'=>0];
            foreach (['fees','interest','principal'] as $component) {
                $take = min($capacity,$remaining[$component]);
                $allocated[$component] += $take; $remaining[$component] -= $take; $capacity -= $take;
            }
            if ($capacity !== 0) throw ValidationException::withMessages(['new_schedule'=>'The proposed schedule could not be reconciled to the current loan components.']);
            $penalty = $index===0 ? $components['penalties'] : 0;
            $result[] = [
                'installment_number'=>$row['installment_number'],'principal'=>$this->money->fromMinor($allocated['principal']),
                'interest'=>$this->money->fromMinor($allocated['interest']),'fees'=>$this->money->fromMinor($allocated['fees']),
                'amount_due'=>$this->money->fromMinor($row['amount_minor']),'penalty_due'=>$this->money->fromMinor($penalty),
                'remaining'=>$this->money->fromMinor($row['amount_minor']+$penalty),'due_date'=>$row['due_date'],'status'=>'upcoming',
            ];
        }
        if (array_sum($remaining)!==0) throw ValidationException::withMessages(['new_schedule'=>'The proposed schedule does not fully allocate the current loan balance.']);
        return $result;
    }

    /** @return list<array<string,mixed>> */
    private function snapshot($installments): array
    {
        return collect($installments)->map(fn(LoanInstallment $item): array=>[
            'id'=>$item->id,'schedule_version'=>(int)$item->schedule_version,'installment_number'=>(int)$item->installment_number,
            'principal'=>(string)$item->principal,'interest'=>(string)$item->interest,'fees'=>(string)$item->fees,'amount_due'=>(string)$item->amount_due,
            'due_date'=>$item->due_date->format('Y-m-d'),'principal_paid'=>(string)$item->principal_paid,'interest_paid'=>(string)$item->interest_paid,
            'fees_paid'=>(string)$item->fees_paid,'penalty_due'=>(string)$item->penalty_due,'penalty_paid'=>(string)$item->penalty_paid,
            'amount_paid'=>(string)$item->amount_paid,'remaining'=>(string)$item->remaining,'status'=>$item->status,
        ])->values()->all();
    }

    /**
     * Normalize a stored schedule before equality checks. Native MySQL JSON storage may
     * reorder object keys, while SQLite JSON text preserves insertion order. The schedule
     * guard must compare business values, not database-specific JSON key ordering.
     *
     * @param array<int, array<string, mixed>> $schedule
     * @return list<array<string, mixed>>
     */
    private function canonicalSnapshot(array $schedule): array
    {
        $money = fn (mixed $value): string => $this->money->fromMinor($this->money->toMinor((string) ($value ?? '0')));

        return collect($schedule)->map(fn (array $row): array => [
            'id'=>(int)($row['id'] ?? 0),
            'schedule_version'=>(int)($row['schedule_version'] ?? 0),
            'installment_number'=>(int)($row['installment_number'] ?? 0),
            'principal'=>$money($row['principal'] ?? '0'),
            'interest'=>$money($row['interest'] ?? '0'),
            'fees'=>$money($row['fees'] ?? '0'),
            'amount_due'=>$money($row['amount_due'] ?? '0'),
            'due_date'=>CarbonImmutable::parse((string)($row['due_date'] ?? '1900-01-01'))->format('Y-m-d'),
            'principal_paid'=>$money($row['principal_paid'] ?? '0'),
            'interest_paid'=>$money($row['interest_paid'] ?? '0'),
            'fees_paid'=>$money($row['fees_paid'] ?? '0'),
            'penalty_due'=>$money($row['penalty_due'] ?? '0'),
            'penalty_paid'=>$money($row['penalty_paid'] ?? '0'),
            'amount_paid'=>$money($row['amount_paid'] ?? '0'),
            'remaining'=>$money($row['remaining'] ?? '0'),
            'status'=>(string)($row['status'] ?? ''),
        ])->values()->all();
    }

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