<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\Pwa;

use App\Http\Controllers\Api\Pwa\Traits\RoleAwareQueries;
use App\Http\Controllers\Controller;
use App\Models\Credit;
use App\Models\Expense;
use App\Models\Installment;
use App\Models\Payment;
use App\Models\User;
use App\Services\CollectorVisibilityResolver;
use App\Services\Dashboard\AdminDashboardMetricsService;
use App\Services\Metrics\BusinessMetricsService;
use App\Services\Metrics\CollectionMetricsService;
use App\Support\Metric;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class DashboardController extends Controller
{
    use RoleAwareQueries;

    public function __construct(
        private CollectionMetricsService $collectionMetrics,
        private AdminDashboardMetricsService $adminMetrics,
        private CollectorVisibilityResolver $visibility,
        private BusinessMetricsService $businessMetrics,
    ) {}

    /**
     * GET /api/pwa/dashboard
     *
     * Dashboard personalizado según el rol del usuario.
     */
    public function index(Request $request): JsonResponse
    {
        $role = $this->getUserPwaRole($request);

        return match ($role) {
            'admin' => $this->adminDashboard($request),
            'supervisor' => $this->supervisorDashboard($request),
            default => $this->collectorDashboard($request),
        };
    }

    /**
     * Dashboard para Collector.
     * Usa CollectionMetricsService para datos enriquecidos.
     *
     * Devuelve todo en una sola llamada: stats + semana + pendientes.
     */
    private function collectorDashboard(Request $request): JsonResponse
    {
        $user = $request->user();
        $today = Carbon::today()->toDateString();

        $dailySummary = $this->collectionMetrics->getCollectorDailySummary(
            $user->company_id,
            $user->id
        );

        // Aging breakdown (SQL-aggregated, single query)
        $aging = $this->collectionMetrics->getOverdueAging(
            $user->company_id,
            $user->id
        );

        // Weekly progress (for bar chart)
        $weeklyProgress = $this->collectionMetrics->getWeeklyProgress(
            $user->company_id,
            $user->id
        );

        return response()->json([
            'today' => $today,
            'role' => 'collector',
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
            ],
            'stats' => [
                'due_today' => [
                    'amount' => $dailySummary['due_today_amount'],
                    'count' => $dailySummary['due_today_count'],
                    'clients' => $dailySummary['due_today_clients'],
                ],
                'collected_today' => [
                    'amount' => $dailySummary['collected_today'],
                    'count' => $dailySummary['collected_today_count'],
                ],
                'overdue' => [
                    'amount' => $dailySummary['overdue_amount'],
                    'count' => $dailySummary['overdue_count'],
                ],
                // Lo que salió y lo que gastó, para que la jornada cuadre sin
                // pedírselo a nadie: hasta ahora el cobrador veía lo que entraba
                // pero no lo que entregaba.
                'disbursed_today' => $this->desembolsadoHoy($user->company_id, $user->id),
                'expenses_today' => $this->gastadoHoy($user->company_id, $user->id),
                'assigned_credits' => $dailySummary['active_credits'],
            ],
            'weekly_progress' => $weeklyProgress->toArray(),
            'week_total' => $weeklyProgress->sum('amount'),
            'overdue_aging' => $aging,
            'pending_sync' => 0,
        ]);
    }

    /**
     * Capital que este cobrador entregó hoy.
     *
     * Se cuenta por `start_date`, que es la fecha de desembolso del crédito, y
     * se suma `amount` (el capital que sale de la caja), no `total_receivable`,
     * que incluye interés que todavía no ha entrado.
     *
     * @return array{amount: float, count: int}
     */
    private function desembolsadoHoy(int $companyId, int $collectorId): array
    {
        $fila = Credit::query()
            ->where('company_id', $companyId)
            ->where('collector_user_id', $collectorId)
            ->whereDate('start_date', Carbon::today())
            ->selectRaw('COALESCE(SUM(amount), 0) AS total, COUNT(*) AS cuantos')
            ->first();

        return [
            'amount' => round((float) ($fila->total ?? 0), 2),
            'count' => (int) ($fila->cuantos ?? 0),
        ];
    }

    /**
     * Gastos que este cobrador registró hoy.
     *
     * Se filtra por `user_id` —quien lo registró— y NO por `affects_profit`, que
     * está mal poblada y dejaría fuera gastos reales.
     *
     * Y se EXCLUYEN los desembolsos: el sistema registra cada crédito entregado
     * como un gasto a nombre de quien lo entregó. Sin excluirlos, a un cobrador
     * que solo había desembolsado le salía el mismo dinero dos veces, en
     * "Desembolsado" y en "Gastos".
     *
     * `pending` va aparte porque para el cobrador no es un detalle: un gasto
     * suyo necesita aprobación, y hasta que llegue no sabe si se lo reconocen.
     *
     * @return array{amount: float, count: int, pending: int}
     */
    private function gastadoHoy(int $companyId, int $userId): array
    {
        $fila = Expense::query()
            ->withoutDisbursements()
            ->where('company_id', $companyId)
            ->where('user_id', $userId)
            ->whereDate('operation_date', Carbon::today())
            ->selectRaw("
                COALESCE(SUM(amount), 0) AS total,
                COUNT(*) AS cuantos,
                SUM(CASE WHEN approval_status = 'pending' THEN 1 ELSE 0 END) AS pendientes
            ")
            ->first();

        return [
            'amount' => round((float) ($fila->total ?? 0), 2),
            'count' => (int) ($fila->cuantos ?? 0),
            'pending' => (int) ($fila->pendientes ?? 0),
        ];
    }

    /**
     * Dashboard para Supervisor.
     */
    private function supervisorDashboard(Request $request): JsonResponse
    {
        $user = $request->user();
        $today = Carbon::today()->toDateString();

        // Visibilidad supervisor→cobrador (issue #71): un supervisor asignado solo
        // ve a su equipo; con sees_all_collectors (o admin) ve toda la empresa.
        //   $scopeIds = null  → company-wide (sin filtro)
        //   $scopeIds = [..]  → solo esos collector_user_id
        //   $scopeIds = []    → unassigned → no ve nada (métricas en cero)
        $vis = $this->visibility->resolve($user);
        $unassigned = $vis['mode'] === CollectorVisibilityResolver::MODE_UNASSIGNED;
        $scopeIds = $vis['mode'] === CollectorVisibilityResolver::MODE_ALL ? null : $vis['collector_ids'];

        // Total cobrado hoy (scoped al equipo del supervisor vía el cobrador del crédito)
        $companyCollectedToday = Payment::query()
            ->where('company_id', $user->company_id)
            ->whereDate('payment_date', $today)
            ->collections()
            ->when($scopeIds !== null, fn ($q) => $q->whereHas('credit', fn ($c) => $c->whereIn('collector_user_id', $scopeIds)))
            ->selectRaw('COALESCE(SUM(amount), 0) as total_amount, COUNT(*) as count')
            ->first();

        // Cartera vigente (saldo por cobrar de créditos activos)
        // IMPORTANTE: Excluir créditos padre (con hijos derivados)
        $activePortfolio = (float) Installment::query()
            ->whereHas('credit', function ($q) use ($user, $scopeIds) {
                $q->where('company_id', $user->company_id)
                    ->whereIn('status', Credit::ACTIVE_STATUSES)
                    ->whereDoesntHave('children'); // Excluir créditos padre
                if ($scopeIds !== null) {
                    $q->whereIn('collector_user_id', $scopeIds);
                }
            })
            ->sum(DB::raw('total_amount - amount_paid'));

        // Total vencidas (scoped)
        // IMPORTANTE: Excluir créditos padre (con hijos derivados)
        $overdue = Installment::query()
            ->whereHas('credit', function ($q) use ($user, $scopeIds) {
                $q->where('company_id', $user->company_id)
                    ->whereIn('status', Credit::ACTIVE_STATUSES)
                    ->whereDoesntHave('children'); // Excluir créditos padre
                if ($scopeIds !== null) {
                    $q->whereIn('collector_user_id', $scopeIds);
                }
            })
            ->where('due_date', '<', $today)
            ->whereIn('status', [
                Installment::STATUS_PENDING,
                Installment::STATUS_PARTIAL_PAID,
                Installment::STATUS_OVERDUE,
            ])
            ->selectRaw('COALESCE(SUM(total_amount - amount_paid), 0) as total_amount, COUNT(*) as count')
            ->first();

        // Collectors activos (con créditos asignados)
        // IMPORTANTE: Excluir créditos padre (con hijos derivados)
        $activeCollectors = Credit::query()
            ->where('company_id', $user->company_id)
            ->whereIn('status', Credit::ACTIVE_STATUSES)
            ->whereDoesntHave('children') // Excluir créditos padre
            ->whereNotNull('collector_user_id')
            ->when($scopeIds !== null, fn ($q) => $q->whereIn('collector_user_id', $scopeIds))
            ->distinct('collector_user_id')
            ->count('collector_user_id');

        // Desglose por cobrador (rendimiento del día), filtrado al equipo visible.
        // Condicional plano en vez de ->when() con callback: evita el falso positivo
        // de covarianza de PHPStan sobre el TValue de Collection.
        $performance = $this->collectionMetrics->getCollectorPerformanceToday($user->company_id);
        if ($scopeIds !== null) {
            $performance = $performance->whereIn('collector_id', $scopeIds);
        }
        $performance = $performance->values();

        $teamGoal = (float) $performance->sum('goal');
        $teamCollected = (float) $performance->sum('collected_today');
        $teamGoalPct = $teamGoal > 0 ? (int) round(($teamCollected / $teamGoal) * 100) : 0;

        $trend = $this->collectionMetrics->getTeamCollectionTrend($user->company_id, 7, $scopeIds);
        $trendPayload = $this->collectionsTrendPayload($trend);

        return response()->json([
            'today' => $today,
            'role' => 'supervisor',
            'unassigned' => $unassigned,
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
            ],
            'stats' => [
                'collected_today' => [
                    'amount' => round((float) $companyCollectedToday->total_amount, 2),
                    'count' => (int) $companyCollectedToday->count,
                ],
                'active_portfolio' => [
                    'amount' => round($activePortfolio, 2),
                ],
                'overdue' => [
                    'amount' => round((float) $overdue->total_amount, 2),
                    'count' => (int) $overdue->count,
                ],
                'active_collectors' => $activeCollectors,
                'collector_performance' => $performance->toArray(),
                'team_goal' => [
                    'amount' => round($teamGoal, 2),
                    'collected' => round($teamCollected, 2),
                    'percentage' => $teamGoalPct,
                ],
                'collections_trend' => $trendPayload['series'],
                'collected_trend' => $trendPayload['delta'],
            ],
            'pending_sync' => 0,
        ]);
    }

    /**
     * Dashboard para Admin.
     *
     * Delegado a AdminDashboardMetricsService para centralizar
     * toda la lógica financiera y garantizar consistencia.
     */
    private function adminDashboard(Request $request): JsonResponse
    {
        $user = $request->user();

        $companyId = (int) $user->company_id;

        $metrics = $this->adminMetrics->getMetrics($companyId);

        // 30 dias de historia para el sparkline (antes 7): el spec §B bloque 4
        // pide ver el ritmo del mes, no solo la semana.
        $trend = $this->collectionMetrics->getTeamCollectionTrend($companyId, 30, null);
        $trendPayload = $this->collectionsTrendPayload($trend);
        $metrics['collections_trend'] = $trendPayload['series'];
        $metrics['collected_trend'] = $trendPayload['delta'];

        // El dueno tambien necesita su meta del dia: antes solo se armaba en la
        // rama del cobrador, asi que su barra de progreso estaba siempre en 0.
        $metrics['due_today'] = [
            'amount' => $metrics['operational_summary']['due_today_amount'],
            'count' => $metrics['operational_summary']['due_today_count'],
        ];

        $metrics['profitability'] = $this->profitabilityBlock($companyId, (float) $metrics['business_total']['amount']);
        $metrics['ritmo'] = $this->ritmoBlock($companyId);

        return response()->json([
            'today' => Carbon::today()->toDateString(),
            'role' => 'admin',
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
            ],
            'stats' => $metrics,
            'pending_sync' => 0,
        ]);
    }

    /**
     * Bloque 2: rentabilidad real, ventana rolling de 30 dias contra los 30
     * previos. El interes sale del ledger de cuotas, no de `incomes`.
     *
     * @return array<string, mixed>
     */
    private function profitabilityBlock(int $companyId, float $patrimonio): array
    {
        $hoy = Carbon::today();
        $actual = $this->businessMetrics->forPeriod($companyId, $hoy->copy()->subDays(29), $hoy->copy());
        $previo = $this->businessMetrics->forPeriod($companyId, $hoy->copy()->subDays(59), $hoy->copy()->subDays(30));

        // Mismo shape que las tendencias de stock ({trend_pct, good}) para que el
        // frontend no tenga que distinguir dos contratos.
        $delta = static fn (float $ahora, float $antes): ?array => $antes > 0.0
            ? [
                'trend_pct' => round((($ahora - $antes) / $antes) * 100, 1),
                'good' => $ahora >= $antes,
            ]
            : null;

        // Sin costos cargados la utilidad NO es utilidad: es el interes bruto con
        // otro nombre — exactamente el bug que este panel vino a corregir. Tanto el
        // margen como la utilidad se marcan `unreliable` bajo la misma condicion,
        // porque son la misma mentira contada de dos formas.
        $sinCostos = $actual['operating_expenses'] <= 0.0;
        $avisoSinCostos = 'no hay costos registrados en el periodo: es practicamente el interes bruto';

        $margenValor = $actual['margin'];
        $margen = $margenValor === null
            ? Metric::noData('sin ingresos de utilidad en el periodo')
            : ($sinCostos
                ? Metric::unreliable(round($margenValor, 1), $avisoSinCostos)
                : Metric::ok(round($margenValor, 1)));

        $tendenciaUtilidad = $delta($actual['net_profit'], $previo['net_profit']);
        $utilidad = $sinCostos && $actual['net_profit'] > 0.0
            ? Metric::unreliable($actual['net_profit'], $avisoSinCostos, $tendenciaUtilidad)
            : Metric::ok($actual['net_profit'], $tendenciaUtilidad);

        return [
            'net_profit' => $utilidad->toArray(),
            'interest_collected' => Metric::ok(
                $actual['interest_collected'],
                $delta($actual['interest_collected'], $previo['interest_collected'])
            )->toArray(),
            'margin' => $margen->toArray(),
            'return_on_equity' => $patrimonio > 0.0
                ? Metric::ok(round(($actual['net_profit'] / $patrimonio) * 100, 1))->toArray()
                : Metric::noData('sin patrimonio para calcular el rendimiento')->toArray(),
            'cash_flow' => Metric::ok($actual['cash_flow'])->toArray(),
            'window' => [
                'from' => $hoy->copy()->subDays(29)->toDateString(),
                'to' => $hoy->toDateString(),
            ],
        ];
    }

    /**
     * Bloque 4: cortes de ritmo. "Mes en curso" viaja SIEMPRE con los dias
     * transcurridos, para que 8 de 30 dias no se lea como una caida del 72%.
     *
     * @return array<string, mixed>
     */
    private function ritmoBlock(int $companyId): array
    {
        $hoy = Carbon::today();
        $inicioMes = $hoy->copy()->startOfMonth();
        $inicioMesPasado = $hoy->copy()->subMonthNoOverflow()->startOfMonth();
        $finMesPasado = $inicioMes->copy()->subDay();

        $cobrado = static fn (Carbon $desde, Carbon $hasta): float => (float) Payment::query()
            ->withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->collections()
            ->whereBetween('payment_date', [$desde->toDateString(), $hasta->toDateString()])
            ->sum('amount');

        return [
            'month_to_date' => [
                'amount' => $cobrado($inicioMes, $hoy),
                'days_elapsed' => $hoy->day,
                'days_in_month' => $hoy->daysInMonth,
            ],
            'last_month' => [
                'amount' => $cobrado($inicioMesPasado, $finMesPasado),
                'label' => $inicioMesPasado->translatedFormat('F'),
            ],
        ];
    }

    /**
     * A partir de la serie de cobros (getTeamCollectionTrend), arma el payload de
     * tendencia: la serie completa (para sparkline) + el delta hoy vs ayer.
     *
     * @param  Collection<int, array{date:string, amount:float, count:int}>  $trend
     * @return array{series: array<int, array{date:string, amount:float}>, delta: array{pct: float|null, good: bool|null}}
     */
    private function collectionsTrendPayload(Collection $trend): array
    {
        $amounts = $trend->pluck('amount')->values()->all();
        $n = count($amounts);
        $today = $n > 0 ? (float) $amounts[$n - 1] : 0.0;
        $prev = $n > 1 ? (float) $amounts[$n - 2] : 0.0;
        $pct = $prev > 0 ? round((($today - $prev) / $prev) * 100, 1) : null;

        return [
            'series' => $trend->map(fn ($d) => ['date' => $d['date'], 'amount' => $d['amount']])->all(),
            'delta' => ['pct' => $pct, 'good' => $pct === null ? null : $pct >= 0],
        ];
    }

    /**
     * GET /api/pwa/dashboard/summary
     *
     * Resumen rápido para widget o notificaciones.
     * - Collector: cuotas urgentes propias
     * - Supervisor/Admin: cuotas urgentes de toda la empresa
     */
    public function summary(Request $request): JsonResponse
    {
        $user = $request->user();
        $today = Carbon::today()->toDateString();
        $role = $this->getUserPwaRole($request);

        // IMPORTANTE: Excluir créditos padre (con hijos derivados)
        $urgentCount = Installment::query()
            ->whereHas('credit', function ($q) use ($user, $role) {
                $q->where('company_id', $user->company_id)
                    ->whereIn('status', Credit::ACTIVE_STATUSES)
                    ->whereDoesntHave('children');

                // Collectors solo cuentan sus cuotas urgentes
                if ($role === 'collector') {
                    $q->where('collector_user_id', $user->id);
                }
                // supervisor/admin cuentan toda la empresa
            })
            ->where('due_date', '<=', $today)
            ->whereIn('status', [
                Installment::STATUS_PENDING,
                Installment::STATUS_PARTIAL_PAID,
                Installment::STATUS_OVERDUE,
            ])
            ->count();

        return response()->json([
            'urgent_count' => $urgentCount,
            'last_sync' => now()->toIso8601String(),
        ]);
    }

    /**
     * GET /api/pwa/collectors
     *
     * Lista de collectors de la empresa (solo supervisor/admin).
     */
    public function collectors(Request $request): JsonResponse
    {
        // Verificar que el usuario puede ver todos los creditos (supervisor/admin)
        if (! $this->canViewAllCredits($request)) {
            return response()->json([
                'message' => 'No tienes permiso para ver la lista de cobradores.',
            ], 403);
        }

        $user = $request->user();

        // Obtener collectors de la misma empresa usando Spatie role()
        $collectors = User::query()
            ->where('company_id', $user->company_id)
            ->role('collector')
            ->select('id', 'name')
            ->orderBy('name')
            ->get();

        return response()->json([
            'data' => $collectors,
            'meta' => [
                'total' => $collectors->count(),
            ],
        ]);
    }

    /**
     * GET /api/pwa/dashboard/weekly-progress
     *
     * Progreso semanal.
     * - Collector: progreso personal (Lun–Dom)
     * - Supervisor/Admin: progreso del equipo completo (últimos 7 días).
     *   Acepta ?collector_id para ver un cobrador específico.
     */
    public function weeklyProgress(Request $request): JsonResponse
    {
        $user = $request->user();
        $role = $this->getUserPwaRole($request);

        if ($role === 'collector') {
            $progress = $this->collectionMetrics->getWeeklyProgress(
                $user->company_id,
                $user->id
            );
        } else {
            // Supervisor/Admin: un cobrador específico o todo su equipo visible (#71).
            $scope = $this->visibleCollectorScope($request); // null | [] | int[]

            if ($request->filled('collector_id')) {
                // Cobrador puntual: solo si está dentro de su visibilidad; si no, vacío.
                $progress = (is_array($scope) && count($scope) === 1)
                    ? $this->collectionMetrics->getWeeklyProgress($user->company_id, $scope[0])
                    : collect();
            } else {
                // Tendencia del equipo visible — adaptar formato para consistencia.
                $trend = $this->collectionMetrics->getTeamCollectionTrend(
                    $user->company_id,
                    7,
                    $scope
                );

                $dayNames = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
                $progress = $trend->map(fn ($item) => [
                    'day' => $item['date'],
                    'day_name' => $dayNames[Carbon::parse($item['date'])->dayOfWeekIso - 1] ?? '?',
                    'amount' => $item['amount'],
                ]);
            }
        }

        $total = $progress->sum('amount');
        $average = $progress->count() > 0 ? $total / $progress->count() : 0;

        return response()->json([
            'data' => $progress->values()->toArray(),
            'meta' => [
                'total' => round($total, 2),
                'average' => round($average, 2),
                'days' => $progress->count(),
                'scope' => $role === 'collector' ? 'personal' : 'team',
            ],
        ]);
    }

    /**
     * GET /api/pwa/dashboard/recent-payments
     *
     * Últimos pagos del collector actual.
     */
    public function recentPayments(Request $request): JsonResponse
    {
        $user = $request->user();
        $limit = $request->input('limit', 10);

        $payments = $this->collectionMetrics->getRecentPayments(
            $user->company_id,
            $user->id,
            min($limit, 20)
        );

        return response()->json([
            'data' => $payments->toArray(),
            'meta' => [
                'count' => $payments->count(),
            ],
        ]);
    }

    /**
     * GET /api/pwa/dashboard/pending-collections
     *
     * Cuotas pendientes de cobrar para el collector actual.
     */
    public function pendingCollections(Request $request): JsonResponse
    {
        $user = $request->user();

        $pending = $this->collectionMetrics->getPendingCollections(
            $user->company_id,
            $user->id
        );

        // Agrupar por prioridad
        $byPriority = [
            'critical' => $pending->where('priority', 'critical')->values(),
            'high' => $pending->where('priority', 'high')->values(),
            'medium' => $pending->where('priority', 'medium')->values(),
            'normal' => $pending->where('priority', 'normal')->values(),
        ];

        return response()->json([
            'data' => $pending->toArray(),
            'by_priority' => $byPriority,
            'meta' => [
                'total' => $pending->count(),
                'total_amount' => $pending->sum('amount_due'),
                'critical_count' => $byPriority['critical']->count(),
                'high_count' => $byPriority['high']->count(),
            ],
        ]);
    }
}
