<?php

namespace App\Service;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * @codeCoverageIgnore Infrastructure monitoring — reads /proc, Docker socket, network
 */
class InfraService
{
    public function __construct(
        private EntityManagerInterface $em,
        #[Autowire(env: 'MESSENGER_TRANSPORT_DSN')] private string $messengerDsn,
        #[Autowire(env: 'SESSION_HANDLER_DSN')] private string $sessionDsn,
        #[Autowire(env: 'REDIS_CACHE_DSN')] private string $cacheDsn,
        #[Autowire(env: 'DATABASE_URL')] private string $databaseUrl,
        #[Autowire(env: 'OUTSIDE_URL')] private string $outsideUrl,
    ) {
    }

    /**
     * @return array<string, mixed>
     */
    public function getAll(): array
    {
        return [
            'server' => $this->getServerInfo(),
            'containers' => $this->getDockerContainers(),
            'redis_global' => $this->getRedisGlobalInfo(),
            'redis_dbs' => [
                $this->getRedisDbInfo($this->messengerDsn, 'Messenger'),
                $this->getRedisDbInfo($this->sessionDsn, 'Sessions'),
                $this->getRedisDbInfo($this->cacheDsn, 'Cache'),
            ],
            'postgres' => $this->getPostgresInfo(),
            'pgbouncer' => $this->getPgBouncerInfo(),
        ];
    }

    // ── Server ───────────────────────────────────────────────

    /**
     * @return array<string, mixed>
     */
    private function getServerInfo(): array
    {
        $info = ['hostname' => gethostname() ?: '?', 'os' => php_uname('s').' '.php_uname('r')];

        // CPU
        $info['cpu_model'] = '?';
        $info['cpu_cores'] = 1;
        if (is_readable('/proc/cpuinfo')) {
            $cpuinfo = file_get_contents('/proc/cpuinfo');
            $info['cpu_cores'] = substr_count($cpuinfo, 'processor') ?: 1;
            if (preg_match('/model name\s*:\s*(.+)/i', $cpuinfo, $m)) {
                $info['cpu_model'] = trim($m[1]);
            }
        }
        $load = sys_getloadavg();
        $info['load_1m'] = $load ? round($load[0], 2) : '?';
        $info['load_5m'] = $load ? round($load[1], 2) : '?';
        $info['load_15m'] = $load ? round($load[2], 2) : '?';
        $info['load_percent'] = $load ? round($load[0] / $info['cpu_cores'] * 100, 1) : '?';

        // RAM
        $info += $this->readHostMemory();

        // Disk
        $diskTotal = @disk_total_space('/app');
        $diskFree = @disk_free_space('/app');
        if (false !== $diskTotal && false !== $diskFree) {
            $diskUsed = $diskTotal - $diskFree;
            $info['disk_total'] = self::fmtBytes((int) $diskTotal);
            $info['disk_used'] = self::fmtBytes((int) $diskUsed);
            $info['disk_free'] = self::fmtBytes((int) $diskFree);
            $info['disk_percent'] = round($diskUsed / $diskTotal * 100, 1);
        } else {
            $info['disk_total'] = $info['disk_used'] = $info['disk_free'] = '?';
            $info['disk_percent'] = '?';
        }

        // Uptime
        $info['uptime'] = is_readable('/proc/uptime')
            ? self::fmtDuration((int) explode(' ', file_get_contents('/proc/uptime'))[0])
            : '?';

        // Services
        $info['caddy'] = $this->checkCaddy();
        $info['docker'] = $this->checkDocker();
        $info['ssl'] = $this->checkSsl();

        return $info;
    }

    /**
     * @return array<string, mixed>
     */
    private function readHostMemory(): array
    {
        if (!is_readable('/proc/meminfo')) {
            return ['ram_total' => '?', 'ram_used' => '?', 'ram_free' => '?', 'ram_percent' => '?'];
        }
        $meminfo = file_get_contents('/proc/meminfo');
        $v = [];
        foreach (['MemTotal', 'MemFree', 'MemAvailable', 'Buffers', 'Cached'] as $k) {
            if (preg_match("/{$k}:\s+(\d+)\s+kB/", $meminfo, $m)) {
                $v[$k] = (int) $m[1] * 1024;
            }
        }
        if (!isset($v['MemTotal'])) {
            return ['ram_total' => '?', 'ram_used' => '?', 'ram_free' => '?', 'ram_percent' => '?'];
        }
        $total = $v['MemTotal'];
        $avail = $v['MemAvailable'] ?? ($v['MemFree'] + ($v['Buffers'] ?? 0) + ($v['Cached'] ?? 0));
        $used = $total - $avail;

        return [
            'ram_total' => self::fmtBytes($total),
            'ram_used' => self::fmtBytes($used),
            'ram_free' => self::fmtBytes($avail),
            'ram_percent' => round($used / $total * 100, 1),
        ];
    }

    /**
     * @return array{status: string, info: string}
     */
    private function checkCaddy(): array
    {
        try {
            $ch = curl_init($this->outsideUrl);
            curl_setopt_array($ch, [\CURLOPT_NOBODY => true, \CURLOPT_RETURNTRANSFER => true, \CURLOPT_TIMEOUT => 3, \CURLOPT_SSL_VERIFYPEER => false]);
            curl_exec($ch);
            $code = curl_getinfo($ch, \CURLINFO_HTTP_CODE);
            curl_close($ch);

            return $code > 0 ? ['status' => 'ok', 'info' => "HTTP {$code}"] : ['status' => 'error', 'info' => 'Pas de reponse'];
        } catch (\Throwable $e) {
            return ['status' => 'error', 'info' => $e->getMessage()];
        }
    }

    /**
     * @return array{status: string, info: string}
     */
    private function checkDocker(): array
    {
        $data = $this->dockerApi('/info');

        return $data ? ['status' => 'ok', 'info' => 'Containers: '.($data['ContainersRunning'] ?? '?').' running'] : ['status' => 'error', 'info' => 'Socket inaccessible'];
    }

    /**
     * @return array{status: string, domain: string, issuer: string, valid_until: string, days_left: int|string}
     */
    private function checkSsl(): array
    {
        try {
            $parsed = parse_url($this->outsideUrl);
            $host = $parsed['host'] ?? '';
            $ctx = stream_context_create(['ssl' => ['capture_peer_cert' => true, 'verify_peer' => false]]);
            $client = @stream_socket_client("ssl://{$host}:443", $errno, $errstr, 3, \STREAM_CLIENT_CONNECT, $ctx);
            if (!$client) {
                return ['status' => 'error', 'domain' => $host, 'issuer' => '?', 'valid_until' => '?', 'days_left' => '?'];
            }
            $params = stream_context_get_params($client);
            $cert = openssl_x509_parse($params['options']['ssl']['peer_certificate']);
            fclose($client);

            $validTo = new \DateTimeImmutable('@'.($cert['validTo_time_t'] ?? 0));
            $daysLeft = (int) (new \DateTimeImmutable())->diff($validTo)->format('%r%a');
            $issuer = $cert['issuer']['O'] ?? $cert['issuer']['CN'] ?? '?';

            return [
                'status' => $daysLeft > 7 ? 'ok' : ($daysLeft > 0 ? 'warning' : 'error'),
                'domain' => $host,
                'issuer' => $issuer,
                'valid_until' => $validTo->format('d/m/Y'),
                'days_left' => $daysLeft,
            ];
        } catch (\Throwable) {
            $host = parse_url($this->outsideUrl, \PHP_URL_HOST) ?? '?';

            return ['status' => 'error', 'domain' => $host, 'issuer' => '?', 'valid_until' => '?', 'days_left' => '?'];
        }
    }

    // ── Docker containers ────────────────────────────────────

    /**
     * @return list<array<string, mixed>>
     */
    private function getDockerContainers(): array
    {
        $containers = $this->dockerApi('/containers/json');
        if (!$containers) {
            return [];
        }

        $result = [];
        foreach ($containers as $c) {
            $name = ltrim($c['Names'][0] ?? '?', '/');
            $stats = $this->dockerApi('/containers/'.($c['Id'] ?? '').'/stats?stream=false');

            $cpu = $this->calcCpuPercent($stats);
            $mem = $this->calcMemory($stats);

            $result[] = [
                'name' => $name,
                'image' => $this->shortImage($c['Image'] ?? '?'),
                'state' => $c['State'] ?? '?',
                'status' => $c['Status'] ?? '?',
                'cpu_percent' => $cpu,
                'ram_used' => $mem['used'],
                'ram_limit' => $mem['limit'],
                'ram_percent' => $mem['percent'],
            ];
        }

        usort($result, fn ($a, $b) => $a['name'] <=> $b['name']);

        return $result;
    }

    /** @param array<string, mixed>|null $stats */
    private function calcCpuPercent(?array $stats): string
    {
        if (!$stats) {
            return '?';
        }
        $cpuDelta = ($stats['cpu_stats']['cpu_usage']['total_usage'] ?? 0) - ($stats['precpu_stats']['cpu_usage']['total_usage'] ?? 0);
        $sysDelta = ($stats['cpu_stats']['system_cpu_usage'] ?? 0) - ($stats['precpu_stats']['system_cpu_usage'] ?? 0);
        $cores = $stats['cpu_stats']['online_cpus'] ?? 1;

        if ($sysDelta > 0 && $cpuDelta >= 0) {
            return round($cpuDelta / $sysDelta * $cores * 100, 1).'%';
        }

        return '0%';
    }

    /**
     * @param array<string, mixed>|null $stats
     *
     * @return array{used: string, limit: string, percent: string}
     */
    private function calcMemory(?array $stats): array
    {
        if (!$stats) {
            return ['used' => '?', 'limit' => '?', 'percent' => '?'];
        }
        $used = $stats['memory_stats']['usage'] ?? 0;
        $limit = $stats['memory_stats']['limit'] ?? 0;
        $cache = $stats['memory_stats']['stats']['cache'] ?? 0;
        $actualUsed = $used - $cache;

        return [
            'used' => self::fmtBytes($actualUsed > 0 ? $actualUsed : $used),
            'limit' => $limit > 0 && $limit < 9_000_000_000_000_000_000 ? self::fmtBytes($limit) : 'No limit',
            'percent' => $limit > 0 && $limit < 9_000_000_000_000_000_000 ? round($actualUsed / $limit * 100, 1).'%' : 'N/A',
        ];
    }

    private function shortImage(string $image): string
    {
        if (str_contains($image, 'sha256:')) {
            return substr($image, 7, 12);
        }

        return str_contains($image, '/') ? substr($image, strrpos($image, '/') + 1) : $image;
    }

    /**
     * @return array<string, mixed>|null
     */
    private function dockerApi(string $path): ?array
    {
        $socket = '/var/run/docker.sock';
        if (!file_exists($socket)) {
            return null;
        }

        $ch = curl_init("http://localhost/v1.43{$path}");
        curl_setopt_array($ch, [
            \CURLOPT_UNIX_SOCKET_PATH => $socket,
            \CURLOPT_RETURNTRANSFER => true,
            \CURLOPT_TIMEOUT => 5,
        ]);
        $response = curl_exec($ch);
        $code = curl_getinfo($ch, \CURLINFO_HTTP_CODE);
        curl_close($ch);

        if (200 !== $code || false === $response) {
            return null;
        }

        return json_decode($response, true);
    }

    // ── Redis ────────────────────────────────────────────────

    /**
     * @return array<string, mixed>
     */
    public function getRedisGlobalInfo(): array
    {
        try {
            $redis = $this->connectRedis($this->messengerDsn);
            $info = $redis->info();
            $redis->close();

            $hits = (int) ($info['keyspace_hits'] ?? 0);
            $misses = (int) ($info['keyspace_misses'] ?? 0);
            $total = $hits + $misses;

            return [
                'connected' => true,
                'version' => $info['redis_version'] ?? '?',
                'uptime_days' => isset($info['uptime_in_seconds']) ? round((int) $info['uptime_in_seconds'] / 86400, 1) : '?',
                'used_memory_human' => $info['used_memory_human'] ?? '?',
                'used_memory_peak_human' => $info['used_memory_peak_human'] ?? '?',
                'connected_clients' => $info['connected_clients'] ?? '?',
                'total_commands_processed' => $info['total_commands_processed'] ?? '?',
                'keyspace_hits' => $hits,
                'keyspace_misses' => $misses,
                'hit_rate' => $total > 0 ? round($hits / $total * 100, 1).'%' : 'N/A',
                'expired_keys' => $info['expired_keys'] ?? '0',
                'evicted_keys' => $info['evicted_keys'] ?? '0',
                'ops_per_sec' => $info['instantaneous_ops_per_sec'] ?? '?',
                'role' => $info['role'] ?? '?',
            ];
        } catch (\Throwable $e) {
            return ['connected' => false, 'error' => $e->getMessage()];
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function getRedisDbInfo(string $dsn, string $label): array
    {
        try {
            $redis = $this->connectRedis($dsn);
            $dbSize = $redis->dbSize();
            $keys = $redis->keys('*');
            $withTtl = 0;
            $totalTtl = 0;
            $sampleSize = min(\count($keys), 50);
            for ($i = 0; $i < $sampleSize; ++$i) {
                $ttl = $redis->ttl($keys[$i]);
                if ($ttl > 0) {
                    ++$withTtl;
                    $totalTtl += $ttl;
                }
            }
            $redis->close();

            $parsed = parse_url(str_replace('redis://', 'http://', $dsn));
            $path = trim($parsed['path'] ?? '', '/');

            return [
                'label' => $label,
                'db' => is_numeric($path) ? 'DB '.$path : $path,
                'connected' => true,
                'keys' => $dbSize,
                'avg_ttl' => $withTtl > 0 ? self::fmtDuration((int) round($totalTtl / $withTtl)) : 'N/A',
            ];
        } catch (\Throwable $e) {
            return ['label' => $label, 'connected' => false, 'error' => $e->getMessage()];
        }
    }

    private function connectRedis(string $dsn): \Redis
    {
        $parsed = parse_url(str_replace('redis://', 'http://', $dsn));
        $host = $parsed['host'] ?? 'redis';
        $port = $parsed['port'] ?? 6379;
        $pass = null;
        if (isset($parsed['pass']) && '' !== $parsed['pass']) {
            $pass = urldecode($parsed['pass']);
        } elseif (isset($parsed['user']) && '' !== $parsed['user']) {
            $pass = urldecode($parsed['user']);
        }

        $redis = new \Redis();
        $redis->connect($host, (int) $port, 2);
        if (null !== $pass) {
            $redis->auth($pass);
        }
        $path = trim($parsed['path'] ?? '', '/');
        if ('' !== $path && is_numeric($path)) {
            $redis->select((int) $path);
        }

        return $redis;
    }

    // ── PostgreSQL ───────────────────────────────────────────

    /**
     * @return array<string, mixed>
     */
    private function getPostgresInfo(): array
    {
        try {
            $conn = $this->em->getConnection();

            return [
                'connected' => true,
                'version' => $conn->executeQuery('SHOW server_version')->fetchOne(),
                'db_size' => $conn->executeQuery('SELECT pg_size_pretty(pg_database_size(current_database()))')->fetchOne(),
                'active_connections' => $conn->executeQuery("SELECT count(*) FROM pg_stat_activity WHERE state = 'active'")->fetchOne(),
                'total_connections' => $conn->executeQuery('SELECT count(*) FROM pg_stat_activity')->fetchOne(),
                'max_connections' => $conn->executeQuery('SHOW max_connections')->fetchOne(),
                'uptime' => $conn->executeQuery('SELECT now() - pg_postmaster_start_time()')->fetchOne(),
                'cache_hit_ratio' => ($v = $conn->executeQuery('SELECT round(sum(blks_hit)::numeric / nullif(sum(blks_hit + blks_read), 0) * 100, 2) FROM pg_stat_database')->fetchOne()) ? $v.'%' : 'N/A',
                'dead_tuples' => $conn->executeQuery('SELECT sum(n_dead_tup) FROM pg_stat_user_tables')->fetchOne() ?? '0',
                'table_count' => $conn->executeQuery("SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'")->fetchOne(),
            ];
        } catch (\Throwable $e) {
            return ['connected' => false, 'error' => $e->getMessage()];
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function getPgBouncerInfo(): array
    {
        try {
            $parsed = parse_url($this->databaseUrl);
            $user = $parsed['user'] ?? 'app';
            $pass = $parsed['pass'] ?? '';
            $host = $parsed['host'] ?? 'pgbouncer';
            $port = (int) ($parsed['port'] ?? 6432);
            if (6432 !== $port) {
                $host = 'pgbouncer';
                $port = 6432;
            }

            $pdo = new \PDO("pgsql:host={$host};port={$port};dbname=pgbouncer", $user, $pass, [
                \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
                \PDO::ATTR_EMULATE_PREPARES => true,
            ]);

            $pools = [];
            foreach ($pdo->query('SHOW POOLS')->fetchAll(\PDO::FETCH_ASSOC) as $p) {
                if ('pgbouncer' === ($p['database'] ?? '')) {
                    continue;
                }
                $pools[] = $p;
            }

            $stats = [];
            foreach ($pdo->query('SHOW STATS')->fetchAll(\PDO::FETCH_ASSOC) as $s) {
                if ('pgbouncer' === ($s['database'] ?? '')) {
                    continue;
                }
                $stats[] = $s;
            }

            return ['connected' => true, 'pools' => $pools, 'stats' => $stats];
        } catch (\Throwable $e) {
            return ['connected' => false, 'error' => $e->getMessage()];
        }
    }

    // ── Helpers ──────────────────────────────────────────────

    public static function fmtBytes(int $bytes): string
    {
        if ($bytes >= 1_073_741_824) {
            return round($bytes / 1_073_741_824, 1).' GB';
        }
        if ($bytes >= 1_048_576) {
            return round($bytes / 1_048_576, 1).' MB';
        }

        return round($bytes / 1024, 1).' KB';
    }

    public static function fmtDuration(int $seconds): string
    {
        if ($seconds >= 86400) {
            return round($seconds / 86400, 1).'j';
        }
        if ($seconds >= 3600) {
            return round($seconds / 3600, 1).'h';
        }
        if ($seconds >= 60) {
            return round($seconds / 60).'min';
        }

        return $seconds.'s';
    }
}
