<?php
/**
 * Server-side IP geolocation resolver.
 *
 * @package BlaminhorEssentials
 */

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

/**
 * Resolves a visitor's country from their IP using a local MaxMind GeoLite2
 * database if available, or via WordPress Geo headers exposed by some hosts
 * (Cloudflare, Argo). Falls back to null when no resolution is possible —
 * callers must default to the strictest regional preset in that case.
 *
 * Embedding a real MaxMind .mmdb requires CC-BY-SA attribution and adds
 * ~6 MB. To keep the plugin lightweight and ship-ready, the .mmdb file is
 * NOT bundled — the admin downloads it via a single click in settings, or
 * MaxMind's geoipupdate cron writes it to the path expected here.
 */
class Blaminhor_Essentials_Cookies_Geo_Resolver {

	const DB_BASENAME = 'GeoLite2-Country.mmdb';

	/**
	 * Resolve the visitor country (ISO 3166-1 alpha-2 uppercase).
	 *
	 * @param string|null $ip Override IP for testing/CLI.
	 * @return string|null Country code or null if unknown.
	 */
	public function resolve_country( $ip = null ) {
		if ( null === $ip ) {
			$ip = $this->detect_ip();
		}
		if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
			return null;
		}

		// 1) Cheap: hosts that already expose a country header.
		$header_country = $this->country_from_headers();
		if ( $header_country ) {
			return $header_country;
		}

		// 2) MaxMind local DB if installed.
		$mmdb_country = $this->country_from_mmdb( $ip );
		if ( $mmdb_country ) {
			return $mmdb_country;
		}

		// 3) Filter for sites that want to plug another provider.
		$external = apply_filters( 'blaminhor_cookies_resolve_country', null, $ip );
		if ( is_string( $external ) && 2 === strlen( $external ) ) {
			return strtoupper( $external );
		}

		return null;
	}

	/**
	 * Best-effort client IP detection (read-only, validated).
	 *
	 * @return string
	 */
	public function detect_ip() {
		$candidates = array(
			'HTTP_CF_CONNECTING_IP',
			'HTTP_X_FORWARDED_FOR',
			'HTTP_X_REAL_IP',
			'HTTP_CLIENT_IP',
			'REMOTE_ADDR',
		);
		foreach ( $candidates as $key ) {
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Validated by filter_var.
			if ( ! empty( $_SERVER[ $key ] ) ) {
				$raw = (string) $_SERVER[ $key ];
				$ip  = trim( explode( ',', $raw )[0] );
				if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
					return $ip;
				}
			}
		}
		return '0.0.0.0';
	}

	/**
	 * Inspect well-known reverse-proxy country headers.
	 *
	 * @return string|null
	 */
	protected function country_from_headers() {
		$keys = array(
			'HTTP_CF_IPCOUNTRY',          // Cloudflare
			'HTTP_X_GEO_COUNTRY',         // common
			'HTTP_X_APPENGINE_COUNTRY',   // GCP App Engine
			'GEOIP_COUNTRY_CODE',         // Apache mod_geoip
		);
		foreach ( $keys as $key ) {
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Compared to a strict pattern.
			if ( ! empty( $_SERVER[ $key ] ) ) {
				$value = strtoupper( substr( (string) $_SERVER[ $key ], 0, 2 ) );
				if ( preg_match( '/^[A-Z]{2}$/', $value ) ) {
					return $value;
				}
			}
		}
		return null;
	}

	/**
	 * Read the country via the bundled MaxMind reader if available.
	 *
	 * @param string $ip IP to lookup.
	 * @return string|null
	 */
	protected function country_from_mmdb( $ip ) {
		$path = $this->get_database_path();
		if ( ! is_readable( $path ) ) {
			return null;
		}

		// Native GeoIP2 PHP reader if installed (composer maxmind-db/reader).
		if ( class_exists( '\\MaxMind\\Db\\Reader' ) ) {
			try {
				$reader = new \MaxMind\Db\Reader( $path );
				$record = $reader->get( $ip );
				$reader->close();
				if ( is_array( $record ) && ! empty( $record['country']['iso_code'] ) ) {
					return strtoupper( $record['country']['iso_code'] );
				}
			} catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
				// Silent: fallback through.
			}
		}

		return null;
	}

	/**
	 * Filesystem path of the MMDB file.
	 *
	 * @return string
	 */
	public function get_database_path() {
		$upload = wp_upload_dir();
		$dir    = trailingslashit( $upload['basedir'] ) . 'blaminhor-essentials/geoip/';
		return $dir . self::DB_BASENAME;
	}

	/**
	 * @return bool Whether the local MMDB exists and is readable.
	 */
	public function has_database() {
		return is_readable( $this->get_database_path() );
	}
}
