<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class GameFilters implements Rule
{
    public const VALID_FILTERS = [
        'fivem', 'fivemx', 'rust', 'source1', 'source2', 'dayz', 'scpsl', 'arksa', 'arkse', 'reforger', 'a2s', 'hll', 'squad', 'bedrock', 'sip', 'udp_generic', 'udp_privileged', 'tcp_generic', 'tcp_privileged', 'tcp_inbound', 'udp_inbound', 'tcp_and_udp_inbound', 'udp_tcp_gate', 'minecraft', 'fivem_double_handshake', 'wireguard', 'http_inbound', 'tls_inbound', 'ssh_inbound',
        'fivemx_double_handshake', 'samp', 'mtasa', 'mtasa_ase', 'openvpn', 'teamspeak3', 'srt', 'ephemeral', 'gamespy',
        'fivem_stateless', 'fivemx_stateless', 'rust_stateless', 'source1_stateless', 'source2_stateless', 'dayz_stateless', 'scpsl_stateless', 'arksa_stateless', 'arkse_stateless', 'reforger_stateless', 'a2s_stateless', 'hll_stateless', 'squad_stateless', 'bedrock_stateless', 'sip_stateless', 'samp_stateless', 'mtasa_stateless', 'gamespy_stateless'
    ];

    private const MIN_PORT = 1;
    private const MAX_PORT = 65535;

    private $reason = 'No reason';

    public function passes($attribute, $value)
    {
        if (!is_array($value)) {
            $this->reason = 'The :attribute must be an array.';
            return false;
        }

        // Reject unknown filter keys up-front so typos don't silently pass.
        foreach (array_keys($value) as $key) {
            if (!in_array($key, self::VALID_FILTERS, true)) {
                $this->reason = "The `:attribute` contains an unknown filter `$key`.";
                return false;
            }
        }

        $expanded = [];

        foreach (self::VALID_FILTERS as $filter) {
            if (!isset($value[$filter]) || $value[$filter] === null) {
                continue;
            }

            $entries = $value[$filter];

            if (!is_array($entries)) {
                $this->reason = "The `:attribute` has an invalid `$filter` filter that must be an array of ports or port ranges.";
                return false;
            }

            if ($entries === []) {
                // An empty array is treated as "no filter set" - skip it.
                continue;
            }

            $expanded[$filter] = [];

            foreach ($entries as $entry) {
                if (!is_string($entry) && !is_int($entry)) {
                    $this->reason = "The `:attribute` has an invalid `$filter` entry that is not a number or range.";
                    return false;
                }

                $entry = (string) $entry;

                $ports = $this->expandEntry($entry, $filter);

                if ($ports === null) {
                    return false;
                }

                // Check this new entry does not overlap with earlier entries
                // in the same filter before folding it in.
                if ($this->overlapsExisting($ports, $expanded[$filter])) {
                    $this->reason = "The `:attribute` has overlapping ranges within the `$filter` filter.";
                    return false;
                }

                foreach ($ports as $port) {
                    $expanded[$filter][$port] = true;
                }
            }
        }

        $filterKeys = array_keys($expanded);

        for ($i = 0; $i < count($filterKeys) - 1; $i++) {
            for ($j = $i + 1; $j < count($filterKeys); $j++) {
                $a = $filterKeys[$i];
                $b = $filterKeys[$j];

                if (array_intersect_key($expanded[$a], $expanded[$b]) !== []) {
                    $this->reason = 'The :attribute must not have overlapping ports between filters.';
                    return false;
                }
            }
        }

        return true;
    }

    private function expandEntry(string $entry, string $filter): ?array
    {
        // Single port.
        if (preg_match('/^\d+$/', $entry)) {
            $port = (int) $entry;

            if (!$this->isValidPort($port)) {
                $this->reason = "The `:attribute` `$filter` filter has a port outside the valid range (1-65535).";
                return null;
            }

            return [$port];
        }

        // Range.
        if (preg_match('/^(\d+)-(\d+)$/', $entry, $matches)) {
            $start = (int) $matches[1];
            $end = (int) $matches[2];

            if (!$this->isValidPort($start) || !$this->isValidPort($end)) {
                $this->reason = "The `:attribute` `$filter` filter has a port outside the valid range (1-65535).";
                return null;
            }

            if ($end <= $start) {
                $this->reason = "The `:attribute` `$filter` filter cannot have a range where the second port is less than or equal to the first.";
                return null;
            }

            return range($start, $end);
        }

        $this->reason = "The `:attribute` `$filter` filter has an invalid entry `$entry` - must be a number or a range like `1000-2000`.";
        return null;
    }

    private function isValidPort(int $port): bool
    {
        return $port >= self::MIN_PORT && $port <= self::MAX_PORT;
    }

    private function overlapsExisting(array $newPorts, array $existing): bool
    {
        foreach ($newPorts as $port) {
            if (isset($existing[$port])) {
                return true;
            }
        }

        return false;
    }

    public function message()
    {
        return $this->reason;
    }
}