<?php
declare(strict_types=1);

namespace SafeTun\Backend\Net;

use SafeTun\Backend\Exception\PublicApiException;

final class ClientIpResolver
{
    /**
     * @param array<string,mixed> $authorization
     * @param array<string,string> $headers lowercase names
     */
    public function resolve(array $authorization, string $remoteAddress, array $headers): string
    {
        $remote = $this->normalizeAddress($remoteAddress);
        $mode = $authorization['client_ip_mode'];

        if ($mode === 'direct') {
            $client = $remote;
        } elseif ($mode === 'trusted_proxy') {
            if (!CidrMatcher::containsAny($authorization['trusted_proxy_cidrs'], $remote)) {
                throw new PublicApiException('SERVICE_UNAVAILABLE', 503, 'REMOTE_ADDR is not a configured trusted proxy.');
            }

            $candidates = [];
            foreach ($authorization['trusted_ip_headers'] as $configuredHeader) {
                $name = strtolower($configuredHeader);
                if (!array_key_exists($name, $headers)) {
                    continue;
                }
                $candidates[] = $name === 'x-forwarded-for'
                    ? $this->fromXForwardedFor($headers[$name], $remote, $authorization['trusted_proxy_cidrs'])
                    : $this->fromSingleHeader($headers[$name], $authorization['trusted_proxy_cidrs']);
            }
            if ($candidates === [] || count(array_unique($candidates)) !== 1) {
                throw new PublicApiException('SERVICE_UNAVAILABLE', 503, 'Trusted proxy headers are missing or disagree.');
            }
            $client = $candidates[0];
        } else {
            throw new PublicApiException('SERVICE_UNAVAILABLE', 503, 'Unknown client IP mode.');
        }

        if ($authorization['require_public_ip'] && !IpNetwork::isPublicAddress($client)) {
            throw new PublicApiException('ACCESS_NOT_ALLOWED', 403, 'Observed client address is not public.');
        }
        return $client;
    }

    /** @param list<string> $trustedCidrs */
    private function fromSingleHeader(string $value, array $trustedCidrs): string
    {
        if (str_contains($value, ',') || trim($value) !== $value) {
            throw new PublicApiException('SERVICE_UNAVAILABLE', 503, 'Malformed single-IP trusted header.');
        }
        $candidate = $this->normalizeAddress($value);
        if (CidrMatcher::containsAny($trustedCidrs, $candidate)) {
            throw new PublicApiException('SERVICE_UNAVAILABLE', 503, 'Ambiguous trusted header chain.');
        }
        return $candidate;
    }

    /** @param list<string> $trustedCidrs */
    private function fromXForwardedFor(string $value, string $remote, array $trustedCidrs): string
    {
        $rawHops = explode(',', $value);
        if (count($rawHops) > 32) {
            throw new PublicApiException('SERVICE_UNAVAILABLE', 503, 'Malformed X-Forwarded-For chain.');
        }
        $hops = [];
        foreach ($rawHops as $rawHop) {
            $trimmed = trim($rawHop);
            if ($trimmed === '') {
                throw new PublicApiException('SERVICE_UNAVAILABLE', 503, 'Empty X-Forwarded-For hop.');
            }
            $hops[] = $this->normalizeAddress($trimmed);
        }

        $current = $remote;
        for ($index = count($hops) - 1; $index >= 0; $index--) {
            if (!CidrMatcher::containsAny($trustedCidrs, $current)) {
                return $current;
            }
            $current = $hops[$index];
        }
        if (CidrMatcher::containsAny($trustedCidrs, $current)) {
            throw new PublicApiException('SERVICE_UNAVAILABLE', 503, 'X-Forwarded-For contains only trusted proxies.');
        }
        return $current;
    }

    private function normalizeAddress(string $address): string
    {
        $packed = @inet_pton($address);
        if (!is_string($packed)) {
            throw new PublicApiException('SERVICE_UNAVAILABLE', 503, 'Invalid network address.');
        }
        $normalized = inet_ntop($packed);
        if (!is_string($normalized)) {
            throw new PublicApiException('SERVICE_UNAVAILABLE', 503, 'Unable to normalize network address.');
        }
        return strtolower($normalized);
    }
}
