<?php

namespace App\Services;

use App\Enums\Role;
use App\Mail\AlertNotification;
use App\Models\AlertIncident;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Throwable;

/**
 * Fans one alert transition (fire / resolve) out to the configured channels. Every channel is
 * BEST-EFFORT: a broken SMTP config or an unreachable webhook is logged, never thrown, so a
 * notification failure can never break the poll loop that triggered it.
 */
class AlertNotifier
{
    public function notify(AlertIncident $incident, bool $resolved): void
    {
        $this->email($incident, $resolved);
        $this->webhooks($incident, $resolved);
    }

    private function email(AlertIncident $incident, bool $resolved): void
    {
        try {
            $recipients = $this->recipients();
            if ($recipients === []) {
                return;
            }
            Mail::to($recipients)->queue(new AlertNotification($incident, $resolved));
        } catch (Throwable $e) {
            Log::warning('alert email failed: '.$e->getMessage());
        }
    }

    private function webhooks(AlertIncident $incident, bool $resolved): void
    {
        $payload = $this->payload($incident, $resolved);

        foreach ($this->webhookUrls() as $url) {
            try {
                // withoutRedirecting: a 30x could otherwise bounce the POST to an internal target that
                // the SSRF host-check below never saw. timeout keeps a slow hook from stalling the loop.
                Http::timeout(5)->withoutRedirecting()->asJson()->post($url, $payload);
            } catch (Throwable $e) {
                Log::warning('alert webhook failed: '.$e->getMessage());
            }
        }
    }

    /**
     * SSRF guard for an operator-configured webhook URL: exactly http(s), and the host must NOT
     * resolve to a loopback/private/link-local/reserved address (so a hook can't be pointed at an
     * internal service). Admin-only config, but defence in depth — checked here AND at save time.
     */
    public static function isSafeWebhookUrl(string $url): bool
    {
        $parts = parse_url(trim($url));
        if ($parts === false || ! isset($parts['scheme'], $parts['host'])) {
            return false;
        }
        if (! in_array(strtolower($parts['scheme']), ['http', 'https'], true)) {
            return false;
        }

        $host = $parts['host'];
        // IP literal → itself; hostname → its A records (IPv4). Unresolvable / IPv6-only → refuse.
        $ips = filter_var($host, FILTER_VALIDATE_IP) ? [$host] : (gethostbynamel($host) ?: []);
        if ($ips === []) {
            return false;
        }

        foreach ($ips as $ip) {
            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
                return false; // loopback/private/link-local/reserved → blocked
            }
        }

        return true;
    }

    /** @return list<string> */
    private function recipients(): array
    {
        $configured = $this->splitLines((string) Setting::get('alert_email_to', ''));
        $emails = array_values(array_filter($configured, fn (string $e) => filter_var($e, FILTER_VALIDATE_EMAIL) !== false));

        if ($emails !== []) {
            return $emails;
        }

        // Fall back to every admin's e-mail so a fresh install still alerts someone.
        return User::query()->where('role', Role::Admin->value)
            ->whereNotNull('email')
            ->pluck('email')->all();
    }

    /** @return list<string> */
    private function webhookUrls(): array
    {
        return array_values(array_filter(
            $this->splitLines((string) Setting::get('alert_webhooks', '')),
            fn (string $u) => self::isSafeWebhookUrl($u),
        ));
    }

    /** @return array<string, mixed> */
    private function payload(AlertIncident $incident, bool $resolved): array
    {
        $server = $incident->server?->name ?? '—';
        $rule = $incident->rule?->name ?? '—';
        $state = $resolved ? 'resolved' : 'firing';
        $text = $resolved
            ? __('alerts.mail_subject_resolved')." — {$rule} ({$server})"
            : __('alerts.mail_subject_firing')." — {$rule} ({$server})";

        // A single `text` field satisfies Slack/Discord/Mattermost/Telegram-relay incoming webhooks;
        // the structured fields let a custom consumer route on server/metric/state.
        return [
            'text' => $text,
            'state' => $state,
            'server' => $server,
            'rule' => $rule,
            'metric' => $incident->rule?->metric,
            'value' => $incident->value,
            'threshold' => $incident->rule?->threshold,
        ];
    }

    /** @return list<string> */
    private function splitLines(string $raw): array
    {
        return array_values(array_filter(array_map('trim', preg_split('/[\r\n,]+/', $raw) ?: [])));
    }
}
