<?php

declare(strict_types=1);

namespace App\Services\Metrics;

use App\Enums\IncomeCategory;
use App\Models\Credit;
use App\Models\Income;
use App\Models\Installment;
use App\Models\Payment;
use App\Services\Dashboard\DashboardCacheService;
use App\ValueObjects\Money;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

/**
 * Servicio de métricas de cartera (portfolio).
 *
 * Proporciona cálculos fundamentales para dashboards:
 * - Cartera activa (saldo pendiente de cobro)
 * - Capital colocado y recuperado
 * - Composición de cartera por estado
 * - Métricas por collector
 *
 * IMPORTANTE:
 * - Todos los queries excluyen parent credits usando leafCredits() scope.
 * - CARTERA se calcula como SUM(total_amount - amount_paid) de cuotas no pagadas,
 *   NO como SUM(principal_balance_after). El campo principal_balance_after es la columna de amortización
 *   del plan (capital restante tras cada cuota al generar) y NO se actualiza con pagos.
 *   Sumar principal_balance_after de todas las cuotas produce un número sin significado financiero.
 */
class PortfolioMetricsService
{
    public function __construct(
        private DashboardCacheService $cache
    ) {}

    /**
     * Resumen completo de la cartera.
     *
     * @return array{
     *   active_credits_count: int,
     *   active_portfolio_balance: float,
     *   total_disbursed: float,
     *   total_recovered: float,
     *   total_interest_earned: float,
     *   average_ticket: float,
     *   credits_by_status: array
     * }
     */
    public function getSummary(int $companyId): array
    {
        return $this->cache->remember($companyId, 'portfolio.summary', function () use ($companyId) {
            $activeCredits = Credit::where('company_id', $companyId)
                ->leafCredits()
                ->whereIn('status', Credit::ACTIVE_STATUSES)
                ->get();

            $activeCount = $activeCredits->count();
            $activeBalance = $this->calculatePortfolioBalance($activeCredits, $companyId);

            // Capital total desembolsado (histórico)
            $totalDisbursed = Credit::where('company_id', $companyId)
                ->leafCredits()
                ->sum('amount');

            // Capital recuperado (principal)
            $totalRecovered = Income::where('company_id', $companyId)
                ->where('category', IncomeCategory::LOAN_PAYMENT_PRINCIPAL->value)
                ->sum('amount');

            // También incluir legacy payments como recuperación conservadora
            $legacyPayments = Income::where('company_id', $companyId)
                ->where('category', IncomeCategory::PAYMENT->value)
                ->sum('amount');

            // Intereses ganados
            $totalInterestEarned = Income::where('company_id', $companyId)
                ->where('category', IncomeCategory::LOAN_PAYMENT_INTEREST->value)
                ->sum('amount');

            // Ticket promedio (últimos 30 días)
            $recentCredits = Credit::where('company_id', $companyId)
                ->leafCredits()
                ->where('created_at', '>=', now()->subDays(30))
                ->pluck('amount');

            $averageTicket = $recentCredits->count() > 0
                ? $recentCredits->avg()
                : 0;

            // Distribución por estado
            $creditsByStatus = $this->getCreditsByStatus($companyId);

            return [
                'active_credits_count' => $activeCount,
                'active_portfolio_balance' => Money::of($activeBalance)->value(),
                'total_disbursed' => Money::of($totalDisbursed)->value(),
                'total_recovered' => Money::of($totalRecovered + $legacyPayments)->value(),
                'total_interest_earned' => Money::of($totalInterestEarned)->value(),
                'average_ticket' => Money::of($averageTicket)->value(),
                'credits_by_status' => $creditsByStatus,
            ];
        }, 'frequent');
    }

    /**
     * Balance de cartera activa (saldo pendiente de cobro).
     */
    public function getActivePortfolioBalance(int $companyId): float
    {
        return $this->cache->remember($companyId, 'portfolio.active_balance', function () use ($companyId) {
            $balance = Installment::where('company_id', $companyId)
                ->whereHas('credit', function ($q) {
                    $q->leafCredits()->whereIn('status', Credit::ACTIVE_STATUSES);
                })
                ->where('status', '!=', Installment::STATUS_PAID)
                ->selectRaw('COALESCE(SUM(total_amount - amount_paid), 0) as total')
                ->value('total');

            return Money::of($balance)->value();
        }, 'frequent');
    }

    /**
     * Distribución de créditos por estado.
     *
     * @return array<string, array{count: int, balance: float, percentage: float}>
     */
    public function getCreditsByStatus(int $companyId): array
    {
        return $this->cache->remember($companyId, 'portfolio.by_status', function () use ($companyId) {
            // Una sola query agrupada en lugar de N queries por status
            $results = DB::table('installments')
                ->join('credits', 'installments.credit_id', '=', 'credits.id')
                ->where('installments.company_id', $companyId)
                ->whereIn('credits.status', Credit::ACTIVE_STATUSES)
                ->where('installments.status', '!=', Installment::STATUS_PAID)
                ->whereNotExists(function ($q) {
                    $q->select(DB::raw(1))
                        ->from('credits as children')
                        ->whereColumn('children.parent_credit_id', 'credits.id');
                })
                ->select(
                    'credits.status',
                    DB::raw('COUNT(DISTINCT credits.id) as credit_count'),
                    DB::raw('SUM(installments.total_amount - installments.amount_paid) as balance')
                )
                ->groupBy('credits.status')
                ->get()
                ->keyBy('status');

            $totalCount = $results->sum('credit_count');
            $result = [];

            foreach (Credit::ACTIVE_STATUSES as $status) {
                $data = $results->get($status);
                $count = $data?->credit_count ?? 0;

                $result[$status] = [
                    'count' => $count,
                    'balance' => Money::of($data?->balance ?? 0)->value(),
                    'percentage' => $totalCount > 0
                        ? round(($count / $totalCount) * 100, 2)
                        : 0,
                ];
            }

            return $result;
        }, 'standard');
    }

    /**
     * Composición de cartera para gráfico (al día, mora leve, mora grave, vencido).
     *
     * @return array<string, array{label: string, count: int, balance: float, percentage: float, color: string}>
     */
    public function getPortfolioComposition(int $companyId): array
    {
        return $this->cache->remember($companyId, 'portfolio.composition', function () use ($companyId) {
            $today = Carbon::today();

            // Obtener todas las cuotas con sus créditos
            $installments = Installment::where('company_id', $companyId)
                ->whereHas('credit', function ($q) {
                    $q->leafCredits()->whereIn('status', Credit::ACTIVE_STATUSES);
                })
                ->where('status', '!=', Installment::STATUS_PAID)
                ->select('id', 'credit_id', 'due_date', 'total_amount', 'amount_paid')
                ->get();

            // Agrupar por categoría de mora
            $categories = [
                'current' => ['label' => 'Al día', 'count' => 0, 'balance' => Money::zero(), 'color' => 'success'],
                'early_delinquency' => ['label' => 'Mora 1-30 días', 'count' => 0, 'balance' => Money::zero(), 'color' => 'warning'],
                'late_delinquency' => ['label' => 'Mora 31-60 días', 'count' => 0, 'balance' => Money::zero(), 'color' => 'orange'],
                'severe_delinquency' => ['label' => 'Mora >60 días', 'count' => 0, 'balance' => Money::zero(), 'color' => 'danger'],
            ];

            // Agrupar por crédito para contar créditos únicos
            $creditCategories = [];

            foreach ($installments as $installment) {
                $daysOverdue = $installment->due_date->lt($today)
                    ? $installment->due_date->diffInDays($today)
                    : 0;

                $category = match (true) {
                    $daysOverdue === 0 => 'current',
                    $daysOverdue <= 30 => 'early_delinquency',
                    $daysOverdue <= 60 => 'late_delinquency',
                    default => 'severe_delinquency',
                };

                // Usar la peor categoría por crédito
                $currentCategory = $creditCategories[$installment->credit_id] ?? 'current';
                $categoryOrder = ['current' => 0, 'early_delinquency' => 1, 'late_delinquency' => 2, 'severe_delinquency' => 3];

                if ($categoryOrder[$category] > $categoryOrder[$currentCategory]) {
                    $creditCategories[$installment->credit_id] = $category;
                }
            }

            // Contar créditos y calcular balances por categoría
            foreach ($creditCategories as $creditId => $category) {
                $creditBalance = $installments
                    ->where('credit_id', $creditId)
                    ->sum(fn ($i) => $i->total_amount - $i->amount_paid);

                $categories[$category]['count']++;
                $categories[$category]['balance'] = $categories[$category]['balance']->add($creditBalance);
            }

            // Calcular porcentajes
            $totalCount = array_sum(array_column($categories, 'count'));
            $result = [];

            foreach ($categories as $key => $data) {
                $result[$key] = [
                    'label' => $data['label'],
                    'count' => $data['count'],
                    'balance' => $data['balance']->value(),
                    'percentage' => $totalCount > 0
                        ? round(($data['count'] / $totalCount) * 100, 2)
                        : 0,
                    'color' => $data['color'],
                ];
            }

            return $result;
        }, 'standard');
    }

    /**
     * Total cobrado en un período.
     */
    public function getCollectedAmount(int $companyId, Carbon $from, Carbon $to): float
    {
        $amount = Payment::where('company_id', $companyId)
            ->whereHas('credit', fn ($q) => $q->leafCredits())
            ->collections()
            ->whereBetween('payment_date', [$from->toDateString(), $to->toDateString()])
            ->sum('amount');

        return Money::of($amount)->value();
    }

    /**
     * Cobrado hoy.
     */
    public function getCollectedToday(int $companyId): float
    {
        return $this->cache->remember($companyId, 'portfolio.collected_today', function () use ($companyId) {
            $today = Carbon::today();

            return $this->getCollectedAmount($companyId, $today, $today);
        }, 'realtime');
    }

    /**
     * Cobrado esta semana.
     */
    public function getCollectedThisWeek(int $companyId): float
    {
        return $this->cache->remember($companyId, 'portfolio.collected_week', function () use ($companyId) {
            $from = Carbon::now()->startOfWeek();
            $to = Carbon::now();

            return $this->getCollectedAmount($companyId, $from, $to);
        }, 'frequent');
    }

    /**
     * Cobrado este mes.
     */
    public function getCollectedThisMonth(int $companyId): float
    {
        return $this->cache->remember($companyId, 'portfolio.collected_month', function () use ($companyId) {
            $from = Carbon::now()->startOfMonth();
            $to = Carbon::now();

            return $this->getCollectedAmount($companyId, $from, $to);
        }, 'frequent');
    }

    /**
     * Métricas por collector.
     *
     * @return Collection<int, array{
     *   collector_id: int,
     *   collector_name: string,
     *   credits_count: int,
     *   portfolio_balance: float,
     *   collected_month: float,
     *   overdue_count: int,
     *   delinquency_rate: float
     * }>
     */
    public function getMetricsByCollector(int $companyId): Collection
    {
        return $this->cache->remember($companyId, 'portfolio.by_collector', function () use ($companyId) {
            $collectors = Credit::where('company_id', $companyId)
                ->leafCredits()
                ->whereIn('status', Credit::ACTIVE_STATUSES)
                ->whereNotNull('collector_user_id')
                ->with('collector:id,name')
                ->select('collector_user_id')
                ->distinct()
                ->get()
                ->pluck('collector')
                ->filter();

            $monthStart = Carbon::now()->startOfMonth();
            $result = collect();

            foreach ($collectors as $collector) {
                $credits = Credit::where('company_id', $companyId)
                    ->leafCredits()
                    ->where('collector_user_id', $collector->id)
                    ->whereIn('status', Credit::ACTIVE_STATUSES)
                    ->get();

                $creditsCount = $credits->count();
                $overdueCount = $credits->where('status', Credit::STATUS_OVERDUE)->count();
                $delayedCount = $credits->where('status', Credit::STATUS_DELAYED)->count();

                $portfolioBalance = Installment::where('company_id', $companyId)
                    ->whereIn('credit_id', $credits->pluck('id'))
                    ->where('status', '!=', Installment::STATUS_PAID)
                    ->selectRaw('COALESCE(SUM(total_amount - amount_paid), 0) as total')
                    ->value('total');

                $collectedMonth = Payment::where('company_id', $companyId)
                    ->whereIn('credit_id', $credits->pluck('id'))
                    ->collections()
                    ->where('payment_date', '>=', $monthStart->toDateString())
                    ->sum('amount');

                $delinquencyRate = $creditsCount > 0
                    ? round((($overdueCount + $delayedCount) / $creditsCount) * 100, 2)
                    : 0;

                $result->push([
                    'collector_id' => $collector->id,
                    'collector_name' => $collector->name,
                    'credits_count' => $creditsCount,
                    'portfolio_balance' => Money::of($portfolioBalance)->value(),
                    'collected_month' => Money::of($collectedMonth)->value(),
                    'overdue_count' => $overdueCount + $delayedCount,
                    'delinquency_rate' => $delinquencyRate,
                ]);
            }

            return $result->sortByDesc('collected_month')->values();
        }, 'standard');
    }

    /**
     * Volumen desembolsado en un período.
     */
    public function getDisbursedVolume(int $companyId, Carbon $from, Carbon $to): float
    {
        $amount = Credit::where('company_id', $companyId)
            ->leafCredits()
            ->whereBetween('created_at', [$from->startOfDay(), $to->endOfDay()])
            ->sum('amount');

        return Money::of($amount)->value();
    }

    /**
     * Volumen desembolsado este mes.
     */
    public function getDisbursedThisMonth(int $companyId): float
    {
        return $this->cache->remember($companyId, 'portfolio.disbursed_month', function () use ($companyId) {
            $from = Carbon::now()->startOfMonth();
            $to = Carbon::now();

            return $this->getDisbursedVolume($companyId, $from, $to);
        }, 'frequent');
    }

    /**
     * Tendencia de cobros diarios (últimos N días).
     *
     * @return Collection<int, array{date: string, amount: float}>
     */
    public function getDailyCollectionTrend(int $companyId, int $days = 30): Collection
    {
        return $this->cache->remember($companyId, "portfolio.trend_daily_{$days}", function () use ($companyId, $days) {
            $from = Carbon::now()->subDays($days - 1)->startOfDay();
            $to = Carbon::now()->endOfDay();

            $payments = Payment::where('company_id', $companyId)
                ->whereHas('credit', fn ($q) => $q->leafCredits())
                ->collections()
                ->whereBetween('payment_date', [$from->toDateString(), $to->toDateString()])
                ->select(
                    DB::raw('DATE(payment_date) as date'),
                    DB::raw('SUM(amount) as total')
                )
                ->groupBy('date')
                ->pluck('total', 'date');

            $result = collect();
            $cursor = $from->copy();

            while ($cursor->lessThanOrEqualTo($to)) {
                $dateKey = $cursor->toDateString();
                $amount = $payments->get($dateKey, 0);

                $result->push([
                    'date' => $dateKey,
                    'amount' => Money::of($amount)->value(),
                ]);

                $cursor->addDay();
            }

            return $result;
        }, 'slow');
    }

    /**
     * Calcula el balance total de una colección de créditos.
     *
     * @param  Collection  $credits  Colección de créditos (ya filtrados por company)
     * @param  int  $companyId  ID de company para defensa multi-tenant en profundidad
     */
    private function calculatePortfolioBalance(Collection $credits, int $companyId): float
    {
        if ($credits->isEmpty()) {
            return 0;
        }

        $balance = Installment::where('company_id', $companyId)
            ->whereIn('credit_id', $credits->pluck('id'))
            ->where('status', '!=', Installment::STATUS_PAID)
            ->selectRaw('COALESCE(SUM(total_amount - amount_paid), 0) as total')
            ->value('total');

        return Money::of($balance)->value();
    }
}
