<?php
/**
 * Whitelist service for managing IP and API key whitelists.
 *
 * @package Carticy\CheckoutShield\Services
 */

declare(strict_types=1);

namespace Carticy\CheckoutShield\Services;

use WP_REST_Request;

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

/**
 * Handles IP and API key whitelist management.
 */
final class WhitelistService {

	/**
	 * Option name for whitelisted IPs.
	 */
	private const OPTION_IPS = 'carticy_checkout_shield_whitelisted_ips';

	/**
	 * Option name for API keys.
	 */
	private const OPTION_API_KEYS = 'carticy_checkout_shield_api_keys';

	/**
	 * Option name for proxy support.
	 */
	private const OPTION_PROXY_SUPPORT = 'carticy_checkout_shield_proxy_support';

	/**
	 * API key header name.
	 */
	private const API_KEY_HEADER = 'X-CCS-Key';

	/**
	 * Check if a request is whitelisted (for REST API requests).
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @return bool
	 */
	public function is_whitelisted( WP_REST_Request $request ): bool {
		// Check API key header.
		$api_key = $request->get_header( self::API_KEY_HEADER );
		if ( $api_key && $this->is_valid_api_key( $api_key ) ) {
			return true;
		}

		// Check IP whitelist.
		return $this->is_whitelisted_ip( $this->get_client_ip() );
	}

	/**
	 * Check if an IP is whitelisted.
	 *
	 * @param string $ip The IP address to check.
	 * @return bool
	 */
	public function is_whitelisted_ip( string $ip ): bool {
		$whitelisted = get_option( self::OPTION_IPS, array() );

		if ( empty( $whitelisted ) || ! is_array( $whitelisted ) ) {
			return false;
		}

		foreach ( $whitelisted as $entry ) {
			if ( $this->ip_matches( $ip, $entry ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if an API key is valid.
	 *
	 * @param string $key The API key to check.
	 * @return bool
	 */
	public function is_valid_api_key( string $key ): bool {
		$keys = get_option( self::OPTION_API_KEYS, array() );

		if ( empty( $keys ) || ! is_array( $keys ) ) {
			return false;
		}

		foreach ( $keys as $stored_key ) {
			if ( isset( $stored_key['hash'] ) && hash_equals( $stored_key['hash'], wp_hash( $key ) ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if an IP matches a whitelist entry (supports CIDR notation).
	 *
	 * @param string $ip    The IP to check.
	 * @param string $entry The whitelist entry (IP or CIDR).
	 * @return bool
	 */
	private function ip_matches( string $ip, string $entry ): bool {
		// Direct match.
		if ( $ip === $entry ) {
			return true;
		}

		// CIDR match.
		if ( strpos( $entry, '/' ) !== false ) {
			return $this->ip_in_cidr( $ip, $entry );
		}

		return false;
	}

	/**
	 * Check if an IP is within a CIDR range.
	 *
	 * @param string $ip   The IP address.
	 * @param string $cidr The CIDR notation (e.g., 192.168.1.0/24).
	 * @return bool
	 */
	private function ip_in_cidr( string $ip, string $cidr ): bool {
		list( $subnet, $mask ) = explode( '/', $cidr, 2 );

		$mask = (int) $mask;

		// IPv4.
		if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
			$ip_long     = ip2long( $ip );
			$subnet_long = ip2long( $subnet );

			if ( false === $ip_long || false === $subnet_long ) {
				return false;
			}

			$mask_long = -1 << ( 32 - $mask );
			return ( $ip_long & $mask_long ) === ( $subnet_long & $mask_long );
		}

		// IPv6 support could be added here if needed.
		return false;
	}

	/**
	 * Get the client IP address with comprehensive proxy support.
	 *
	 * @return string
	 */
	public function get_client_ip(): string {
		$proxy_support = get_option( self::OPTION_PROXY_SUPPORT, 'no' ) === 'yes';

		// Headers to check, in order of trust priority.
		$headers = $proxy_support
			? array(
				'HTTP_CF_CONNECTING_IP',      // Cloudflare.
				'HTTP_CLIENT_IP',             // Some proxies.
				'HTTP_X_FORWARDED_FOR',       // Standard proxy header.
				'HTTP_X_FORWARDED',           // Variation.
				'HTTP_X_CLUSTER_CLIENT_IP',   // Load balancer clusters.
				'HTTP_X_REAL_IP',             // Nginx proxy.
				'HTTP_FORWARDED_FOR',         // Variation.
				'HTTP_FORWARDED',             // RFC 7239.
				'REMOTE_ADDR',                // Direct connection.
			)
			: array( 'REMOTE_ADDR' ); // No proxy trust - direct connection only.

		foreach ( $headers as $header ) {
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- IP addresses validated by filter_var() below.
			$value = isset( $_SERVER[ $header ] ) ? sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) : '';

			// Fallback to getenv() if $_SERVER not populated.
			if ( empty( $value ) ) {
				$value = getenv( $header );
			}

			if ( empty( $value ) ) {
				continue;
			}

			// Handle comma-separated IPs (X-Forwarded-For can have multiple).
			$ips = array_map( 'trim', explode( ',', $value ) );

			foreach ( $ips as $ip ) {
				$ip = $this->normalize_ip( $ip );

				if ( $this->is_valid_public_ip( $ip ) ) {
					return $ip;
				}
			}
		}

		// Absolute fallback.
		return '0.0.0.0';
	}

	/**
	 * Normalize IP address - remove port, sanitize.
	 *
	 * @param string $ip The IP address.
	 * @return string
	 */
	private function normalize_ip( string $ip ): string {
		$ip = trim( $ip );

		// Remove port number if present.
		// Handle IPv6 with port: [2001:db8::1]:8080.
		if ( preg_match( '/^\[(.+)\]:\d+$/', $ip, $matches ) ) {
			$ip = $matches[1];
		} elseif ( preg_match( '/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):\d+$/', $ip, $matches ) ) {
			// Handle IPv4 with port: 1.2.3.4:8080.
			$ip = $matches[1];
		}

		return sanitize_text_field( $ip );
	}

	/**
	 * Validate IP is public (not private/reserved) and properly formatted.
	 *
	 * @param string $ip The IP address.
	 * @return bool
	 */
	private function is_valid_public_ip( string $ip ): bool {
		return filter_var(
			$ip,
			FILTER_VALIDATE_IP,
			FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
		) !== false;
	}

	/**
	 * Add an IP to the whitelist.
	 *
	 * @param string $ip The IP address or CIDR.
	 * @return bool
	 */
	public function add_ip( string $ip ): bool {
		$ip = sanitize_text_field( $ip );

		// Validate IP or CIDR format.
		if ( strpos( $ip, '/' ) !== false ) {
			// CIDR validation.
			list( $subnet, $mask ) = explode( '/', $ip, 2 );
			if ( ! filter_var( $subnet, FILTER_VALIDATE_IP ) || ! is_numeric( $mask ) ) {
				return false;
			}
		} elseif ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
			return false;
		}

		$whitelisted   = get_option( self::OPTION_IPS, array() );
		$whitelisted   = is_array( $whitelisted ) ? $whitelisted : array();
		$whitelisted[] = $ip;
		$whitelisted   = array_unique( $whitelisted );

		return update_option( self::OPTION_IPS, $whitelisted );
	}

	/**
	 * Remove an IP from the whitelist.
	 *
	 * @param string $ip The IP address or CIDR.
	 * @return bool
	 */
	public function remove_ip( string $ip ): bool {
		$whitelisted = get_option( self::OPTION_IPS, array() );
		$whitelisted = is_array( $whitelisted ) ? $whitelisted : array();
		$whitelisted = array_values( array_diff( $whitelisted, array( $ip ) ) );

		return update_option( self::OPTION_IPS, $whitelisted );
	}

	/**
	 * Generate a new API key.
	 *
	 * @param string $name Optional name/description for the key.
	 * @return array{key: string, hash: string, name: string, created: int}
	 */
	public function generate_api_key( string $name = '' ): array {
		$key  = wp_generate_password( 32, false );
		$hash = wp_hash( $key );

		$key_data = array(
			'hash'    => $hash,
			'name'    => sanitize_text_field( $name ),
			'created' => time(),
		);

		// Store the key.
		$keys   = get_option( self::OPTION_API_KEYS, array() );
		$keys   = is_array( $keys ) ? $keys : array();
		$keys[] = $key_data;
		update_option( self::OPTION_API_KEYS, $keys );

		// Return key with the unhashed version (show once to user).
		return array_merge( $key_data, array( 'key' => $key ) );
	}

	/**
	 * Remove an API key by hash.
	 *
	 * @param string $hash The key hash.
	 * @return bool
	 */
	public function remove_api_key( string $hash ): bool {
		$keys = get_option( self::OPTION_API_KEYS, array() );
		$keys = is_array( $keys ) ? $keys : array();

		$keys = array_filter(
			$keys,
			static fn( array $key ): bool => ( $key['hash'] ?? '' ) !== $hash
		);

		return update_option( self::OPTION_API_KEYS, array_values( $keys ) );
	}

	/**
	 * Get all whitelisted IPs.
	 *
	 * @return array
	 */
	public function get_whitelisted_ips(): array {
		$ips = get_option( self::OPTION_IPS, array() );
		return is_array( $ips ) ? $ips : array();
	}

	/**
	 * Get all API keys (without the actual key, just metadata).
	 *
	 * @return array
	 */
	public function get_api_keys(): array {
		$keys = get_option( self::OPTION_API_KEYS, array() );
		return is_array( $keys ) ? $keys : array();
	}
}
