<?php

namespace Formatrica\Submission\Security;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Formatrica\Support\Client_Ip_Matcher;

/**
 * Handles IP address detection and anonymization.
 *
 * This is the single client-IP resolver for the whole plugin (submissions,
 * drafts, rate limits, headless allowlist). It trusts ONLY the direct peer
 * (REMOTE_ADDR) by default; forwarding headers (X-Forwarded-For) are
 * attacker-controlled on a directly reachable PHP server, so they are honoured
 * only when the direct peer is an administrator-declared trusted proxy.
 */
final class IP_Handler
{
    /**
     * Detect the client IP address.
     *
     * By default returns REMOTE_ADDR. When the site sits behind a reverse proxy,
     * the administrator declares its CIDR(s) via the `formatrica_trusted_proxy_cidrs`
     * filter; only then is X-Forwarded-For walked (right-to-left, skipping known
     * proxies) to find the real client. This prevents rate-bucket rotation and
     * headless IP-allowlist bypass via spoofed headers.
     *
     * @return string
     */
    public function detect(): string
    {
        $remote = isset($_SERVER['REMOTE_ADDR'])
            ? trim(sanitize_text_field(wp_unslash((string) $_SERVER['REMOTE_ADDR'])))
            : '';

        if (!filter_var($remote, FILTER_VALIDATE_IP)) {
            $remote = '';
        }

        /**
         * Trusted reverse-proxy CIDRs. Empty (default) means "no proxy" — only
         * REMOTE_ADDR is ever trusted. Add your load balancer / CDN ranges here
         * to have Formatrica read the real client IP from X-Forwarded-For.
         *
         * @param array<int, string> $cidrs
         */
        $trusted = (array) apply_filters('formatrica_trusted_proxy_cidrs', []);

        if ($remote === '' || $trusted === [] || !Client_Ip_Matcher::matches_any($remote, $trusted)) {
            return $remote;
        }

        $forwarded = isset($_SERVER['HTTP_X_FORWARDED_FOR'])
            ? sanitize_text_field(wp_unslash((string) $_SERVER['HTTP_X_FORWARDED_FOR']))
            : '';
        $chain = array_values(array_filter(array_map('trim', explode(',', $forwarded))));

        // Walk from the closest proxy backwards to the first address that is not
        // itself a trusted proxy — that is the real client.
        for ($i = count($chain) - 1; $i >= 0; $i--) {
            $candidate = $chain[$i];
            if (!filter_var($candidate, FILTER_VALIDATE_IP)) {
                continue;
            }
            if (!Client_Ip_Matcher::matches_any($candidate, $trusted)) {
                return $candidate;
            }
        }

        return $remote;
    }

    /**
     * Format IP for storage based on privacy mode
     *
     * @param string $ip
     * @param string $mode 'full', 'anonymous', or 'none'
     * @return string|null
     */
    public function format_for_storage(string $ip, string $mode): ?string
    {
        if ($ip === '') {
            return null;
        }

        switch ($mode) {
            case 'none':
                return null;

            case 'anonymous':
                return $this->anonymise($ip);

            case 'full':
            default:
                return $ip;
        }
    }

    /**
     * Anonymize an IP address
     *
     * @param string $ip
     * @return string|null
     */
    public function anonymise(string $ip): ?string
    {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $segments = explode('.', $ip);
            $segments[count($segments) - 1] = '0';

            return implode('.', $segments);
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            $packed = @inet_pton($ip);

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

            $prefix = substr($packed, 0, 8);
            $anonymised = $prefix . str_repeat("\0", 8);

            $expanded = @inet_ntop($anonymised);

            return $expanded ?: null;
        }

        return null;
    }
}
