<?php

declare(strict_types=1);

namespace App\Framework\Http\ValueObjects;

use App\Framework\Http\IpAddress;
use InvalidArgumentException;

/**
 * Value Object für CIDR-Notation (z.B. 192.168.1.0/24)
 */
final readonly class CidrRange
{
    public function __construct(
        public IpAddress $network,
        public int $prefixLength
    ) {
        if ($this->network->isV4() && ($prefixLength < 0 || $prefixLength > 32)) {
            throw new InvalidArgumentException("IPv4 prefix length must be between 0 and 32, got: {$prefixLength}");
        }

        if ($this->network->isV6() && ($prefixLength < 0 || $prefixLength > 128)) {
            throw new InvalidArgumentException("IPv6 prefix length must be between 0 and 128, got: {$prefixLength}");
        }
    }

    /**
     * Create from CIDR notation string (e.g. "192.168.1.0/24")
     */
    public static function fromString(string $cidr): self
    {
        if (! str_contains($cidr, '/')) {
            throw new InvalidArgumentException("Invalid CIDR notation: {$cidr}");
        }

        [$network, $prefix] = explode('/', $cidr, 2);

        if (! is_numeric($prefix)) {
            throw new InvalidArgumentException("Invalid prefix length: {$prefix}");
        }

        return new self(
            IpAddress::from(trim($network)),
            (int) $prefix
        );
    }

    /**
     * Check if an IP address is within this CIDR range
     */
    public function contains(IpAddress $ip): bool
    {
        // Both addresses must be same version
        if ($this->network->isV4() !== $ip->isV4()) {
            return false;
        }

        if ($this->network->isV4()) {
            return $this->containsV4($ip);
        }

        return $this->containsV6($ip);
    }

    /**
     * Check if IPv4 address is in range
     */
    private function containsV4(IpAddress $ip): bool
    {
        $networkLong = ip2long($this->network->value);
        $ipLong = ip2long($ip->value);

        if ($networkLong === false || $ipLong === false) {
            return false;
        }

        $mask = -1 << (32 - $this->prefixLength);

        return ($networkLong & $mask) === ($ipLong & $mask);
    }

    /**
     * Check if IPv6 address is in range
     */
    private function containsV6(IpAddress $ip): bool
    {
        $networkBin = inet_pton($this->network->value);
        $ipBin = inet_pton($ip->value);

        if ($networkBin === false || $ipBin === false) {
            return false;
        }

        $bytesToCheck = intval($this->prefixLength / 8);
        $bitsToCheck = $this->prefixLength % 8;

        // Check full bytes
        if ($bytesToCheck > 0 && substr($networkBin, 0, $bytesToCheck) !== substr($ipBin, 0, $bytesToCheck)) {
            return false;
        }

        // Check remaining bits
        if ($bitsToCheck > 0) {
            $networkByte = ord($networkBin[$bytesToCheck] ?? "\x00");
            $ipByte = ord($ipBin[$bytesToCheck] ?? "\x00");

            $mask = (0xFF << (8 - $bitsToCheck)) & 0xFF;

            if (($networkByte & $mask) !== ($ipByte & $mask)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Get network mask
     */
    public function getMask(): string
    {
        if ($this->network->isV4()) {
            $mask = -1 << (32 - $this->prefixLength);

            return long2ip($mask);
        }

        // IPv6 mask calculation is more complex
        $bytes = [];
        $remainingBits = $this->prefixLength;

        for ($i = 0; $i < 16; $i++) {
            if ($remainingBits >= 8) {
                $bytes[] = 0xFF;
                $remainingBits -= 8;
            } elseif ($remainingBits > 0) {
                $bytes[] = (0xFF << (8 - $remainingBits)) & 0xFF;
                $remainingBits = 0;
            } else {
                $bytes[] = 0x00;
            }
        }

        return inet_ntop(pack('C*', ...$bytes)) ?: '::';
    }

    /**
     * Check if this is a local/private range
     */
    public function isLocalRange(): bool
    {
        // Common local ranges
        $localRanges = [
            '127.0.0.0/8',    // Loopback
            '10.0.0.0/8',     // Private Class A
            '172.16.0.0/12',  // Private Class B
            '192.168.0.0/16', // Private Class C
            '169.254.0.0/16', // Link-local
            '::1/128',        // IPv6 Loopback
            'fe80::/10',      // IPv6 Link-local
            'fc00::/7',       // IPv6 Unique local
        ];

        $thisRange = $this->toString();

        foreach ($localRanges as $localRange) {
            $range = self::fromString($localRange);

            // Check if this range is contained within a local range
            if ($range->contains($this->network)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Convert to string representation
     */
    public function toString(): string
    {
        return $this->network->value . '/' . $this->prefixLength;
    }

    public function __toString(): string
    {
        return $this->toString();
    }

    /**
     * Check equality
     */
    public function equals(CidrRange $other): bool
    {
        return $this->network->equals($other->network)
            && $this->prefixLength === $other->prefixLength;
    }

    /**
     * Common local CIDR ranges
     */
    public static function localhost(): self
    {
        return self::fromString('127.0.0.0/8');
    }

    public static function privateClassA(): self
    {
        return self::fromString('10.0.0.0/8');
    }

    public static function privateClassB(): self
    {
        return self::fromString('172.16.0.0/12');
    }

    public static function privateClassC(): self
    {
        return self::fromString('192.168.0.0/16');
    }
}
