<?php

declare(strict_types=1);

namespace App\Services\Dashboard;

use Closure;
use Illuminate\Cache\TaggableStore;
use Illuminate\Support\Facades\Cache;

/**
 * Servicio de cache para dashboards.
 *
 * Gestiona el caching de métricas con invalidación por company_id
 * y soporte para tags que permiten flush selectivo.
 *
 * Si el driver de cache no soporta tags (ej: array, file),
 * usa keys compuestas como fallback.
 */
class DashboardCacheService
{
    /** TTL por defecto en segundos */
    private const DEFAULT_TTL = 300; // 5 minutos

    /** Prefijo para todas las keys de dashboard */
    private const KEY_PREFIX = 'dashboard';

    /**
     * TTL predefinidos por tipo de métrica (en segundos).
     */
    private const TTL_MAP = [
        'realtime' => 30,        // 30 segundos - cobros del día, stats en vivo
        'frequent' => 300,       // 5 minutos - cartera, caja
        'standard' => 900,       // 15 minutos - mora por collector
        'slow' => 1800,          // 30 minutos - stats semanales
        'daily' => 3600,         // 1 hora - MRR, métricas de plataforma
        'report' => 86400,       // 24 horas - reportes históricos
    ];

    private bool $supportsTags;

    public function __construct()
    {
        $this->supportsTags = $this->checkTagSupport();
    }

    /**
     * Verifica si el driver de cache actual soporta tags.
     */
    private function checkTagSupport(): bool
    {
        try {
            return Cache::getStore() instanceof TaggableStore;
        } catch (\Throwable) {
            return false;
        }
    }

    /**
     * Obtiene un valor del cache o lo computa si no existe.
     *
     * @param  int  $companyId  ID de la company (0 para métricas globales)
     * @param  string  $key  Identificador único de la métrica
     * @param  Closure  $compute  Función que calcula el valor si no está en cache
     * @param  string|int  $ttl  TTL en segundos o nombre del preset ('realtime', 'frequent', etc.)
     */
    public function remember(int $companyId, string $key, Closure $compute, string|int $ttl = 'frequent'): mixed
    {
        $cacheKey = $this->buildKey($companyId, $key);
        $ttlSeconds = $this->resolveTtl($ttl);

        if ($this->supportsTags) {
            return Cache::tags($this->getTags($companyId))
                ->remember($cacheKey, $ttlSeconds, $compute);
        }

        return Cache::remember($cacheKey, $ttlSeconds, $compute);
    }

    /**
     * Obtiene un valor del cache sin computar.
     */
    public function get(int $companyId, string $key): mixed
    {
        $cacheKey = $this->buildKey($companyId, $key);

        if ($this->supportsTags) {
            return Cache::tags($this->getTags($companyId))->get($cacheKey);
        }

        return Cache::get($cacheKey);
    }

    /**
     * Guarda un valor en cache.
     */
    public function put(int $companyId, string $key, mixed $value, string|int $ttl = 'frequent'): void
    {
        $cacheKey = $this->buildKey($companyId, $key);
        $ttlSeconds = $this->resolveTtl($ttl);

        if ($this->supportsTags) {
            Cache::tags($this->getTags($companyId))->put($cacheKey, $value, $ttlSeconds);
        } else {
            Cache::put($cacheKey, $value, $ttlSeconds);
        }
    }

    /**
     * Elimina una key específica del cache.
     */
    public function forget(int $companyId, string $key): void
    {
        $cacheKey = $this->buildKey($companyId, $key);

        if ($this->supportsTags) {
            Cache::tags($this->getTags($companyId))->forget($cacheKey);
        } else {
            Cache::forget($cacheKey);
        }
    }

    /**
     * Invalida todo el cache de una company.
     * Usar cuando hay cambios significativos (nuevo pago, nuevo crédito, etc.)
     *
     * Nota: Sin soporte de tags, esto solo invalida keys conocidas.
     */
    public function invalidateCompany(int $companyId): void
    {
        if ($this->supportsTags) {
            Cache::tags(["company:{$companyId}"])->flush();
        } else {
            // Sin tags, invalidamos las keys más comunes
            $this->invalidateCommonKeys($companyId);
        }
    }

    /**
     * Invalida todo el cache de dashboards (todas las companies).
     * Usar con precaución - solo para mantenimiento.
     */
    public function invalidateAll(): void
    {
        if ($this->supportsTags) {
            Cache::tags([self::KEY_PREFIX])->flush();
        }
        // Sin tags, no hay forma eficiente de invalidar todo
    }

    /**
     * Invalida keys comunes cuando no hay soporte de tags.
     */
    private function invalidateCommonKeys(int $companyId): void
    {
        $commonKeys = [
            'portfolio.summary', 'portfolio.active_balance', 'portfolio.by_status',
            'portfolio.composition', 'portfolio.collected_today', 'portfolio.collected_week',
            'portfolio.collected_month', 'portfolio.by_collector', 'portfolio.disbursed_month',
            'delinquency.par', 'delinquency.aging', 'delinquency.by_collector', 'delinquency.summary',
            'delinquency.top_overdue_v2_10', 'delinquency.top_overdue_v2_20',
            'cashflow.position', 'cashflow.profit_month', 'cashflow.interest_month',
            'cashflow.summary', 'collection.alerts',
        ];

        foreach ($commonKeys as $key) {
            Cache::forget($this->buildKey($companyId, $key));
        }
    }

    /**
     * Invalida métricas específicas de una company.
     * Útil para invalidación selectiva después de eventos específicos.
     *
     * @param  array<string>  $keys  Lista de keys a invalidar
     */
    public function invalidateKeys(int $companyId, array $keys): void
    {
        foreach ($keys as $key) {
            $this->forget($companyId, $key);
        }
    }

    /**
     * Invalida métricas relacionadas con pagos.
     * Llamar después de registrar un pago.
     */
    public function invalidatePaymentRelated(int $companyId): void
    {
        $this->invalidateKeys($companyId, [
            'portfolio.summary',
            'portfolio.active_balance',
            'portfolio.collected_today',
            'portfolio.collected_week',
            'portfolio.collected_month',
            'delinquency.par',
            'delinquency.aging',
            'delinquency.summary',
            'delinquency.top_overdue_v2_10',
            'delinquency.top_overdue_v2_20',
            'delinquency.by_collector',
            'cashflow.position',
            'cashflow.summary',
        ]);
    }

    /**
     * Invalida métricas relacionadas con créditos.
     * Llamar después de crear/modificar un crédito.
     */
    public function invalidateCreditRelated(int $companyId): void
    {
        $this->invalidateKeys($companyId, [
            'portfolio.summary',
            'portfolio.active_balance',
            'portfolio.composition',
            'portfolio.by_collector',
            'delinquency.par',
            'delinquency.aging',
            'delinquency.summary',
            'delinquency.by_collector',
        ]);
    }

    /**
     * Invalida métricas relacionadas con operaciones financieras de socios.
     * Llamar después de registrar aportes, aprobar retiros o distribuir utilidades.
     */
    public function invalidateFinancialRelated(int $companyId): void
    {
        $this->invalidateKeys($companyId, [
            'cashflow.position',
            'cashflow.summary',
            'cashflow.profit_month',
            'cashflow.income_breakdown',
            'cashflow.expense_breakdown',
        ]);
    }

    /**
     * Invalida métricas de plataforma (Super Admin).
     * Llamar después de cambios en suscripciones/companies.
     */
    public function invalidatePlatformMetrics(): void
    {
        Cache::tags(['platform'])->flush();
    }

    /**
     * Verifica si una key existe en cache.
     */
    public function has(int $companyId, string $key): bool
    {
        $cacheKey = $this->buildKey($companyId, $key);

        if ($this->supportsTags) {
            return Cache::tags($this->getTags($companyId))->has($cacheKey);
        }

        return Cache::has($cacheKey);
    }

    /**
     * Obtiene estadísticas de cache para diagnóstico.
     */
    public function getStats(int $companyId): array
    {
        // Esto depende del driver de cache usado
        // Para Redis podríamos obtener más info
        return [
            'company_id' => $companyId,
            'prefix' => self::KEY_PREFIX,
            'driver' => config('cache.default'),
        ];
    }

    /**
     * Construye la key completa para el cache.
     */
    private function buildKey(int $companyId, string $key): string
    {
        return sprintf('%s:%d:%s', self::KEY_PREFIX, $companyId, $key);
    }

    /**
     * Obtiene los tags para una company.
     */
    private function getTags(int $companyId): array
    {
        $tags = [self::KEY_PREFIX];

        if ($companyId > 0) {
            $tags[] = "company:{$companyId}";
        } else {
            $tags[] = 'platform';
        }

        return $tags;
    }

    /**
     * Resuelve el TTL desde un preset o valor directo.
     */
    private function resolveTtl(string|int $ttl): int
    {
        if (is_int($ttl)) {
            return $ttl;
        }

        return self::TTL_MAP[$ttl] ?? self::DEFAULT_TTL;
    }
}
