<?php

namespace App\Services\CreditScore;

use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class CreditScoreCalibrationReportService
{
    private const APPROVED_STATUSES = [
        'ready_for_disbursement', 'active', 'completed', 'fully_paid', 'closed_through_top_up', 'written_off',
    ];

    private const REPAYMENT_STATUSES = [
        'active', 'completed', 'fully_paid', 'closed_through_top_up', 'written_off',
    ];

    private const CLOSED_STATUSES = ['completed', 'fully_paid', 'closed_through_top_up'];

    public function generate(array $branchIds, ?string $scoreVersion = null): array
    {
        $branchIds = array_values(array_unique(array_filter(array_map('intval', $branchIds), fn (int $id) => $id > 0)));
        if ($branchIds === []) {
            throw ValidationException::withMessages(['branch_ids' => 'At least one accessible branch is required.']);
        }

        $scoreVersion = filled($scoreVersion) ? trim((string) $scoreVersion) : (string) config('credit_score.version', 'v1');
        $allRows = $this->applicationRows($branchIds);
        $rows = $allRows->where('score_version', $scoreVersion)->values();
        $bands = $this->bandDefinitions();
        $bandRanks = $this->bandRanks($bands);
        $summary = $this->metrics($rows);

        $bandPerformance = $bands->map(function (array $band) use ($rows): array {
            return [
                'key' => $band['key'],
                'label' => $band['label'],
                ...$this->metrics($rows->where('application_band', $band['key'])->values()),
            ];
        })->values()->all();

        $branchModels = DB::table('branches')
            ->whereIn('id', $branchIds)
            ->orderBy('name')
            ->get(['id', 'name']);

        $branchPerformance = $branchModels->map(function ($branch) use ($rows): array {
            return [
                'branch_id' => (int) $branch->id,
                'branch_name' => (string) $branch->name,
                ...$this->metrics($rows->where('branch_id', (int) $branch->id)->values()),
            ];
        })->all();

        $versionPerformance = $allRows
            ->groupBy('score_version')
            ->map(function (Collection $versionRows, string $version): array {
                return [
                    'score_version' => $version,
                    ...$this->metrics($versionRows->values()),
                ];
            })
            ->sortByDesc('score_version')
            ->values()
            ->all();

        $migration = [
            'overall' => $this->migrationMetrics($rows, $bandRanks),
            'bands' => $bands->map(function (array $band) use ($rows, $bandRanks): array {
                return [
                    'key' => $band['key'],
                    'label' => $band['label'],
                    ...$this->migrationMetrics($rows->where('application_band', $band['key'])->values(), $bandRanks),
                ];
            })->values()->all(),
        ];

        $monthlyTrend = $rows
            ->groupBy(fn (array $row) => $row['application_calculated_at']->format('Y-m'))
            ->sortKeys()
            ->map(function (Collection $monthRows, string $month): array {
                return [
                    'month' => $month,
                    'label' => CarbonImmutable::createFromFormat('Y-m-d', $month.'-01')->format('M Y'),
                    ...$this->metrics($monthRows->values()),
                ];
            })
            ->values()
            ->slice(-12)
            ->values()
            ->all();

        return [
            'generated_at' => now(),
            'selected_score_version' => $scoreVersion,
            'available_score_versions' => $allRows->pluck('score_version')->filter()->unique()->sort()->values()->all(),
            'summary' => $summary,
            'bands' => $bandPerformance,
            'branches' => $branchPerformance,
            'versions' => $versionPerformance,
            'migration' => $migration,
            'monthly_trend' => $monthlyTrend,
            'drift' => $this->drift($rows, $branchModels, $bands),
            'monitoring' => [
                'serious_arrears_days' => (int) config('credit_score.thresholds.repayment.serious_arrears_days', 30),
                'minimum_sample_size' => (int) config('credit_score.monitoring.minimum_sample_size', 20),
                'recent_window_days' => (int) config('credit_score.monitoring.recent_window_days', 90),
            ],
        ];
    }

    private function applicationRows(array $branchIds): Collection
    {
        $seriousArrearsDays = (int) config('credit_score.thresholds.repayment.serious_arrears_days', 30);

        return DB::table('loans')
            ->join('customer_credit_score_histories as application_score', 'application_score.id', '=', 'loans.application_credit_score_history_id')
            ->join('branches', 'branches.id', '=', 'loans.branch_id')
            ->leftJoin('customer_risk_profiles as current_risk', 'current_risk.customer_id', '=', 'loans.customer_id')
            ->whereIn('loans.branch_id', $branchIds)
            ->orderBy('application_score.calculated_at')
            ->orderBy('loans.id')
            ->get([
                'loans.id as loan_id',
                'loans.branch_id',
                'branches.name as branch_name',
                'loans.status',
                'loans.approved_at',
                'loans.activated_at',
                'loans.completed_at',
                'loans.written_off_at',
                'loans.days_overdue',
                'loans.total_outstanding',
                'loans.total_overdue',
                'application_score.score as application_score',
                'application_score.score_band as application_band',
                'application_score.confidence as application_confidence',
                'application_score.thin_file',
                'application_score.manual_review_required',
                'application_score.score_version',
                'application_score.calculated_at as application_calculated_at',
                'current_risk.score as current_score',
                'current_risk.score_band as current_band',
                'current_risk.score_version as current_score_version',
            ])
            ->map(function ($row) use ($seriousArrearsDays): array {
                $status = (string) $row->status;
                $approved = $row->approved_at !== null || in_array($status, self::APPROVED_STATUSES, true);
                $rejected = $status === 'rejected';
                $observable = $row->activated_at !== null || in_array($status, self::REPAYMENT_STATUSES, true);
                $active = $status === 'active';
                $writtenOff = $status === 'written_off' || $row->written_off_at !== null;
                $outstanding = (float) ($row->total_outstanding ?? 0);
                $overdue = (float) ($row->total_overdue ?? 0);
                $daysOverdue = (int) ($row->days_overdue ?? 0);

                return [
                    'loan_id' => (int) $row->loan_id,
                    'branch_id' => (int) $row->branch_id,
                    'branch_name' => (string) $row->branch_name,
                    'status' => $status,
                    'approved' => $approved,
                    'rejected' => $rejected,
                    'pending' => ! $approved && ! $rejected,
                    'observable' => $observable,
                    'active' => $active,
                    'closed' => in_array($status, self::CLOSED_STATUSES, true),
                    'written_off' => $writtenOff,
                    'current_arrears' => $active && ($overdue > 0 || $daysOverdue > 0),
                    'serious_arrears' => $active && $daysOverdue >= $seriousArrearsDays,
                    'outstanding' => $active ? $outstanding : 0.0,
                    'overdue' => $active ? $overdue : 0.0,
                    'application_score' => (float) $row->application_score,
                    'application_band' => (string) $row->application_band,
                    'application_confidence' => (float) $row->application_confidence,
                    'thin_file' => (bool) $row->thin_file,
                    'manual_review' => (bool) $row->manual_review_required,
                    'score_version' => (string) $row->score_version,
                    'application_calculated_at' => CarbonImmutable::parse($row->application_calculated_at),
                    'current_score' => $row->current_score !== null ? (float) $row->current_score : null,
                    'current_band' => $row->current_band !== null ? (string) $row->current_band : null,
                    'current_score_version' => $row->current_score_version !== null ? (string) $row->current_score_version : null,
                ];
            });
    }

    private function metrics(Collection $rows): array
    {
        $applications = $rows->count();
        $approved = $rows->where('approved', true)->count();
        $rejected = $rows->where('rejected', true)->count();
        $pending = $rows->where('pending', true)->count();
        $decided = $approved + $rejected;
        $observed = $rows->where('observable', true);
        $active = $rows->where('active', true);
        $outstanding = (float) $active->sum('outstanding');
        $overdue = (float) $active->sum('overdue');
        $writtenOff = $observed->where('written_off', true)->count();

        return [
            'applications' => $applications,
            'average_application_score' => $applications > 0 ? round((float) $rows->avg('application_score'), 1) : null,
            'average_confidence' => $applications > 0 ? round((float) $rows->avg('application_confidence'), 1) : null,
            'manual_review' => $rows->where('manual_review', true)->count(),
            'thin_file' => $rows->where('thin_file', true)->count(),
            'approved' => $approved,
            'rejected' => $rejected,
            'pending' => $pending,
            'decision_rate' => $this->rate($decided, $applications),
            'approval_rate' => $this->rate($approved, $decided),
            'observed_loans' => $observed->count(),
            'active_loans' => $active->count(),
            'closed_loans' => $observed->where('closed', true)->count(),
            'current_arrears_loans' => $active->where('current_arrears', true)->count(),
            'serious_arrears_loans' => $active->where('serious_arrears', true)->count(),
            'written_off_loans' => $writtenOff,
            'write_off_rate' => $this->rate($writtenOff, $observed->count()),
            'total_outstanding' => round($outstanding, 2),
            'total_overdue' => round($overdue, 2),
            'arrears_rate' => $outstanding > 0 ? round(($overdue / $outstanding) * 100, 2) : 0.0,
        ];
    }

    private function migrationMetrics(Collection $rows, array $bandRanks): array
    {
        $withCurrent = $rows->filter(fn (array $row) => $row['current_score'] !== null);
        $comparable = $withCurrent->filter(fn (array $row) => $row['current_score_version'] === $row['score_version']);
        $crossVersion = $withCurrent->count() - $comparable->count();
        $deltas = $comparable->map(fn (array $row) => $row['current_score'] - $row['application_score']);

        $improved = 0;
        $worsened = 0;
        $stable = 0;
        foreach ($comparable as $row) {
            $applicationRank = $bandRanks[$row['application_band']] ?? null;
            $currentRank = $bandRanks[$row['current_band']] ?? null;
            if ($applicationRank === null || $currentRank === null || $applicationRank === $currentRank) {
                $stable++;
            } elseif ($currentRank > $applicationRank) {
                $improved++;
            } else {
                $worsened++;
            }
        }

        return [
            'comparable' => $comparable->count(),
            'missing_current' => $rows->count() - $withCurrent->count(),
            'cross_version' => $crossVersion,
            'average_score_change' => $deltas->isNotEmpty() ? round((float) $deltas->avg(), 1) : null,
            'improved_band' => $improved,
            'stable_band' => $stable,
            'worsened_band' => $worsened,
        ];
    }

    private function drift(Collection $rows, Collection $branches, Collection $bands): array
    {
        $windowDays = max(1, (int) config('credit_score.monitoring.recent_window_days', 90));
        $minimumSample = max(1, (int) config('credit_score.monitoring.minimum_sample_size', 20));
        $scoreWarning = max(0, (float) config('credit_score.monitoring.average_score_shift_warning_points', 25));
        $shareWarning = max(0, (float) config('credit_score.monitoring.band_share_shift_warning_percentage_points', 15));
        $recentStart = CarbonImmutable::now()->subDays($windowDays);
        $previousStart = $recentStart->subDays($windowDays);
        $recent = $rows->filter(fn (array $row) => $row['application_calculated_at']->gte($recentStart))->values();
        $previous = $rows->filter(fn (array $row) => $row['application_calculated_at']->gte($previousStart) && $row['application_calculated_at']->lt($recentStart))->values();

        $overall = $this->driftWindow($recent, $previous, $bands, $minimumSample, $scoreWarning, $shareWarning);
        $branchDrift = $branches->map(function ($branch) use ($recent, $previous, $bands, $minimumSample, $scoreWarning, $shareWarning): array {
            return [
                'branch_id' => (int) $branch->id,
                'branch_name' => (string) $branch->name,
                ...$this->driftWindow(
                    $recent->where('branch_id', (int) $branch->id)->values(),
                    $previous->where('branch_id', (int) $branch->id)->values(),
                    $bands,
                    $minimumSample,
                    $scoreWarning,
                    $shareWarning,
                ),
            ];
        })->all();

        return [
            'window_days' => $windowDays,
            'recent_start' => $recentStart,
            'previous_start' => $previousStart,
            'minimum_sample_size' => $minimumSample,
            'average_score_shift_warning_points' => $scoreWarning,
            'band_share_shift_warning_percentage_points' => $shareWarning,
            'overall' => $overall,
            'branches' => $branchDrift,
        ];
    }

    private function driftWindow(
        Collection $recent,
        Collection $previous,
        Collection $bands,
        int $minimumSample,
        float $scoreWarning,
        float $shareWarning,
    ): array {
        $recentAverage = $recent->isNotEmpty() ? (float) $recent->avg('application_score') : null;
        $previousAverage = $previous->isNotEmpty() ? (float) $previous->avg('application_score') : null;
        $averageChange = $recentAverage !== null && $previousAverage !== null ? round($recentAverage - $previousAverage, 1) : null;
        $sufficient = $recent->count() >= $minimumSample && $previous->count() >= $minimumSample;

        $bandShifts = $bands->map(function (array $band) use ($recent, $previous, $shareWarning, $sufficient): array {
            $recentShare = $this->rate($recent->where('application_band', $band['key'])->count(), $recent->count());
            $previousShare = $this->rate($previous->where('application_band', $band['key'])->count(), $previous->count());
            $change = round($recentShare - $previousShare, 2);

            return [
                'key' => $band['key'],
                'label' => $band['label'],
                'recent_share' => $recentShare,
                'previous_share' => $previousShare,
                'change' => $change,
                'warning' => $sufficient && abs($change) >= $shareWarning,
            ];
        })->values()->all();

        $scoreShiftWarning = $sufficient && $averageChange !== null && abs($averageChange) >= $scoreWarning;
        $bandShiftWarning = $sufficient && collect($bandShifts)->contains('warning', true);
        $status = ! $sufficient ? 'insufficient_data' : (($scoreShiftWarning || $bandShiftWarning) ? 'warning' : 'stable');

        return [
            'status' => $status,
            'recent_applications' => $recent->count(),
            'previous_applications' => $previous->count(),
            'recent_average_score' => $recentAverage !== null ? round($recentAverage, 1) : null,
            'previous_average_score' => $previousAverage !== null ? round($previousAverage, 1) : null,
            'average_score_change' => $averageChange,
            'score_shift_warning' => $scoreShiftWarning,
            'band_shift_warning' => $bandShiftWarning,
            'band_shifts' => $bandShifts,
        ];
    }

    private function bandDefinitions(): Collection
    {
        return collect(config('credit_score.bands', []))
            ->map(fn (array $band, string $key) => [
                'key' => $key,
                'label' => (string) ($band['label'] ?? str($key)->headline()),
            ])
            ->values();
    }

    private function bandRanks(Collection $bands): array
    {
        return $bands
            ->reverse()
            ->values()
            ->mapWithKeys(fn (array $band, int $index) => [$band['key'] => $index + 1])
            ->all();
    }

    private function rate(int|float $numerator, int|float $denominator): float
    {
        return $denominator > 0 ? round(($numerator / $denominator) * 100, 2) : 0.0;
    }
}
