<?php

declare(strict_types=1);

namespace SiteFort\Includes;

if ( ! defined( 'ABSPATH' ) && ! defined( 'SITEFORT_STANDALONE_BOOTSTRAP' ) ) {
    $sitefort_provenance_script = filter_input( INPUT_SERVER, 'SCRIPT_FILENAME', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
    $sitefort_provenance_script = is_string( $sitefort_provenance_script ) ? $sitefort_provenance_script : '';
    if ( $sitefort_provenance_script !== '' && realpath( $sitefort_provenance_script ) === __FILE__ ) {
        http_response_code( 404 );
        exit;
    }
}

final class RequestProvenance {

    private const AUTHORITATIVE_ORIGINAL_PEER_SERVER_KEYS = [
        'SITEFORT_ORIGINAL_REMOTE_ADDR',
        'REDIRECT_SITEFORT_ORIGINAL_REMOTE_ADDR',
    ];

    private const AUTHORITATIVE_PROXY_CHAIN_SERVER_KEYS = [
        'SITEFORT_ORIGINAL_PROXY_CHAIN',
        'REDIRECT_SITEFORT_ORIGINAL_PROXY_CHAIN',
    ];

    /**
     * @var array<string, array<string,mixed>>
     */
    private static array $cached_current_request = [];

    /**
     * @param array<string,mixed>|null $server_override
     * @param array<string,mixed>      $context
     * @return array<string,mixed>
     */
    public static function resolve_current_request( ?array $server_override = null, array $context = [] ): array {
        $cache_key = self::context_cache_key( $context );
        if ( $server_override === null && isset( self::$cached_current_request[ $cache_key ] ) ) {
            return self::$cached_current_request[ $cache_key ];
        }

        $resolved = self::resolve_from_server( $server_override ?? $_SERVER, $context );

        if ( $server_override === null ) {
            self::$cached_current_request[ $cache_key ] = $resolved;
        }

        return $resolved;
    }

    /**
     * @param array<string,mixed> $server
     * @param array<string,mixed> $context
     * @return array<string,mixed>
     */
    public static function resolve_from_server( array $server, array $context = [] ): array {
        $cloudflare_ranges = self::normalize_ranges( $context['cloudflare_ranges'] ?? [] );
        $remote_addr       = self::normalize_ip( self::server_value( $server, 'REMOTE_ADDR' ) );
        $cf_connecting_ip  = self::normalize_ip( self::server_value( $server, 'HTTP_CF_CONNECTING_IP' ) );
        $cf_connecting_ipv6 = self::normalize_ip( self::server_value( $server, 'HTTP_CF_CONNECTING_IPV6' ) );
        $cf_ipcountry_raw  = strtoupper( (string) preg_replace( '/[^A-Za-z]/', '', self::server_value( $server, 'HTTP_CF_IPCOUNTRY' ) ) );
        $cf_ipcountry      = strlen( $cf_ipcountry_raw ) === 2 ? $cf_ipcountry_raw : '';

        $cloudflare_country_header_present = $cf_ipcountry !== '';
        $cloudflare_country_valid          = $cf_ipcountry !== '' && ! in_array( $cf_ipcountry, [ 'XX', 'T1' ], true );
        $cloudflare_header_present         = $cf_connecting_ip !== '' || $cf_connecting_ipv6 !== '' || $cloudflare_country_header_present;

        $original_peer_ip       = self::resolve_original_peer( $server );
        $proxy_chain_verified = self::proxy_chain_contains_cloudflare( $server, $cloudflare_ranges );
        $remote_addr_is_cloudflare = self::ip_in_range_list( $remote_addr, $cloudflare_ranges );
        $original_peer_is_cloudflare = self::ip_in_range_list( $original_peer_ip, $cloudflare_ranges );

        $cloudflare_peer_verified = false;

        if ( $remote_addr_is_cloudflare ) {
            $cloudflare_peer_verified = true;
        } elseif ( $original_peer_is_cloudflare ) {
            $cloudflare_peer_verified = true;
        } elseif ( $proxy_chain_verified ) {
            $cloudflare_peer_verified = true;
        }

        $server_restored_remote_addr = $original_peer_ip !== '' && $remote_addr !== '' && $remote_addr !== $original_peer_ip;
        $cloudflare_detected         = $cloudflare_header_present || $remote_addr_is_cloudflare || $original_peer_is_cloudflare || $proxy_chain_verified;

        $failure_reason = '';
        if ( $remote_addr === '' ) {
            $failure_reason = 'missing_remote_addr';
        } elseif ( $cloudflare_peer_verified ) {
            if ( ! $cloudflare_country_header_present ) {
                $failure_reason = 'cloudflare_country_missing';
            } elseif ( ! $cloudflare_country_valid ) {
                $failure_reason = 'cloudflare_country_invalid';
            }
        } elseif ( $cloudflare_detected ) {
            $failure_reason = $server_restored_remote_addr
                ? 'cloudflare_origin_provenance_unavailable'
                : 'cloudflare_peer_unverified';
        } else {
            $failure_reason = 'cloudflare_not_detected';
        }

        return [
            'remote_addr'                      => $remote_addr,
            'cf_ipcountry'                     => $cf_ipcountry,
            'cloudflare_detected'              => $cloudflare_detected,
            'cloudflare_header_present'        => $cloudflare_header_present,
            'cloudflare_country_valid'         => $cloudflare_country_valid,
            'cloudflare_peer_verified'         => $cloudflare_peer_verified,
            'server_restored_remote_addr'      => $server_restored_remote_addr,
            'failure_reason'                   => $failure_reason,
        ];
    }

    public static function reset_cache(): void {
        self::$cached_current_request = [];
    }

    /**
     * @param array<string,mixed> $server
     * @return string
     */
    private static function resolve_original_peer( array $server ): string {
        foreach ( self::AUTHORITATIVE_ORIGINAL_PEER_SERVER_KEYS as $key ) {
            $candidate = self::normalize_ip( self::server_value( $server, $key ) );
            if ( $candidate !== '' ) {
                return $candidate;
            }
        }

        return '';
    }

    /**
     * @param array<string,mixed> $server
     * @param string[]            $cloudflare_ranges
     */
    private static function proxy_chain_contains_cloudflare( array $server, array $cloudflare_ranges ): bool {
        foreach ( self::AUTHORITATIVE_PROXY_CHAIN_SERVER_KEYS as $key ) {
            if ( self::chain_contains_cloudflare( self::server_value( $server, $key ), $cloudflare_ranges ) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param string[] $cloudflare_ranges
     */
    private static function chain_contains_cloudflare( string $raw_chain, array $cloudflare_ranges ): bool {
        if ( $raw_chain === '' || empty( $cloudflare_ranges ) ) {
            return false;
        }

        foreach ( preg_split( '/[\s,]+/', $raw_chain, -1, PREG_SPLIT_NO_EMPTY ) ?: [] as $token ) {
            $candidate = self::normalize_ip( (string) $token );
            if ( $candidate !== '' && self::ip_in_range_list( $candidate, $cloudflare_ranges ) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param mixed $value
     * @return string[]
     */
    private static function normalize_ranges( $value ): array {
        if ( ! is_array( $value ) ) {
            return [];
        }

        $ranges = [];
        foreach ( $value as $entry ) {
            $entry = trim( (string) $entry );
            if ( $entry !== '' ) {
                $ranges[] = $entry;
            }
        }

        return $ranges;
    }

    /**
     * @param array<string,mixed> $context
     */
    private static function context_cache_key( array $context ): string {
        $encoded = json_encode( $context );

        return is_string( $encoded ) ? md5( $encoded ) : md5( serialize( $context ) );
    }

    /**
     * @param array<string,mixed> $server
     */
    private static function server_value( array $server, string $key ): string {
        $value = $server[ $key ] ?? '';

        return is_scalar( $value ) ? str_replace( "\0", '', trim( (string) $value ) ) : '';
    }

    private static function normalize_ip( string $value ): string {
        $value = trim( $value );
        if ( $value === '' ) {
            return '';
        }

        if ( preg_match( '/^\[([A-Fa-f0-9:.]+)\](?::\d+)?$/', $value, $matches ) === 1 ) {
            $value = $matches[1];
        } elseif ( substr_count( $value, ':' ) === 1 && strpos( $value, '.' ) !== false ) {
            [ $candidate, $port ] = explode( ':', $value, 2 );
            if ( ctype_digit( $port ) ) {
                $value = $candidate;
            }
        }

        if ( filter_var( $value, FILTER_VALIDATE_IP ) === false ) {
            return '';
        }

        $binary = @inet_pton( $value );
        if ( $binary !== false && strlen( $binary ) === 16 && substr( $binary, 0, 12 ) === str_repeat( "\x00", 10 ) . "\xff\xff" ) {
            $mapped = @inet_ntop( substr( $binary, 12 ) );
            if ( is_string( $mapped ) && $mapped !== '' ) {
                return $mapped;
            }
        }

        return $value;
    }

    /**
     * @param string[] $ranges
     */
    private static function ip_in_range_list( string $ip, array $ranges ): bool {
        if ( $ip === '' || empty( $ranges ) ) {
            return false;
        }

        foreach ( $ranges as $range ) {
            $range = trim( (string) $range );
            if ( $range === '' ) {
                continue;
            }
            if ( self::ip_in_range( $ip, $range ) ) {
                return true;
            }
        }

        return false;
    }

    private static function ip_in_range( string $ip, string $range ): bool {
        if ( strpos( $range, '/' ) === false ) {
            $left  = @inet_pton( $ip );
            $right = @inet_pton( $range );

            return $left !== false && $right !== false && $left === $right;
        }

        [ $subnet, $bits_str ] = explode( '/', $range, 2 );
        $bits                  = (int) $bits_str;
        $ip_bin                = @inet_pton( $ip );
        $subnet_bin            = @inet_pton( $subnet );

        if ( $ip_bin === false || $subnet_bin === false || strlen( $ip_bin ) !== strlen( $subnet_bin ) ) {
            return false;
        }

        $max_bits = strlen( $ip_bin ) * 8;
        if ( $bits < 0 || $bits > $max_bits ) {
            return false;
        }

        $full_bytes     = (int) floor( $bits / 8 );
        $remaining_bits = $bits % 8;
        $mask           = str_repeat( "\xff", $full_bytes );

        if ( $remaining_bits > 0 ) {
            $mask .= chr( ( 0xff << ( 8 - $remaining_bits ) ) & 0xff );
        }

        $mask = str_pad( $mask, strlen( $ip_bin ), "\x00" );

        return ( $ip_bin & $mask ) === ( $subnet_bin & $mask );
    }
}
