<?php

namespace App\Services;

use App\Models\Branch;
use App\Models\Loan;
use App\Models\LoanProduct;
use App\Models\LoanStatusHistory;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;

class LoanApplicationService
{
    public function __construct(private RepaymentScheduleGenerator $scheduleGenerator) {}

    public function create(array $data, LoanProduct $product, User $user): Loan
    {
        return DB::transaction(function () use ($data, $product, $user): Loan {
            $branch = Branch::query()->with('loanProducts:id')->lockForUpdate()->findOrFail($data['branch_id']);
            $this->assertProductAvailableForBranch($branch, $product);
            [$sequence, $year, $loanNumber] = $this->nextNumber($branch);
            $schedule = $this->schedule($product, $data);
            $loan = Loan::create(array_merge($this->snapshot($data, $schedule, $product), [
                'branch_id'=>$branch->id,'customer_id'=>$data['customer_id'],'loan_product_id'=>$product->id,
                'guarantor_id'=>$data['guarantor_id'] ?? null,'sequence_number'=>$sequence,'sequence_year'=>$year,
                'loan_number'=>$loanNumber,'status'=>'draft','created_by'=>$user->id,'updated_by'=>$user->id,
            ]));
            $loan->collaterals()->sync($data['collateral_ids'] ?? []);
            $this->replaceSchedule($loan, $schedule['installments']);
            $this->history($loan, null, 'draft', 'Loan application created.', $user);
            return $loan;
        });
    }

    public function updateDraft(Loan $loan, array $data, LoanProduct $product, User $user): Loan
    {
        if (! $loan->canEditCoreTerms()) throw ValidationException::withMessages(['loan'=>'Core loan terms can only be changed while the application is a draft or correction has been requested.']);
        return DB::transaction(function () use ($loan, $data, $product, $user): Loan {
            $branch = Branch::query()->with('loanProducts:id')->findOrFail($loan->branch_id);
            $this->assertProductAvailableForBranch($branch, $product);
            $schedule = $this->schedule($product, $data);
            $loan->update(array_merge($this->snapshot($data, $schedule, $product), ['loan_product_id'=>$product->id,'guarantor_id'=>$data['guarantor_id'] ?? null,'updated_by'=>$user->id]));
            $loan->collaterals()->sync($data['collateral_ids'] ?? []);
            $loan->installments()->delete();
            $this->replaceSchedule($loan, $schedule['installments']);
            return $loan->fresh();
        });
    }

    public function transition(Loan $loan, string $toStatus, User $user, ?string $notes = null): Loan
    {
        $lateApplicationSubmission = $toStatus === 'application_submitted'
            && in_array($loan->status, ['contract_generated','contract_signed'], true);

        if (! $loan->canTransitionTo($toStatus) && ! $lateApplicationSubmission) {
            throw ValidationException::withMessages(['status'=>"Invalid transition from {$loan->status} to {$toStatus}."]);
        }

        return DB::transaction(function () use ($loan, $toStatus, $user, $notes, $lateApplicationSubmission): Loan {
            if ($lateApplicationSubmission) {
                if (! $loan->submitted_at) {
                    $loan->update(['submitted_at'=>now(),'updated_by'=>$user->id]);
                }

                return $loan->fresh();
            }

            $from = $loan->status;
            $attributes = ['status'=>$toStatus, 'updated_by'=>$user->id];
            if ($toStatus === 'application_submitted' && ! $loan->submitted_at) $attributes['submitted_at'] = now();
            $loan->update($attributes);
            $this->history($loan, $from, $toStatus, $notes, $user);
            return $loan->fresh();
        });
    }

    public function markContractGenerated(Loan $loan, User $user): Loan
    {
        if (! $loan->canGenerateContract()) {
            throw ValidationException::withMessages(['contract'=>'A new contract cannot be generated while this application is under review or after an approval/rejection decision.']);
        }

        return DB::transaction(function () use ($loan, $user): Loan {
            $from = $loan->status;
            $attributes = ['contract_generated_at'=>now(),'contract_generated_by'=>$user->id,'updated_by'=>$user->id];
            if ($loan->canTransitionTo('contract_generated')) $attributes['status'] = 'contract_generated';
            $loan->update($attributes);
            if ($from !== $loan->status) $this->history($loan, $from, $loan->status, 'Loan contract generated.', $user);
            return $loan->fresh();
        });
    }

    public function markContractSigned(Loan $loan, User $user): Loan
    {
        if (! $loan->canUploadSignedContract()) {
            throw ValidationException::withMessages(['status'=>'A signed contract can only be uploaded before approval review, or after a reviewer requests correction, documents, or additional collateral.']);
        }

        return DB::transaction(function () use ($loan, $user): Loan {
            $from = $loan->status;
            $attributes = ['contract_signed_at'=>now(),'contract_signed_uploaded_by'=>$user->id,'updated_by'=>$user->id];
            if ($loan->canTransitionTo('contract_signed')) $attributes['status'] = 'contract_signed';
            $loan->update($attributes);
            if ($from !== $loan->status) $this->history($loan, $from, $loan->status, 'Signed loan contract uploaded.', $user);
            return $loan->fresh();
        });
    }

    private function assertProductAvailableForBranch(Branch $branch, LoanProduct $product): void
    {
        if (! $branch->allowsLoanProduct($product)) {
            throw ValidationException::withMessages([
                'loan_product_id'=>'The selected loan product is not available for this branch.',
            ]);
        }
    }

    private function schedule(LoanProduct $product, array $data): array
    {
        try {
            return $this->scheduleGenerator->generate($product,(string)$data['principal'],CarbonImmutable::parse($data['schedule_start_date']),$data['custom_repayment_dates'] ?? []);
        } catch (InvalidArgumentException $e) {
            throw ValidationException::withMessages(['principal'=>$e->getMessage()]);
        }
    }

    private function snapshot(array $data, array $schedule, LoanProduct $product): array
    {
        $summary = $schedule['summary'];
        $product->loadMissing('business');
        $business = $product->business;

        return [
            'principal'=>$summary['principal'],'interest'=>$summary['interest'],'processing_fee'=>$summary['processing_fee'],
            'processing_fee_payment_mode'=>$summary['processing_fee_payment_mode'] ?? ($product->processing_fee_payment_mode ?: 'financed'),
            'insurance_fee'=>$summary['insurance_fee'],'other_charges'=>$summary['other_charges'],'fees'=>$summary['fees'],
            'total_repayment'=>$summary['total_repayment'],'purpose'=>$data['purpose'],'schedule_start_date'=>$data['schedule_start_date'],
            'custom_repayment_dates'=>$data['custom_repayment_dates'] ?? null,
            'late_payment_penalty_type'=>$product->late_payment_penalty_type,
            'late_payment_penalty_value'=>$product->late_payment_penalty_value,
            'payment_deadline_enabled'=>(bool)$product->payment_deadline_enabled,
            'payment_deadline_time'=>$product->payment_deadline_enabled ? $product->payment_deadline_time : null,
            'deadline_late_penalty_type'=>$business->deadline_late_penalty_type ?: 'fixed',
            'deadline_late_penalty_value'=>$business->deadline_late_penalty_value ?? '0',
            'deadline_late_penalty_settlement'=>$business->deadline_late_penalty_settlement ?: 'add_to_loan',
            'missed_schedule_penalty_type'=>$business->missed_schedule_penalty_type ?: 'fixed',
            'missed_schedule_penalty_value'=>$business->missed_schedule_penalty_value ?? '0',
            'missed_schedule_penalty_settlement'=>$business->missed_schedule_penalty_settlement ?: 'add_to_loan',
        ];
    }

    private function replaceSchedule(Loan $loan, array $installments): void
    {
        foreach ($installments as $installment) {
            $loan->installments()->create([
                'installment_number'=>$installment['number'],'principal'=>$installment['principal'],'interest'=>$installment['interest'],
                'fees'=>$installment['fees'],'amount_due'=>$installment['amount_due'],'due_date'=>$installment['due_date'],
                'amount_paid'=>$installment['amount_paid'],'remaining'=>$installment['remaining'],'status'=>$installment['status'],
            ]);
        }
    }

    private function nextNumber(Branch $branch): array
    {
        $year = (int) now()->format('Y');
        DB::table('number_sequences')->insertOrIgnore(['type'=>'loan','branch_id'=>$branch->id,'year'=>$year,'current_value'=>0,'created_at'=>now(),'updated_at'=>now()]);
        $row = DB::table('number_sequences')->where('type','loan')->where('branch_id',$branch->id)->where('year',$year)->lockForUpdate()->first();
        $highestExisting = (int) Loan::query()->where('branch_id',$branch->id)->where('sequence_year',$year)->max('sequence_number');
        $sequence = max((int) $row->current_value, $highestExisting) + 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-', '', $branch->code)) ?: (string)$branch->id);
        return [$sequence, $year, 'LN-'.$tag.'-'.$year.'-'.str_pad((string)$sequence, 6,'0',STR_PAD_LEFT)];
    }

    private function history(Loan $loan, ?string $from, string $to, ?string $notes, User $user): void
    {
        LoanStatusHistory::create(['loan_id'=>$loan->id,'from_status'=>$from,'to_status'=>$to,'notes'=>$notes,'changed_by'=>$user->id,'changed_at'=>now()]);
    }
}
