<?php

declare(strict_types=1);

namespace App\Domain\System\Services;

use Illuminate\Support\Facades\DB;

/**
 * Gerçek sistem metrik toplayıcısı (Linux /proc + PHP yerleşik fonksiyonlar).
 *
 * Bu servis, sysapi köprüsünden GEÇMEZ — okuduğu dosyalar dünya-okunabilir
 * (/proc/stat, /proc/meminfo, /proc/uptime, /proc/net/dev) olduğundan
 * doğrudan PHP işlemi tarafından erişilebilir. shell_exec yalnızca `nproc`
 * için fallback olarak kullanılır.
 *
 * Windows geliştirme ortamı: is_readable('/proc/*') false döner — bu durumda
 * servis sıfır değerler + `is_windows_dev=true` flag ile zarif fallback yapar.
 * Controller bu bayrağı Vue'ya geçirerek "dev mode" banner gösterilebilir.
 */
final class SystemMetrics
{
    /** /proc tabanlı metrik okumaları destekleniyor mu? (Linux = true). */
    public function isLinux(): bool
    {
        return is_readable('/proc/stat');
    }

    public function isWindowsDev(): bool
    {
        return ! $this->isLinux();
    }

    /**
     * CPU kullanımı + load average + core sayısı.
     *
     * /proc/stat'ı 200ms arayla iki kez okuyup idle/total diff'inden anlık % hesaplar.
     *
     * @return array{percent: float, cores: int, model: string, load_1m: float, load_5m: float, load_15m: float}
     */
    public function cpu(): array
    {
        if (! $this->isLinux()) {
            return [
                'percent' => 0.0,
                'cores' => (int) (getenv('NUMBER_OF_PROCESSORS') ?: 1),
                'model' => 'Windows CPU (dev)',
                'load_1m' => 0.0,
                'load_5m' => 0.0,
                'load_15m' => 0.0,
            ];
        }

        $sample1 = $this->readProcStat();
        usleep(200_000); // 200ms
        $sample2 = $this->readProcStat();

        $totalDiff = $sample2['total'] - $sample1['total'];
        $idleDiff = $sample2['idle'] - $sample1['idle'];
        $usage = $totalDiff > 0 ? round(100 * (1 - $idleDiff / $totalDiff), 2) : 0.0;

        $load = function_exists('sys_getloadavg') ? (sys_getloadavg() ?: [0, 0, 0]) : [0, 0, 0];
        $cores = $this->detectCpuCores();
        $model = $this->detectCpuModel();

        return [
            'percent' => (float) $usage,
            'cores' => $cores,
            'model' => $model,
            'load_1m' => round((float) $load[0], 2),
            'load_5m' => round((float) $load[1], 2),
            'load_15m' => round((float) $load[2], 2),
        ];
    }

    /**
     * Bellek kullanımı — /proc/meminfo parse.
     *
     * @return array{total_bytes:int,used_bytes:int,available_bytes:int,percent:float,swap_total_bytes:int,swap_used_bytes:int}
     */
    public function memory(): array
    {
        $defaults = [
            'total_bytes' => 0,
            'used_bytes' => 0,
            'available_bytes' => 0,
            'percent' => 0.0,
            'swap_total_bytes' => 0,
            'swap_used_bytes' => 0,
        ];

        if (! is_readable('/proc/meminfo')) {
            return $defaults;
        }

        $info = (string) @file_get_contents('/proc/meminfo');
        if ($info === '') {
            return $defaults;
        }

        preg_match_all('/^(\w+):\s+(\d+)/m', $info, $m, PREG_SET_ORDER);
        $kv = [];
        foreach ($m as $row) {
            // kB → bytes
            $kv[$row[1]] = (int) $row[2] * 1024;
        }

        $total = $kv['MemTotal'] ?? 0;
        $available = $kv['MemAvailable'] ?? ($kv['MemFree'] ?? 0);
        $used = max(0, $total - $available);

        return [
            'total_bytes' => $total,
            'used_bytes' => $used,
            'available_bytes' => $available,
            'percent' => $total > 0 ? round(100 * $used / $total, 2) : 0.0,
            'swap_total_bytes' => $kv['SwapTotal'] ?? 0,
            'swap_used_bytes' => max(0, ($kv['SwapTotal'] ?? 0) - ($kv['SwapFree'] ?? 0)),
        ];
    }

    /**
     * Tek bir mount point için disk kullanımı.
     *
     * @return array{path:string,total_bytes:int,free_bytes:int,used_bytes:int,percent:float}
     */
    public function disk(string $path = '/'): array
    {
        // Windows dev fallback
        if (! $this->isLinux() && $path === '/') {
            $path = 'C:\\';
        }

        $total = (int) (@disk_total_space($path) ?: 0);
        $free = (int) (@disk_free_space($path) ?: 0);
        $used = max(0, $total - $free);

        return [
            'path' => $path,
            'total_bytes' => $total,
            'free_bytes' => $free,
            'used_bytes' => $used,
            'percent' => $total > 0 ? round(100 * $used / $total, 2) : 0.0,
        ];
    }

    /**
     * Birden fazla mount point.
     *
     * @return array<int, array{path:string,total_bytes:int,free_bytes:int,used_bytes:int,percent:float}>
     */
    public function disks(): array
    {
        if (! $this->isLinux()) {
            return [$this->disk('/')];
        }

        $points = ['/', '/home', '/var', '/tmp'];
        $results = [];
        foreach ($points as $p) {
            // Yalnızca gerçekten var olan/ölçülebilen mount'ları döndür
            if (! @is_dir($p)) {
                continue;
            }
            $info = $this->disk($p);
            if ($info['total_bytes'] > 0) {
                $results[] = $info;
            }
        }

        return $results !== [] ? $results : [$this->disk('/')];
    }

    /**
     * Disk için inode istatistiği (Linux). disk_total_space inode vermez, df gerekiyor.
     * Best-effort: shell_exec mevcut değilse 0 döner.
     *
     * @return array{total:int,used:int,percent:float}
     */
    public function inodes(string $path = '/'): array
    {
        if (! $this->isLinux() || ! function_exists('shell_exec')) {
            return ['total' => 0, 'used' => 0, 'percent' => 0.0];
        }

        $escaped = escapeshellarg($path);
        $out = @shell_exec("df -i -P {$escaped} 2>/dev/null | tail -1");
        if (! is_string($out) || $out === '') {
            return ['total' => 0, 'used' => 0, 'percent' => 0.0];
        }

        $parts = preg_split('/\s+/', trim($out)) ?: [];
        $total = (int) ($parts[1] ?? 0);
        $used = (int) ($parts[2] ?? 0);

        return [
            'total' => $total,
            'used' => $used,
            'percent' => $total > 0 ? round(100 * $used / $total, 2) : 0.0,
        ];
    }

    /**
     * Ağ arayüzleri — /proc/net/dev parse (lo hariç).
     *
     * @return array{interfaces:array<int,array{name:string,rx_bytes:int,tx_bytes:int}>, rx_total:int, tx_total:int}
     */
    public function network(): array
    {
        if (! is_readable('/proc/net/dev')) {
            return ['interfaces' => [], 'rx_total' => 0, 'tx_total' => 0];
        }

        $lines = @file('/proc/net/dev', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if ($lines === false) {
            return ['interfaces' => [], 'rx_total' => 0, 'tx_total' => 0];
        }

        $interfaces = [];
        $rxTotal = 0;
        $txTotal = 0;
        // İlk iki satır header — atla
        foreach (array_slice($lines, 2) as $line) {
            $parts = preg_split('/\s+/', trim($line)) ?: [];
            $iface = isset($parts[0]) ? rtrim((string) $parts[0], ':') : '';
            if ($iface === '' || $iface === 'lo') {
                continue;
            }
            $rx = (int) ($parts[1] ?? 0);
            $tx = (int) ($parts[9] ?? 0);
            $interfaces[] = [
                'name' => $iface,
                'rx_bytes' => $rx,
                'tx_bytes' => $tx,
            ];
            $rxTotal += $rx;
            $txTotal += $tx;
        }

        return [
            'interfaces' => $interfaces,
            'rx_total' => $rxTotal,
            'tx_total' => $txTotal,
        ];
    }

    /**
     * Aktif TCP/UDP bağlantı sayısı (best-effort).
     * /proc/net/tcp + /proc/net/tcp6 line count.
     */
    public function activeConnections(): int
    {
        $count = 0;
        foreach (['/proc/net/tcp', '/proc/net/tcp6'] as $file) {
            if (! is_readable($file)) {
                continue;
            }
            $lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
            if (is_array($lines) && count($lines) > 1) {
                $count += count($lines) - 1; // header
            }
        }

        return $count;
    }

    /**
     * Sistem uptime.
     *
     * @return array{seconds:int,days:int,human:string,boot_at:string|null}
     */
    public function uptime(): array
    {
        if (! is_readable('/proc/uptime')) {
            return [
                'seconds' => 0,
                'days' => 0,
                'human' => '—',
                'boot_at' => null,
            ];
        }

        $line = (string) @file_get_contents('/proc/uptime');
        $seconds = (float) (explode(' ', trim($line))[0] ?? 0);

        return [
            'seconds' => (int) $seconds,
            'days' => (int) floor($seconds / 86400),
            'human' => $this->humanizeUptime($seconds),
            'boot_at' => now()->subSeconds((int) $seconds)->toIso8601String(),
        ];
    }

    /**
     * Son 24 saatlik system-level load/cpu/memory history (snapshotSystemLevel'in yazdığı tabloyu okur).
     *
     * @return array<int, array{recorded_at:string, cpu_percent:float, memory_bytes:int, load_1m:float}>
     */
    public function loadHistory(int $hours = 24): array
    {
        try {
            $rows = DB::table('system_metrics_history')
                ->where('recorded_at', '>=', now()->subHours($hours))
                ->orderBy('recorded_at')
                ->get(['recorded_at', 'cpu_percent', 'memory_bytes', 'load_1m'])
                ->toArray();

            return array_map(fn ($r) => [
                'recorded_at' => (string) ($r->recorded_at ?? ''),
                'cpu_percent' => (float) ($r->cpu_percent ?? 0),
                'memory_bytes' => (int) ($r->memory_bytes ?? 0),
                'load_1m' => (float) ($r->load_1m ?? 0),
            ], $rows);
        } catch (\Throwable $e) {
            // Tablo henüz migrate edilmediyse boş array dön
            return [];
        }
    }

    /**
     * Sistem seviyesinde tek bir snapshot al ve `system_metrics_history` tablosuna yaz.
     * routes/console.php → Schedule::call() tarafından her 5 dk çağrılır.
     */
    public function snapshotSystemLevel(): void
    {
        if (! $this->isLinux()) {
            return; // Dev'de snapshot yazma
        }

        try {
            $cpu = $this->cpu();
            $mem = $this->memory();
            $net = $this->network();

            DB::table('system_metrics_history')->insert([
                'cpu_percent' => $cpu['percent'],
                'memory_bytes' => $mem['used_bytes'],
                'memory_percent' => $mem['percent'],
                'load_1m' => $cpu['load_1m'],
                'load_5m' => $cpu['load_5m'],
                'load_15m' => $cpu['load_15m'],
                'network_rx_bytes' => $net['rx_total'],
                'network_tx_bytes' => $net['tx_total'],
                'recorded_at' => now(),
                'created_at' => now(),
            ]);
        } catch (\Throwable $e) {
            // Tablo yoksa veya DB hatası — sessizce yut, scheduler bir sonraki tickte tekrar dener
        }
    }

    /**
     * Tüm metriklerin toplu özetini döndür — controller için convenience.
     *
     * @return array<string, mixed>
     */
    public function snapshot(): array
    {
        return [
            'cpu' => $this->cpu(),
            'memory' => $this->memory(),
            'disk' => $this->disk('/'),
            'disks' => $this->disks(),
            'inodes' => $this->inodes('/'),
            'network' => $this->network(),
            'connections' => $this->activeConnections(),
            'uptime' => $this->uptime(),
            'is_windows_dev' => $this->isWindowsDev(),
        ];
    }

    // ─── Private helpers ────────────────────────────────────────────────────

    /**
     * @return array{total:int, idle:int}
     */
    private function readProcStat(): array
    {
        $content = (string) @file_get_contents('/proc/stat');
        $line = strtok($content, "\n");
        if (! is_string($line)) {
            return ['total' => 0, 'idle' => 0];
        }
        $parts = preg_split('/\s+/', trim($line)) ?: [];
        array_shift($parts); // 'cpu' prefix
        $nums = array_map('intval', $parts);

        // Sırasıyla: user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice
        $idle = ($nums[3] ?? 0) + ($nums[4] ?? 0); // idle + iowait
        $total = array_sum($nums);

        return ['total' => $total, 'idle' => $idle];
    }

    private function detectCpuCores(): int
    {
        // 1) /proc/cpuinfo'da processor satır sayısı
        if (is_readable('/proc/cpuinfo')) {
            $content = (string) @file_get_contents('/proc/cpuinfo');
            $count = substr_count($content, "\nprocessor");
            if ($content !== '' && str_starts_with($content, 'processor')) {
                $count++;
            }
            if ($count > 0) {
                return $count;
            }
        }

        // 2) nproc fallback
        if (function_exists('shell_exec')) {
            $out = @shell_exec('nproc 2>/dev/null');
            if (is_string($out) && trim($out) !== '') {
                return (int) trim($out);
            }
        }

        // 3) Windows env var
        $env = getenv('NUMBER_OF_PROCESSORS');
        if ($env !== false && $env !== '') {
            return (int) $env;
        }

        return 1;
    }

    private function detectCpuModel(): string
    {
        if (is_readable('/proc/cpuinfo')) {
            $content = (string) @file_get_contents('/proc/cpuinfo');
            if (preg_match('/model name\s+:\s+(.+)/', $content, $m)) {
                return trim($m[1]);
            }
        }

        return 'Unknown CPU';
    }

    private function humanizeUptime(float $seconds): string
    {
        $s = (int) $seconds;
        if ($s < 60) {
            return "{$s} sn";
        }
        $days = intdiv($s, 86400);
        $hours = intdiv($s % 86400, 3600);
        $minutes = intdiv($s % 3600, 60);

        $parts = [];
        if ($days > 0) {
            $parts[] = "{$days} gün";
        }
        if ($hours > 0) {
            $parts[] = "{$hours} sa";
        }
        if ($minutes > 0 && $days === 0) {
            $parts[] = "{$minutes} dk";
        }

        return implode(' ', $parts) ?: '< 1 dk';
    }
}
