<?php

namespace App\Services\CreditScore;

use App\Services\CreditScore\Components\AffordabilityComponent;
use App\Services\CreditScore\Components\CollateralComponent;
use App\Services\CreditScore\Components\ConductFlagsComponent;
use App\Services\CreditScore\Components\LoanHistoryComponent;
use App\Services\CreditScore\Components\ProfileCompletenessComponent;
use App\Services\CreditScore\Components\RepaymentBehaviourComponent;
use InvalidArgumentException;

class CreditScoreCalculator
{
    private array $configuration;

    public function __construct(?array $configuration = null)
    {
        $this->configuration = $configuration ?? config('credit_score', []);
        $this->assertConfiguration();
    }

    public function calculate(array $input): CreditScoreResult
    {
        $components = [
            'repayment_behaviour' => (new RepaymentBehaviourComponent($this->configuration))->calculate($input['repayment'] ?? []),
            'loan_history' => (new LoanHistoryComponent($this->configuration))->calculate($input['loan_history'] ?? []),
            'affordability' => (new AffordabilityComponent($this->configuration))->calculate($input['affordability'] ?? []),
            'collateral' => (new CollateralComponent($this->configuration))->calculate($input['collateral'] ?? []),
            'profile_completeness' => (new ProfileCompletenessComponent())->calculate($input['profile'] ?? []),
            'conduct_flags' => (new ConductFlagsComponent())->calculate($input['conduct'] ?? []),
        ];

        $weightedScore = 0.0;
        foreach ($components as $name => $component) {
            $weightedScore += $component->normalizedScore * ((float) $this->configuration['weights'][$name] / 100);
        }
        $normalizedScore = $this->clamp($weightedScore, 0, 100);
        $creditScore = $this->mapCreditScore($normalizedScore);
        [$band, $bandConfig] = $this->resolveBand($creditScore);

        $riskLevel = (string) ($bandConfig['risk_level'] ?? 'unrated');
        $thinFile = $this->isThinFile($input);
        $confidence = $this->calculateConfidence($components, $thinFile);
        $manualReviewRequired = false;
        $overlays = [];

        $conduct = $input['conduct'] ?? [];
        if ((bool) ($conduct['active_blacklist'] ?? false)) {
            $rule = $this->configuration['overlays']['active_blacklist'] ?? [];
            $manualReviewRequired = $manualReviewRequired || (bool) ($rule['manual_review_required'] ?? true);
            $riskLevel = $this->floorRisk($riskLevel, (string) ($rule['minimum_risk_level'] ?? 'moderate'));
            $overlays[] = ['key' => 'active_blacklist', 'message' => 'Active blacklist requires manual credit review.'];
        }

        $unresolvedWriteOffs = max(0, (int) (($input['loan_history']['unresolved_write_offs'] ?? 0)));
        if ($unresolvedWriteOffs > 0) {
            $rule = $this->configuration['overlays']['unresolved_write_off'] ?? [];
            $manualReviewRequired = $manualReviewRequired || (bool) ($rule['manual_review_required'] ?? true);
            $riskLevel = $this->floorRisk($riskLevel, (string) ($rule['minimum_risk_level'] ?? 'high'));
            $overlays[] = ['key' => 'unresolved_write_off', 'message' => 'Unresolved write-off history requires manual credit review.'];
        }

        $currentDaysOverdue = max(0, (int) ($input['repayment']['current_days_overdue'] ?? 0));
        $arrearsRule = $this->configuration['overlays']['serious_current_arrears'] ?? [];
        $arrearsThreshold = (int) ($arrearsRule['days_overdue'] ?? 90);
        if ($currentDaysOverdue >= $arrearsThreshold) {
            $manualReviewRequired = $manualReviewRequired || (bool) ($arrearsRule['manual_review_required'] ?? true);
            $riskLevel = $this->floorRisk($riskLevel, (string) ($arrearsRule['minimum_risk_level'] ?? 'high'));
            $overlays[] = ['key' => 'serious_current_arrears', 'message' => "Current arrears of {$currentDaysOverdue} days require manual credit review."];
        }

        if ((bool) ($components['affordability']->factors['missing_income'] ?? false)) {
            $rule = $this->configuration['overlays']['missing_income_for_affordability'] ?? [];
            $manualReviewRequired = $manualReviewRequired || (bool) ($rule['manual_review_required'] ?? true);
            $overlays[] = ['key' => 'missing_income_for_affordability', 'message' => 'Affordability cannot be verified because monthly income is unavailable.'];
        }

        return new CreditScoreResult(
            normalizedScore: $normalizedScore,
            creditScore: $creditScore,
            band: $band,
            bandLabel: (string) ($bandConfig['label'] ?? ucfirst(str_replace('_', ' ', $band))),
            riskLevel: $riskLevel,
            confidence: $confidence,
            confidenceLevel: $this->confidenceLevel($confidence),
            thinFile: $thinFile,
            manualReviewRequired: $manualReviewRequired,
            scoreVersion: (string) $this->configuration['version'],
            components: $components,
            overlays: $overlays,
            explanation: $this->buildExplanation($creditScore, (string) ($bandConfig['label'] ?? $band), $components, $thinFile, $manualReviewRequired),
        );
    }

    private function calculateConfidence(array $components, bool $thinFile): float
    {
        $confidence = 100.0;

        if (! $components['repayment_behaviour']->available) {
            $confidence -= 20;
        }
        if (! $components['loan_history']->available) {
            $confidence -= 15;
        }
        if (! $components['affordability']->available) {
            $confidence -= 25;
        }
        if (($components['collateral']->factors['required'] ?? false) && ! $components['collateral']->available) {
            $confidence -= 5;
        }
        if ($components['profile_completeness']->normalizedScore < 80) {
            $confidence -= 5;
        }

        if ($thinFile) {
            $confidence = min($confidence, (float) ($this->configuration['confidence']['thin_file_cap'] ?? 55));
        }
        if ((bool) ($components['affordability']->factors['missing_income'] ?? false)) {
            $confidence = min($confidence, (float) ($this->configuration['confidence']['missing_income_cap'] ?? 45));
        }

        return $this->clamp($confidence, 0, 100);
    }

    private function confidenceLevel(float $confidence): string
    {
        if ($confidence >= (float) ($this->configuration['confidence']['high_minimum'] ?? 80)) {
            return 'high';
        }
        if ($confidence >= (float) ($this->configuration['confidence']['medium_minimum'] ?? 60)) {
            return 'medium';
        }

        return 'low';
    }

    private function isThinFile(array $input): bool
    {
        $completedInstallments = max(0, (int) ($input['repayment']['completed_installments'] ?? $input['repayment']['total_installments'] ?? 0));
        $completedLoans = max(0, (int) ($input['loan_history']['completed_loans'] ?? 0));

        return $completedInstallments < (int) ($this->configuration['thin_file']['minimum_completed_installments'] ?? 3)
            || $completedLoans < (int) ($this->configuration['thin_file']['minimum_completed_loans'] ?? 1);
    }

    private function mapCreditScore(float $normalizedScore): int
    {
        $minimum = (int) $this->configuration['score']['minimum'];
        $maximum = (int) $this->configuration['score']['maximum'];

        return (int) round($minimum + (($normalizedScore / 100) * ($maximum - $minimum)));
    }

    private function resolveBand(int $creditScore): array
    {
        foreach ($this->configuration['bands'] as $band => $config) {
            if ($creditScore >= (int) $config['minimum'] && $creditScore <= (int) $config['maximum']) {
                return [$band, $config];
            }
        }

        throw new InvalidArgumentException("No credit score band covers score {$creditScore}.");
    }

    private function floorRisk(string $currentRisk, string $minimumRisk): string
    {
        $severity = ['low' => 0, 'moderate' => 1, 'high' => 2, 'very_high' => 3, 'unrated' => -1];
        $current = $severity[$currentRisk] ?? -1;
        $floor = $severity[$minimumRisk] ?? $current;

        return $floor > $current ? $minimumRisk : $currentRisk;
    }

    private function buildExplanation(int $score, string $bandLabel, array $components, bool $thinFile, bool $manualReviewRequired): string
    {
        $parts = [];
        if ($thinFile) {
            $parts[] = 'Thin-file credit profile; confidence is limited because repayment history is incomplete.';
        }

        $parts[] = "Credit score {$score} ({$bandLabel}).";

        $available = array_filter($components, fn (ComponentResult $component): bool => $component->available);
        if ($available !== []) {
            $strongest = collect($available)->sortByDesc(fn (ComponentResult $component): float => $component->normalizedScore)->first();
            $weakest = collect($available)->sortBy(fn (ComponentResult $component): float => $component->normalizedScore)->first();

            if ($strongest instanceof ComponentResult && $strongest->positiveFactors !== []) {
                $parts[] = $strongest->positiveFactors[0];
            }
            if ($weakest instanceof ComponentResult && $weakest->negativeFactors !== []) {
                $parts[] = $weakest->negativeFactors[0];
            }
        }

        if ($manualReviewRequired) {
            $parts[] = 'One or more control overlays require manual review; the score does not automatically approve or reject a loan.';
        }

        return implode(' ', $parts);
    }

    private function assertConfiguration(): void
    {
        $requiredComponents = [
            'repayment_behaviour',
            'loan_history',
            'affordability',
            'collateral',
            'profile_completeness',
            'conduct_flags',
        ];
        $weights = $this->configuration['weights'] ?? [];

        foreach ($requiredComponents as $component) {
            if (! array_key_exists($component, $weights)) {
                throw new InvalidArgumentException("Missing credit score weight for {$component}.");
            }
        }
        if ((float) array_sum($weights) !== 100.0) {
            throw new InvalidArgumentException('Credit score component weights must total 100.');
        }

        $minimum = (int) ($this->configuration['score']['minimum'] ?? 0);
        $maximum = (int) ($this->configuration['score']['maximum'] ?? 0);
        if ($minimum < 0 || $maximum <= $minimum) {
            throw new InvalidArgumentException('Credit score minimum and maximum are invalid.');
        }
        if (empty($this->configuration['version']) || empty($this->configuration['bands'])) {
            throw new InvalidArgumentException('Credit score version and bands are required.');
        }
    }

    private function clamp(float $value, float $minimum, float $maximum): float
    {
        return max($minimum, min($maximum, $value));
    }
}
