<?php

namespace App\Domains\Webhook\Jobs;

use App\Domains\Webhook\Models\Webhook;
use App\Domains\Webhook\Models\WebhookDelivery;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Http;

class DeliverWebhookJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public int $tries = 5;

    public int $backoff = 60;

    /**
     * @param  array<string, mixed>  $payload
     */
    public function __construct(
        public readonly Webhook $webhook,
        public readonly string $event,
        public readonly array $payload,
    ) {}

    public function handle(): void
    {
        $body = json_encode([
            'event' => $this->event,
            'payload' => $this->payload,
            'timestamp' => now()->toISOString(),
        ], JSON_THROW_ON_ERROR);

        $signature = 'sha256='.hash_hmac('sha256', $body, $this->webhook->secret);

        $host = parse_url($this->webhook->url, PHP_URL_HOST);
        if ($this->isPrivateHost($host)) {
            return;
        }

        $decodedPayload = json_decode($body, true, 512, JSON_THROW_ON_ERROR);

        $response = Http::timeout(10)
            ->withHeaders([
                'Content-Type' => 'application/json',
                'X-Nimuli-Signature' => $signature,
                'X-Nimuli-Event' => $this->event,
            ])
            ->withBody($body, 'application/json')
            ->post($this->webhook->url);

        WebhookDelivery::create([
            'webhook_id' => $this->webhook->id,
            'event' => $this->event,
            'payload' => $decodedPayload,
            'status' => $response->successful() ? 'success' : 'failed',
            'response_code' => $response->status(),
            'attempts' => max(1, $this->attempts()),
        ]);
    }

    private function isPrivateHost(?string $host): bool
    {
        if (! $host) {
            return true;
        }

        // Strip IPv6 brackets: [::1] → ::1
        $host = ltrim(rtrim($host, ']'), '[');

        // If it's already an IP, check it directly (bypasses DNS resolution)
        if (filter_var($host, FILTER_VALIDATE_IP)) {
            return ! filter_var(
                $host,
                FILTER_VALIDATE_IP,
                FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
            ) || $this->isPrivateIPv6($host);
        }

        // Resolve hostname to IPv4
        $ip = gethostbyname($host);
        if ($ip === $host) {
            // Could not resolve or is an IPv6 hostname that gethostbyname can't handle — block
            return true;
        }

        return ! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
    }

    private function isPrivateIPv6(string $ip): bool
    {
        if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            return false;
        }

        // Block loopback, link-local, unique local, unspecified
        $privateRanges = ['::1', 'fe80::', 'fc00::', 'fd00::', '::'];
        foreach ($privateRanges as $range) {
            if (str_starts_with($ip, $range)) {
                return true;
            }
        }

        return false;
    }
}
