<?php

namespace App\Services;

use App\Events\MetricRecorded;
use App\Events\ServerStatusChanged;
use App\Models\Server;
use App\Models\ServerMetric;
use Illuminate\Support\Facades\Log;
use phpseclib3\Net\SSH2;
use phpseclib3\Crypt\PublicKeyLoader;

/**
 * Collects live metrics from a single server via SSH and persists them.
 *
 * The agent expects a small shell snippet to be runnable on the target
 * (mpstat / free / df / uptime / iostat). On cPanel servers all of these
 * are present by default. The output is parsed to a flat metrics array.
 */
class ServerScraperService
{
    /**
     * Run a single scrape pass for one server.
     */
    public function scrape(Server $server): ?ServerMetric
    {
        $previousStatus = $server->status;

        try {
            $output = $this->sshExec($server, $this->scrapeScript());
            $metrics = $this->parseOutput($output);

            $metric = $server->metrics()->create([
                'measured_at' => now(),
                ...$metrics,
            ]);

            $server->update([
                'status' => $this->classify($metrics),
                'cpu' => $metrics['cpu'] ?? null,
                'ram' => $metrics['ram'] ?? null,
                'disk' => $metrics['disk'] ?? null,
                'load_1m' => $metrics['load_1m'] ?? null,
                'load_5m' => $metrics['load_5m'] ?? null,
                'load_15m' => $metrics['load_15m'] ?? null,
                'net_in_mbps' => $metrics['net_in_mbps'] ?? null,
                'net_out_mbps' => $metrics['net_out_mbps'] ?? null,
                'services' => $metrics['services'] ?? $server->services,
                'uptime_human' => $metrics['uptime_human'] ?? $server->uptime_human,
                'uptime_seconds' => $metrics['uptime_seconds'] ?? $server->uptime_seconds,
                'last_seen_at' => now(),
                'last_metric_at' => now(),
            ]);

            event(new MetricRecorded($server, $metric));
            if ($server->status !== $previousStatus) {
                event(new ServerStatusChanged($server, $previousStatus));
            }

            return $metric;
        } catch (\Throwable $e) {
            Log::warning("Scrape failed for {$server->name}: " . $e->getMessage());
            $server->update(['status' => 'offline', 'last_seen_at' => $server->last_seen_at]);
            if ($server->status !== $previousStatus) {
                event(new ServerStatusChanged($server, $previousStatus));
            }
            return null;
        }
    }

    /**
     * Decide a server's overall status from collected metrics + service health.
     */
    protected function classify(array $m): string
    {
        $cpu = $m['cpu'] ?? 0;
        $ram = $m['ram'] ?? 0;
        $disk = $m['disk'] ?? 0;
        $services = $m['services'] ?? [];

        if (in_array('down', $services, true)) return Server::STATUS_WARNING;
        if ($cpu > 90 || $ram > 90 || $disk > 95) return Server::STATUS_WARNING;
        if ($cpu > 80 || $ram > 80 || $disk > 88) return Server::STATUS_WARNING;

        return Server::STATUS_ONLINE;
    }

    /**
     * The shell script we ship to the server. Output is plain key=value lines.
     * Service checks are issued via systemctl is-active.
     */
    protected function scrapeScript(): string
    {
        return <<<'BASH'
        #!/usr/bin/env bash
        set -eu

        # CPU (% user + sys, sampled over 1s via top)
        cpu=$(LC_ALL=C top -bn2 -d 1 | awk '/^%Cpu/ {usr=$2; sys=$4} END {printf "%.2f", usr+sys}')
        echo "cpu=${cpu}"

        # RAM
        free -m | awk '/^Mem:/ {printf "ram=%.2f\n", ($3/$2)*100}'

        # Root disk
        df -P / | awk 'NR==2 {gsub("%","",$5); printf "disk=%.2f\n", $5}'

        # Load
        read l1 l5 l15 _ < /proc/loadavg
        echo "load_1m=${l1}"
        echo "load_5m=${l5}"
        echo "load_15m=${l15}"

        # Uptime seconds
        read up _ < /proc/uptime
        echo "uptime_seconds=${up%.*}"
        echo "uptime_human=$(uptime -p)"

        # Network throughput (sum of all interfaces, excluding lo)
        rx1=$(awk 'NR>2 && $1!~"lo:" {sub(":","",$1); s+=$2} END{print s}' /proc/net/dev)
        tx1=$(awk 'NR>2 && $1!~"lo:" {sub(":","",$1); s+=$10} END{print s}' /proc/net/dev)
        sleep 1
        rx2=$(awk 'NR>2 && $1!~"lo:" {sub(":","",$1); s+=$2} END{print s}' /proc/net/dev)
        tx2=$(awk 'NR>2 && $1!~"lo:" {sub(":","",$1); s+=$10} END{print s}' /proc/net/dev)
        echo "net_in_mbps=$(awk -v a="$rx1" -v b="$rx2" 'BEGIN{printf "%.2f",((b-a)*8/1000000)}')"
        echo "net_out_mbps=$(awk -v a="$tx1" -v b="$tx2" 'BEGIN{printf "%.2f",((b-a)*8/1000000)}')"

        # Services on cPanel-style servers
        for svc in litespeed mysql mariadb exim dovecot apache nginx redis memcached; do
            if systemctl is-active --quiet "$svc"; then
                echo "svc.$svc=up"
            elif systemctl list-unit-files --type=service 2>/dev/null | grep -q "^$svc"; then
                echo "svc.$svc=down"
            fi
        done
        BASH;
    }

    protected function parseOutput(string $output): array
    {
        $out = ['services' => []];
        foreach (explode("\n", trim($output)) as $line) {
            if (!str_contains($line, '=')) continue;
            [$k, $v] = array_map('trim', explode('=', $line, 2));

            if (str_starts_with($k, 'svc.')) {
                $out['services'][substr($k, 4)] = $v;
                continue;
            }

            $out[$k] = is_numeric($v) ? (float) $v : $v;
        }
        return $out;
    }

    protected function sshExec(Server $server, string $command): string
    {
        $ssh = new SSH2($server->ip, $server->ssh_port ?? 22, 8);
        $ssh->setTimeout(10);

        if (empty($server->ssh_key_encrypted)) {
            throw new \RuntimeException("No SSH key configured for {$server->name}");
        }

        $key = PublicKeyLoader::load($server->ssh_key_encrypted);
        if (!$ssh->login($server->ssh_user ?? 'root', $key)) {
            throw new \RuntimeException("SSH login failed for {$server->name}");
        }

        $result = $ssh->exec($command);
        if ($ssh->getExitStatus() !== 0) {
            throw new \RuntimeException("Scrape command exited non-zero on {$server->name}");
        }

        return $result;
    }
}
