<?php

namespace App\Services;

use App\Models\Customer;
use App\Models\CustomerCreditScoreHistory;
use App\Models\CustomerRiskProfile;
use App\Models\Loan;
use App\Services\CreditScore\CreditScoreCalculator;
use App\Services\CreditScore\CreditScoreInputBuilder;
use App\Services\CreditScore\CreditScoreResult;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class CustomerRiskService
{
    public function __construct(
        private readonly CreditScoreCalculator $calculator,
        private readonly CreditScoreInputBuilder $inputBuilder,
    ) {
    }

    public function refresh(Customer $customer, string $trigger = 'manual'): CustomerRiskProfile
    {
        return DB::transaction(function () use ($customer, $trigger): CustomerRiskProfile {
            $result = $this->calculate($customer);

            return $this->persistCurrentProfile($customer, $result, $this->normalizeTrigger($trigger));
        });
    }

    public function refreshWithHistory(Customer $customer, string $trigger): CustomerCreditScoreHistory
    {
        return DB::transaction(function () use ($customer, $trigger): CustomerCreditScoreHistory {
            $normalizedTrigger = $this->normalizeTrigger($trigger);
            $result = $this->calculate($customer);
            $calculatedAt = now();

            $this->persistCurrentProfile($customer, $result, $normalizedTrigger, $calculatedAt);

            return $this->createHistory($customer, $result, $normalizedTrigger, $calculatedAt);
        });
    }

    public function snapshotForLoan(Loan $loan, string $trigger = 'loan_application_submitted'): CustomerCreditScoreHistory
    {
        return DB::transaction(function () use ($loan, $trigger): CustomerCreditScoreHistory {
            // Application snapshots are immutable, so always score the latest persisted
            // customer state at the exact snapshot boundary instead of trusting a relation
            // that may have been loaded earlier in the request or workflow.
            $customer = Customer::query()
                ->whereKey($loan->customer_id)
                ->where('branch_id', $loan->branch_id)
                ->first();

            if (! $customer) {
                throw (new ModelNotFoundException())->setModel(Customer::class, [$loan->customer_id]);
            }

            $result = $this->calculate($customer, $loan);

            return $this->createHistory(
                $customer,
                $result,
                $this->normalizeTrigger($trigger),
                now(),
            );
        });
    }

    public function previewForLoan(Customer $customer, Loan $loan): CreditScoreResult
    {
        return $this->calculate($customer, $loan);
    }

    private function calculate(Customer $customer, ?Loan $proposedLoan = null): CreditScoreResult
    {
        $this->assertCustomerAccessible($customer);

        return $this->calculator->calculate($this->inputBuilder->build($customer, $proposedLoan));
    }

    private function persistCurrentProfile(
        Customer $customer,
        CreditScoreResult $result,
        string $trigger,
        $calculatedAt = null,
    ): CustomerRiskProfile {
        return CustomerRiskProfile::updateOrCreate(
            ['customer_id' => $customer->getKey()],
            [
                'risk_level' => $result->riskLevel,
                'score' => $result->creditScore,
                'score_band' => $result->band,
                'score_version' => $result->scoreVersion,
                'confidence' => $result->confidence,
                'thin_file' => $result->thinFile,
                'manual_review_required' => $result->manualReviewRequired,
                'last_trigger' => $trigger,
                'factors' => $this->factors($result),
                'explanation' => $result->explanation,
                'calculated_at' => $calculatedAt ?? now(),
            ],
        );
    }

    private function createHistory(
        Customer $customer,
        CreditScoreResult $result,
        string $trigger,
        $calculatedAt,
    ): CustomerCreditScoreHistory {
        return CustomerCreditScoreHistory::create([
            'customer_id' => $customer->getKey(),
            'branch_id' => $customer->branch_id,
            'score' => $result->creditScore,
            'score_band' => $result->band,
            'risk_level' => $result->riskLevel,
            'confidence' => $result->confidence,
            'thin_file' => $result->thinFile,
            'manual_review_required' => $result->manualReviewRequired,
            'score_version' => $result->scoreVersion,
            'trigger' => $trigger,
            'component_scores' => $this->componentScores($result),
            'factors' => $this->factors($result),
            'explanation' => $result->explanation,
            'calculated_at' => $calculatedAt,
            'created_by' => Auth::id(),
        ]);
    }

    private function componentScores(CreditScoreResult $result): array
    {
        return collect($result->components)
            ->map(fn ($component): array => [
                'normalized_score' => round($component->normalizedScore, 2),
                'available' => $component->available,
            ])
            ->all();
    }

    private function factors(CreditScoreResult $result): array
    {
        $data = $result->toArray();

        return [
            'score_version' => $data['score_version'],
            'normalized_score' => $data['normalized_score'],
            'band' => $data['band'],
            'band_label' => $data['band_label'],
            'risk_level' => $data['risk_level'],
            'confidence_level' => $data['confidence_level'],
            'thin_file' => $data['thin_file'],
            'components' => $data['components'],
            'overlays' => $data['overlays'],
        ];
    }

    private function normalizeTrigger(string $trigger): string
    {
        $trigger = trim($trigger);

        return Str::limit($trigger !== '' ? $trigger : 'manual', 80, '');
    }

    private function assertCustomerAccessible(Customer $customer): void
    {
        $accessible = Customer::query()
            ->whereKey($customer->getKey())
            ->where('branch_id', $customer->branch_id)
            ->exists();

        if (! $accessible) {
            throw (new ModelNotFoundException())->setModel(Customer::class, [$customer->getKey()]);
        }
    }
}
