<?php
/**
 * Best-effort geolocation hint for the country selector.
 *
 * @since   1.0.0
 * @package DonationRouter
 */

declare( strict_types=1 );
namespace DonationRouter;

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

/**
 * Resolves a likely donor country code without making a binding decision.
 *
 * Sources are tried in priority order:
 *   1. WordPress geo plugin filter `geoip_country_code`.
 *   2. Cloudflare `CF-IPCountry` request header.
 *   3. MaxMind GeoIP lookup (if `geoip_detect2_get_info_from_current_ip` exists).
 *
 * The result is a *suggestion* only. The donor must explicitly confirm via
 * the country selector modal before any routing decision is made.
 *
 * @since 1.0.0
 */
class Geolocation_Hint {

	/**
	 * Object cache key for geolocation hint (scoped per IP hash).
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private const CACHE_KEY_PREFIX = 'geo_hint_';

	/**
	 * TTL for cached hints — one hour.
	 *
	 * @since 1.0.0
	 *
	 * @var int
	 */
	private const CACHE_TTL = 3_600;

	/**
	 * Resolve the donor's likely country code.
	 *
	 * Returns null when no hint is available rather than guessing.
	 *
	 * @since 1.0.0
	 *
	 * @return string|null  Two-letter ISO 3166-1 alpha-2 code, or null.
	 */
	public function get_country_hint(): ?string {
		$cache_key = self::CACHE_KEY_PREFIX . md5( $this->get_client_ip() ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_md5

		$cached = wp_cache_get( $cache_key, Cache_Manager::GROUP );
		if ( is_string( $cached ) ) {
			return '' === $cached ? null : $cached;
		}

		$code = $this->resolve_hint();

		wp_cache_set( $cache_key, $code ?? '', Cache_Manager::GROUP, self::CACHE_TTL );

		return $code;
	}

	/**
	 * Attempt to resolve a country code from the available sources.
	 *
	 * @since 1.0.0
	 *
	 * @return string|null
	 */
	private function resolve_hint(): ?string {
		// Source 1: WordPress geo plugin filter (e.g. WooCommerce GeoIP, GeoIP Detect).
		$from_filter = apply_filters( 'geoip_country_code', '' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- external hook from geo-IP plugins
		if ( is_string( $from_filter ) && $this->is_valid_code( $from_filter ) ) {
			return strtoupper( $from_filter );
		}

		// Source 2: Cloudflare CF-IPCountry header.
		$cf_country = isset( $_SERVER['HTTP_CF_IPCOUNTRY'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			? strtoupper( sanitize_key( (string) wp_unslash( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			: '';
		if ( $this->is_valid_code( $cf_country ) ) {
			return $cf_country;
		}

		// Source 3: MaxMind GeoIP Detect plugin.
		if ( function_exists( 'geoip_detect2_get_info_from_current_ip' ) ) {
			$record = geoip_detect2_get_info_from_current_ip();
			$iso    = $record->country->isoCode ?? '';
			if ( is_string( $iso ) && $this->is_valid_code( $iso ) ) {
				return strtoupper( $iso );
			}
		}

		return null;
	}

	/**
	 * Validate that a string looks like a two-letter ISO 3166-1 alpha-2 code.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $code  Candidate country code.
	 *
	 * @return bool
	 */
	private function is_valid_code( string $code ): bool {
		return 1 === preg_match( '/^[A-Z]{2}$/', strtoupper( $code ) );
	}

	/**
	 * Return the client's IP address for cache keying.
	 *
	 * Prefers REMOTE_ADDR; not used for routing, only for cache scoping.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	private function get_client_ip(): string {
		if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
			return (string) wp_unslash( $_SERVER['REMOTE_ADDR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		}

		return 'unknown';
	}
}
