<?php

declare(strict_types=1);

namespace App\Services\Metrics;

use App\Enums\ExpenseCategory;
use App\Enums\IncomeCategory;
use App\Models\Expense;
use App\Models\Income;
use App\Services\CashFlowService;
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 flujo de caja para dashboards.
 *
 * Complementa CashFlowService con métricas específicas para dashboards:
 * - Posición de caja actual
 * - Ganancia neta
 * - ROI de cartera
 * - Tendencias y proyecciones
 */
class CashFlowMetricsService
{
    public function __construct(
        private DashboardCacheService $cache,
        private CashFlowService $cashFlowService
    ) {}

    /**
     * Posición de caja actual.
     *
     * Caja = SUM(ingresos) - SUM(egresos efectivos que afectan caja)
     *
     * CRITERIOS DELIBERADOS:
     * - Se suman TODOS los ingresos sin excepción. El income con categoría OPENING_BALANCE
     *   es la fuente de verdad del saldo inicial; settings.opening_balance es un campo
     *   separado que no se mantiene sincronizado y no es fiable para este cálculo.
     * - Se restan solo los egresos EFECTIVOS con affects_cash=true (scopeEffective):
     *   aprobados o que no requieren aprobación. Un egreso registrado por un supervisor o
     *   cobrador queda PENDIENTE y NO descuenta caja hasta que el admin lo aprueba.
     *   Misma regla canónica que getCashBase, getNetProfit y FinancialController.
     */
    public function getCashPosition(int $companyId): float
    {
        return $this->cache->remember($companyId, 'cashflow.position', function () use ($companyId) {
            $totalIncomes = Income::where('company_id', $companyId)
                ->sum('amount');

            $totalExpenses = Expense::where('company_id', $companyId)
                ->where('affects_cash', true)
                ->effective()
                ->sum('amount');

            return Money::of($totalIncomes)->subtract($totalExpenses)->value();
        }, 'frequent');
    }

    /**
     * Ganancia neta de un período.
     *
     * Ganancia = Ingresos (affects_profit) - Egresos (affects_profit)
     */
    public function getNetProfit(int $companyId, Carbon $from, Carbon $to): float
    {
        // Ingresos que afectan utilidad REAL
        // Excluir legacy PAYMENT: contiene capital recuperado que NO es ganancia
        $profitIncomeCategories = collect(IncomeCategory::cases())
            ->filter(fn ($cat) => $cat->affectsProfit() && $cat !== IncomeCategory::PAYMENT)
            ->map(fn ($cat) => $cat->value)
            ->toArray();

        $incomes = Income::where('company_id', $companyId)
            ->whereIn('category', $profitIncomeCategories)
            ->whereBetween('operation_date', [$from->toDateString(), $to->toDateString()])
            ->sum('amount');

        // Egresos que afectan utilidad operativa
        // Excluir retiros de utilidades y dividendos: son distribución de ganancias,
        // no gastos operativos (van debajo de la línea de utilidad neta)
        $nonOperatingExpenseCategories = [
            ExpenseCategory::PROFIT_WITHDRAWAL->value,
            ExpenseCategory::CAPITAL_RETURN->value,
            ExpenseCategory::DIVIDEND_PAYMENT->value,
        ];

        $expenses = Expense::where('company_id', $companyId)
            ->where('affects_profit', true)
            ->whereNotIn('category', $nonOperatingExpenseCategories)
            ->effective()
            ->whereBetween('operation_date', [$from->toDateString(), $to->toDateString()])
            ->sum('amount');

        return Money::of($incomes)->subtract($expenses)->value();
    }

    /**
     * Ganancia neta del mes actual.
     */
    public function getNetProfitThisMonth(int $companyId): float
    {
        return $this->cache->remember($companyId, 'cashflow.profit_month', function () use ($companyId) {
            $from = Carbon::now()->startOfMonth();
            $to = Carbon::now();

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

    /**
     * Intereses ganados en un período.
     */
    public function getInterestEarned(int $companyId, Carbon $from, Carbon $to): float
    {
        $interest = Income::where('company_id', $companyId)
            ->where('category', IncomeCategory::LOAN_PAYMENT_INTEREST->value)
            ->whereBetween('operation_date', [$from->toDateString(), $to->toDateString()])
            ->sum('amount');

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

    /**
     * Intereses ganados este mes.
     */
    public function getInterestEarnedThisMonth(int $companyId): float
    {
        return $this->cache->remember($companyId, 'cashflow.interest_month', function () use ($companyId) {
            $from = Carbon::now()->startOfMonth();
            $to = Carbon::now();

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

    /**
     * ROI de cartera (Return on Investment).
     *
     * ROI = (Intereses Ganados / Capital Promedio Colocado) × 100 (anualizado)
     */
    public function getPortfolioROI(int $companyId, Carbon $from, Carbon $to): float
    {
        return $this->cache->remember($companyId, 'cashflow.roi', function () use ($companyId, $from, $to) {
            // Intereses ganados en el período
            $interestEarned = $this->getInterestEarned($companyId, $from, $to);

            if ($interestEarned <= 0) {
                return 0;
            }

            // Capital promedio (simplificado: usamos el capital actual)
            // En una implementación más sofisticada, calcularíamos el promedio ponderado
            $capitalDeployed = Income::where('company_id', $companyId)
                ->where('category', IncomeCategory::LOAN_PAYMENT_PRINCIPAL->value)
                ->sum('amount');

            // Añadir capital inicial/inyecciones
            $capitalInjections = Income::where('company_id', $companyId)
                ->whereIn('category', [
                    IncomeCategory::CAPITAL_INJECTION->value,
                    IncomeCategory::OPENING_BALANCE->value,
                    IncomeCategory::INVESTOR_CONTRIBUTION->value,
                ])
                ->sum('amount');

            $totalCapital = $capitalDeployed + $capitalInjections;

            if ($totalCapital <= 0) {
                return 0;
            }

            // Calcular ROI del período
            $periodDays = $from->diffInDays($to) ?: 1;
            $periodROI = ($interestEarned / $totalCapital) * 100;

            // Anualizar
            $annualizedROI = ($periodROI / $periodDays) * 365;

            return round($annualizedROI, 2);
        }, 'slow');
    }

    /**
     * Tasa de recuperación (Collection Rate).
     *
     * Recovery Rate = (Cobrado en período / Programado en período) × 100
     *
     * Retorna null si no hay cuotas programadas en el período (en lugar de
     * un falso 100%). Se limita a 150% para señalar anomalías (prepagos masivos)
     * sin distorsionar el indicador.
     */
    public function getRecoveryRate(int $companyId, Carbon $from, Carbon $to): ?float
    {
        return $this->cache->remember($companyId, "cashflow.recovery_{$from->format('Ymd')}_{$to->format('Ymd')}", function () use ($companyId, $from, $to) {
            // Lo que se debía cobrar (cuotas programadas en el período)
            $scheduled = DB::table('installments')
                ->join('credits', 'installments.credit_id', '=', 'credits.id')
                ->where('installments.company_id', $companyId)
                ->whereNotExists(function ($q) {
                    $q->select(DB::raw(1))
                        ->from('credits as children')
                        ->whereColumn('children.parent_credit_id', 'credits.id');
                })
                ->whereBetween('installments.due_date', [$from->toDateString(), $to->toDateString()])
                ->sum('installments.total_amount');

            if ($scheduled <= 0) {
                return null;
            }

            // Lo que realmente se cobró
            $collected = DB::table('payments')
                ->join('credits', 'payments.credit_id', '=', 'credits.id')
                ->where('payments.company_id', $companyId)
                ->where('payments.voided', false)
                ->where('payments.payment_method', '!=', 'reversal')
                ->whereNotExists(function ($q) {
                    $q->select(DB::raw(1))
                        ->from('credits as children')
                        ->whereColumn('children.parent_credit_id', 'credits.id');
                })
                ->whereBetween('payments.payment_date', [$from->toDateString(), $to->toDateString()])
                ->sum('payments.amount');

            $rate = round(($collected / $scheduled) * 100, 2);

            return min($rate, 150.0);
        }, 'slow');
    }

    /**
     * Tasa de recuperación del mes actual.
     */
    public function getRecoveryRateThisMonth(int $companyId): ?float
    {
        $from = Carbon::now()->startOfMonth();
        $to = Carbon::now();

        return $this->getRecoveryRate($companyId, $from, $to);
    }

    /**
     * Resumen financiero completo para dashboard de Admin.
     */
    public function getFinancialSummary(int $companyId): array
    {
        return $this->cache->remember($companyId, 'cashflow.summary', function () use ($companyId) {
            $today = Carbon::today();
            $monthStart = $today->copy()->startOfMonth();
            $lastMonthStart = $today->copy()->subMonth()->startOfMonth();
            $lastMonthEnd = $today->copy()->subMonth()->endOfMonth();

            // Métricas actuales
            $cashPosition = $this->getCashPosition($companyId);
            $profitThisMonth = $this->getNetProfit($companyId, $monthStart, $today);
            $profitLastMonth = $this->getNetProfit($companyId, $lastMonthStart, $lastMonthEnd);
            $interestThisMonth = $this->getInterestEarned($companyId, $monthStart, $today);
            $recoveryRate = $this->getRecoveryRate($companyId, $monthStart, $today);

            // Calcular variaciones
            $profitVariation = $profitLastMonth > 0
                ? round((($profitThisMonth - $profitLastMonth) / $profitLastMonth) * 100, 2)
                : ($profitThisMonth > 0 ? 100 : 0);

            return [
                'cash_position' => $cashPosition,
                'net_profit_month' => $profitThisMonth,
                'net_profit_last_month' => $profitLastMonth,
                'profit_variation_percentage' => $profitVariation,
                'interest_earned_month' => $interestThisMonth,
                'recovery_rate' => $recoveryRate,
            ];
        }, 'frequent');
    }

    /**
     * Tendencia de ingresos vs egresos (mensual).
     *
     * @return Collection<int, array{
     *   month: string,
     *   incomes: float,
     *   expenses: float,
     *   net: float
     * }>
     */
    public function getIncomeVsExpenseTrend(int $companyId, int $months = 6): Collection
    {
        return $this->cache->remember($companyId, "cashflow.trend_{$months}m", function () use ($companyId, $months) {
            $from = Carbon::now()->subMonths($months - 1)->startOfMonth();
            $to = Carbon::now()->endOfMonth();

            return $this->cashFlowService->monthlySeries($from, $to, $companyId);
        }, 'slow');
    }

    /**
     * Desglose de ingresos por categoría (mes actual).
     *
     * @return array<string, array{label: string, amount: float, percentage: float}>
     */
    public function getIncomeBreakdown(int $companyId): array
    {
        return $this->cache->remember($companyId, 'cashflow.income_breakdown', function () use ($companyId) {
            $monthStart = Carbon::now()->startOfMonth();

            $incomes = Income::where('company_id', $companyId)
                ->where('operation_date', '>=', $monthStart->toDateString())
                ->select('category', DB::raw('SUM(amount) as total'))
                ->groupBy('category')
                ->get()
                ->keyBy('category');

            $total = $incomes->sum('total');
            $result = [];

            foreach ($incomes as $category => $data) {
                $cat = IncomeCategory::tryFrom($category);
                if (! $cat) {
                    continue;
                }

                $result[$category] = [
                    'label' => $cat->getLabel(),
                    'amount' => Money::of($data->total)->value(),
                    'percentage' => $total > 0
                        ? round(($data->total / $total) * 100, 2)
                        : 0,
                ];
            }

            // Ordenar por monto descendente
            uasort($result, fn ($a, $b) => $b['amount'] <=> $a['amount']);

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

    /**
     * Desglose de egresos por categoría (mes actual).
     *
     * @return array<string, array{label: string, amount: float, percentage: float}>
     */
    public function getExpenseBreakdown(int $companyId): array
    {
        return $this->cache->remember($companyId, 'cashflow.expense_breakdown', function () use ($companyId) {
            $monthStart = Carbon::now()->startOfMonth();

            $expenses = Expense::where('company_id', $companyId)
                ->where('operation_date', '>=', $monthStart->toDateString())
                ->effective()
                ->select('category', DB::raw('SUM(amount) as total'))
                ->groupBy('category')
                ->get()
                ->keyBy('category');

            $total = $expenses->sum('total');
            $result = [];

            foreach ($expenses as $category => $data) {
                $cat = ExpenseCategory::tryFrom($category);
                if (! $cat) {
                    continue;
                }

                $result[$category] = [
                    'label' => $cat->getLabel(),
                    'amount' => Money::of($data->total)->value(),
                    'percentage' => $total > 0
                        ? round(($data->total / $total) * 100, 2)
                        : 0,
                ];
            }

            // Ordenar por monto descendente
            uasort($result, fn ($a, $b) => $b['amount'] <=> $a['amount']);

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