<?php

namespace App\Services;

use App\Events\ServerStatusChanged;
use App\Models\Server;
use App\Models\ServerMetric;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;

/**
 * Probes servers that don't have the agent installed. Reports up/down only.
 *
 * Adaptive scheduling:
 *   - On failure, exponential backoff (handled by ProbeBackoff)
 *   - On WAF-style block, set monitor_blocked = true and stop probing.
 *     The dashboard surfaces this as a banner: "Monitoring paused — your
 *     firewall is blocking us. Whitelist <NOC IP> in CSF, then click
 *     Resume."
 *   - On success, reset all backoff state.
 */
class ManualPingService
{
    public function ping(Server $server): array
    {
        if (!$server->isManualMode()) {
            return ['skipped' => true, 'reason' => 'not in manual mode'];
        }

        // Circuit breaker: once flagged blocked, refuse to probe until the
        // operator clears the flag. Hammering a firewall that already blocks
        // us makes things worse.
        if ($server->monitor_blocked) {
            return ['skipped' => true, 'reason' => 'monitor_blocked: ' . ($server->monitor_blocked_reason ?? 'manual review required')];
        }

        // Respect backoff window.
        if ($server->manual_next_check_at && $server->manual_next_check_at->isFuture()) {
            return [
                'skipped' => true,
                'reason' => 'backoff: next check at ' . $server->manual_next_check_at->toIso8601String(),
            ];
        }

        $previousStatus = $server->status;
        $start = microtime(true);

        $result = match ($server->manual_check_method ?? 'icmp') {
            'icmp' => $this->pingIcmp($server),
            'tcp' => $this->pingTcp($server),
            'http', 'https' => $this->pingHttp($server),
            default => ['ok' => false, 'error' => 'unknown method'],
        };

        $latencyMs = (int) ((microtime(true) - $start) * 1000);
        $newStatus = $result['ok'] ? Server::STATUS_ONLINE : Server::STATUS_OFFLINE;

        ServerMetric::create([
            'server_id' => $server->id,
            'measured_at' => now(),
            'cpu' => null,
            'ram' => null,
            'disk' => null,
            'extra' => [
                'manual_ping' => true,
                'method' => $server->manual_check_method,
                'latency_ms' => $result['ok'] ? $latencyMs : null,
                'error' => $result['error'] ?? null,
                'status_code' => $result['status_code'] ?? null,
            ],
        ]);

        // Mirror this check into location_check_results attributed to the
        // primary NOC probe. This feeds the multi-location consensus engine
        // and uptime aggregator the same way remote probes do. Without this
        // a single-NOC deployment would show 0% uptime forever.
        //
        // The recorder also keeps noc-primary's last_seen_at fresh so the
        // dashboard shows it as "healthy" — same heartbeat semantics as a
        // remote probe.
        app(\App\Services\PrimaryProbeRecorder::class)->record(
            $server,
            $result['ok'],
            $result['status_code'] ?? null,
            $result['ok'] ? $latencyMs : null,
            $result['error'] ?? null,
        );

        // Detect WAF-style block markers.
        $blockReason = null;
        if (!$result['ok']) {
            $blockReason = ProbeBackoff::detectBlock(
                $result['status_code'] ?? null,
                $result['headers'] ?? null,
                $result['body'] ?? null,
                $result['error'] ?? null,
            );
        }

        $update = [
            'status' => $newStatus,
            'last_metric_at' => now(),
        ];

        if ($result['ok']) {
            $update['last_seen_at'] = now();
            $update['manual_consecutive_failures'] = 0;
            $update['manual_next_check_at'] = null;
        } else {
            $failures = ($server->manual_consecutive_failures ?? 0) + 1;
            $update['manual_consecutive_failures'] = $failures;
            $update['manual_next_check_at'] = ProbeBackoff::nextCheckAt(
                $server->check_interval_seconds ?? 60,
                $failures,
            );

            if ($blockReason) {
                $update['monitor_blocked'] = true;
                $update['monitor_blocked_at'] = now();
                $update['monitor_blocked_reason'] = mb_substr($blockReason, 0, 250);
                Log::warning("Monitor blocked for server #{$server->id} ({$server->name}): {$blockReason}");
            } elseif ($failures >= ProbeBackoff::MAX_CONSECUTIVE_FAILURES) {
                $update['monitor_blocked'] = true;
                $update['monitor_blocked_at'] = now();
                $update['monitor_blocked_reason'] = "Server has been unreachable for {$failures} consecutive checks.";
            }
        }

        $server->update($update);

        if ($newStatus !== $previousStatus) {
            event(new ServerStatusChanged($server, $previousStatus));
        }

        return $result + [
            'latency_ms' => $latencyMs,
            'status' => $newStatus,
            'consecutive_failures' => $update['manual_consecutive_failures'],
            'blocked' => $update['monitor_blocked'] ?? false,
        ];
    }

    protected function pingIcmp(Server $server): array
    {
        $timeout = max(1, (int) ($server->manual_timeout_seconds ?? 5));
        $proc = Process::timeout($timeout + 2)
            ->run(['ping', '-c', '1', '-W', (string) $timeout, $server->ip]);

        return [
            'ok' => $proc->successful(),
            'method' => 'icmp',
            'error' => $proc->successful() ? null : trim($proc->errorOutput() ?: $proc->output()),
        ];
    }

    protected function pingTcp(Server $server): array
    {
        $port = $server->manual_check_port ?? 22;
        $timeout = max(1, (int) ($server->manual_timeout_seconds ?? 5));

        $errno = 0; $errstr = '';
        $sock = @fsockopen($server->ip, $port, $errno, $errstr, $timeout);
        if ($sock) { fclose($sock); return ['ok' => true, 'method' => 'tcp', 'port' => $port]; }
        return ['ok' => false, 'method' => 'tcp', 'port' => $port, 'error' => $errstr ?: "errno {$errno}"];
    }

    protected function pingHttp(Server $server): array
    {
        $scheme = $server->manual_check_method === 'https' ? 'https' : 'http';
        $port = $server->manual_check_port ?: ($scheme === 'https' ? 443 : 80);
        $path = $server->manual_check_path ?: '/';
        $url = "{$scheme}://{$server->ip}:{$port}{$path}";

        try {
            $client = new Client([
                'timeout' => $server->manual_timeout_seconds ?? 5,
                'connect_timeout' => 3,
                'allow_redirects' => true,
                'verify' => false,
                'http_errors' => false,
                'headers' => [
                    'User-Agent' => ProbeBackoff::userAgent(),
                    'Accept-Encoding' => 'gzip, deflate',
                    'Cache-Control' => 'no-cache',
                ],
            ]);
            $res = $client->get($url);
            $code = $res->getStatusCode();
            $body = (string) $res->getBody();
            return [
                'ok' => $code >= 200 && $code < 400,
                'method' => $scheme,
                'status_code' => $code,
                'headers' => array_change_key_case($res->getHeaders(), CASE_LOWER),
                'body' => mb_substr($body, 0, 2048),
            ];
        } catch (\Throwable $e) {
            return ['ok' => false, 'method' => $scheme, 'error' => $e->getMessage()];
        }
    }
}
