<?php
/**
 * Security checks for OTP send / verify (rate limits, blocks, captcha).
 *
 * @package Mersal_Notif
 */

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

class Mersal_Notif_Security {

	const RL_IP_HOUR   = 'mnotif_rl_ip_h_';
	const RL_PHONE_DAY = 'mnotif_rl_ph_d_';
	const RL_COOLDOWN  = 'mnotif_rl_cd_';

	/**
	 * Run all enabled checks before sending a code.
	 *
	 * @param string $channel    email|whatsapp|sms.
	 * @param string $identifier Normalised identifier.
	 * @return true|WP_Error
	 */
	public static function allow_send( $channel, $identifier ) {
		$ip = self::client_ip();
		if ( self::is_ip_blocked( $ip ) ) {
			return new WP_Error( 'mnotif_blocked', __( 'Access from your network is not allowed.', 'mersal-notifications' ) );
		}

		if ( in_array( $channel, array( 'whatsapp', 'sms' ), true ) ) {
			$valid = self::validate_phone( $identifier );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
			$cc = self::phone_country_iso( $identifier );
			if ( $cc && self::is_country_blocked( $cc ) ) {
				return new WP_Error( 'mnotif_blocked', __( 'Codes cannot be sent to this country.', 'mersal-notifications' ) );
			}
			if ( Mersal_Notif_Settings::get( 'block_disposable' ) && self::is_disposable_phone( $identifier ) ) {
				return new WP_Error( 'mnotif_disposable', __( 'Temporary / disposable numbers are not allowed.', 'mersal-notifications' ) );
			}
			if ( Mersal_Notif_Settings::get( 'hlr_lookup' ) && ! self::hlr_reachable( $identifier ) ) {
				return new WP_Error( 'mnotif_hlr', __( 'This number appears to be unreachable.', 'mersal-notifications' ) );
			}
		}

		if ( Mersal_Notif_Settings::get( 'block_vpn_proxy' ) && self::looks_like_proxy() ) {
			return new WP_Error( 'mnotif_proxy', __( 'VPN or proxy connections are not allowed.', 'mersal-notifications' ) );
		}

		if ( ! self::check_rate_limit( $identifier ) ) {
			return new WP_Error( 'mnotif_rate', __( 'Too many code requests. Please try again later.', 'mersal-notifications' ) );
		}

		return true;
	}

	/**
	 * @return string
	 */
	public static function client_ip() {
		// Forwarding headers are client-controllable and must only be trusted when
		// the site is genuinely behind a fixed proxy/CDN (opt-in). Otherwise an
		// attacker rotates X-Forwarded-For to get a fresh rate-limit bucket per
		// request and to slip past the IP blocklist.
		$keys = Mersal_Notif_Settings::get( 'trust_proxy' )
			? array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR' )
			: array( 'REMOTE_ADDR' );
		foreach ( $keys as $key ) {
			if ( empty( $_SERVER[ $key ] ) ) {
				continue;
			}
			$raw = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
			if ( 'HTTP_X_FORWARDED_FOR' === $key && false !== strpos( $raw, ',' ) ) {
				$raw = trim( explode( ',', $raw )[0] );
			}
			if ( filter_var( $raw, FILTER_VALIDATE_IP ) ) {
				return $raw;
			}
		}
		return 'unknown';
	}

	/**
	 * @param string $ip IP address.
	 * @return bool
	 */
	public static function is_ip_blocked( $ip ) {
		if ( ! Mersal_Notif_Settings::get( 'block_ips_enabled', 1 ) ) {
			return false;
		}
		$list = self::parse_lines( Mersal_Notif_Settings::get( 'blocked_ips', '' ) );
		foreach ( $list as $line ) {
			if ( $line === $ip ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param string $iso Two-letter country code.
	 * @return bool
	 */
	public static function is_country_blocked( $iso ) {
		if ( ! Mersal_Notif_Settings::get( 'block_countries_enabled', 1 ) ) {
			return false;
		}
		$iso = strtoupper( sanitize_key( $iso ) );
		if ( strlen( $iso ) !== 2 ) {
			return false;
		}
		$list = array_map( 'strtoupper', self::parse_countries( Mersal_Notif_Settings::get( 'blocked_countries', '' ) ) );
		$mode = Mersal_Notif_Settings::get( 'countries_mode', 'allow' );
		if ( 'allow' === $mode ) {
			// The list is an allow-list: block everything that is NOT on it.
			if ( empty( $list ) ) {
				return false;
			}
			return ! in_array( $iso, $list, true );
		}
		return in_array( $iso, $list, true );
	}

	/**
	 * ISO2 country → international dialing prefix (digits, no +).
	 *
	 * @param string $iso Two-letter ISO code.
	 * @return string
	 */
	public static function iso_to_dial( $iso ) {
		$iso = strtoupper( sanitize_key( $iso ) );
		if ( strlen( $iso ) !== 2 ) {
			return '';
		}
		foreach ( self::dialing_prefix_map() as $prefix => $code ) {
			if ( $code === $iso ) {
				return $prefix;
			}
		}
		// Common fallbacks not present in the (security-oriented) prefix map.
		$extra = array(
			'EG' => '20', 'SA' => '966', 'AE' => '971', 'KW' => '965', 'QA' => '974',
			'BH' => '973', 'OM' => '968', 'JO' => '962', 'LB' => '961', 'PS' => '970',
			'MA' => '212', 'DZ' => '213', 'TN' => '216', 'TR' => '90', 'US' => '1',
			'GB' => '44', 'DE' => '49', 'FR' => '33', 'IT' => '39', 'ES' => '34',
		);
		return isset( $extra[ $iso ] ) ? $extra[ $iso ] : '';
	}

	/**
	 * Normalise a phone to E.164 digits (no +), applying the default country
	 * for local numbers when configured.
	 *
	 * @param string $raw Raw phone input.
	 * @return string
	 */
	public static function normalize_phone( $raw ) {
		$raw    = trim( (string) $raw );
		$had_plus = ( '' !== $raw && '+' === $raw[0] );
		$digits = preg_replace( '/\D/', '', $raw );
		if ( '' === $digits ) {
			return '';
		}
		// 00-prefixed international form → drop the 00.
		if ( 0 === strpos( $digits, '00' ) ) {
			$digits = substr( $digits, 2 );
			return $digits;
		}
		if ( $had_plus ) {
			return $digits;
		}
		$default = strtoupper( (string) Mersal_Notif_Settings::get( 'default_country', '' ) );
		$dial    = $default ? self::iso_to_dial( $default ) : '';
		if ( $dial ) {
			// Local trunk form (leading 0) → swap for the country dial code.
			if ( 0 === strpos( $digits, '0' ) ) {
				return $dial . ltrim( $digits, '0' );
			}
			// Already starts with the dial code → keep as-is.
			if ( 0 === strpos( $digits, $dial ) ) {
				return $digits;
			}
			// A bare national number (no country code detected) → prepend dial code.
			if ( '' === self::phone_country_iso( $digits ) ) {
				return $dial . $digits;
			}
		}
		return $digits;
	}

	/**
	 * Validate a normalised phone against the configured rules.
	 *
	 * @param string $phone Digits-only phone.
	 * @return true|WP_Error
	 */
	public static function validate_phone( $phone ) {
		$phone = preg_replace( '/\D/', '', (string) $phone );

		if ( Mersal_Notif_Settings::get( 'validate_format' ) ) {
			$len = strlen( $phone );
			if ( $len < 8 || $len > 15 ) {
				return new WP_Error( 'mnotif_phone_format', __( 'Please enter a valid phone number.', 'mersal-notifications' ) );
			}
		}
		if ( Mersal_Notif_Settings::get( 'require_country_code' ) ) {
			if ( '' === self::phone_country_iso( $phone ) ) {
				return new WP_Error( 'mnotif_phone_cc', __( 'Please include your country code.', 'mersal-notifications' ) );
			}
		}
		if ( ! Mersal_Notif_Settings::get( 'allow_landline', 1 ) && self::is_landline( $phone ) ) {
			return new WP_Error( 'mnotif_landline', __( 'Please use a mobile number.', 'mersal-notifications' ) );
		}
		return true;
	}

	/**
	 * Best-effort landline detection (extendable via filter for accuracy).
	 *
	 * @param string $phone Digits-only E.164 phone.
	 * @return bool
	 */
	public static function is_landline( $phone ) {
		$phone = preg_replace( '/\D/', '', (string) $phone );
		$guess = false;
		// Egypt (20): mobiles are 10/11/12/15 after the country code; anything else
		// (2 = Cairo, 3 = Alex, …) is a landline.
		if ( 0 === strpos( $phone, '20' ) ) {
			$nat   = substr( $phone, 2 );
			$guess = ( '1' !== substr( $nat, 0, 1 ) );
		}
		return (bool) apply_filters( 'mnotif_is_landline', $guess, $phone );
	}

	/**
	 * HLR reachability check. Returns true unless a gateway/filter says otherwise,
	 * so enabling it without an HLR provider never blocks everyone.
	 *
	 * @param string $phone Digits-only phone.
	 * @return bool
	 */
	public static function hlr_reachable( $phone ) {
		$phone = preg_replace( '/\D/', '', (string) $phone );
		return (bool) apply_filters( 'mnotif_hlr_reachable', true, $phone );
	}

	/**
	 * @param string $phone Digits only.
	 * @return string ISO code or empty.
	 */
	public static function phone_country_iso( $phone ) {
		$phone = preg_replace( '/\D/', '', (string) $phone );
		if ( '' === $phone ) {
			return '';
		}
		$map = self::dialing_prefix_map();
		$keys = array_keys( $map );
		usort(
			$keys,
			static function ( $a, $b ) {
				return strlen( $b ) - strlen( $a );
			}
		);
		foreach ( $keys as $prefix ) {
			if ( 0 === strpos( $phone, $prefix ) ) {
				return $map[ $prefix ];
			}
		}
		return '';
	}

	/**
	 * Whether a phone looks disposable / fake.
	 *
	 * Real detection in three safe layers (no false positives on normal numbers):
	 *   1. Obvious junk patterns — all-identical or strictly sequential digits and
	 *      well-known test numbers (the bulk of bot signups).
	 *   2. An owner-defined prefix blocklist (filter `mnotif_disposable_prefixes`).
	 *   3. Carrier line-type lookup (numverify) flagging VoIP — only when an API
	 *      key is configured; results are cached and the check fails open.
	 *
	 * @param string $phone Digits only.
	 * @return bool
	 */
	public static function is_disposable_phone( $phone ) {
		$phone = preg_replace( '/\D/', '', (string) $phone );
		if ( '' === $phone ) {
			return false;
		}

		if ( self::is_junk_phone( $phone ) ) {
			return (bool) apply_filters( 'mnotif_is_disposable_phone', true, $phone );
		}

		foreach ( self::disposable_prefixes() as $p ) {
			$p = preg_replace( '/\D/', '', (string) $p );
			if ( '' !== $p && 0 === strpos( $phone, $p ) ) {
				return (bool) apply_filters( 'mnotif_is_disposable_phone', true, $phone );
			}
		}

		$voip = self::phone_is_voip( $phone );
		if ( null !== $voip ) {
			return (bool) apply_filters( 'mnotif_is_disposable_phone', $voip, $phone );
		}

		return (bool) apply_filters( 'mnotif_is_disposable_phone', false, $phone );
	}

	/**
	 * Obvious fake / placeholder numbers (safe to block, never a real subscriber).
	 *
	 * @param string $phone Digits only.
	 * @return bool
	 */
	private static function is_junk_phone( $phone ) {
		// 7+ identical digits in a row (0000000000, 11111111111…).
		if ( preg_match( '/^(\d)\1{6,}$/', $phone ) ) {
			return true;
		}
		// Strictly ascending or descending runs (1234567890 / 0987654321).
		if ( self::is_sequential( $phone ) ) {
			return true;
		}
		$known = array( '1234567890', '0123456789', '1111111111', '0000000000', '12345678', '00000000', '1234567', '7654321' );
		return in_array( $phone, $known, true );
	}

	/**
	 * @param string $digits Digits only.
	 * @return bool
	 */
	private static function is_sequential( $digits ) {
		$len = strlen( $digits );
		if ( $len < 7 ) {
			return false;
		}
		$asc  = true;
		$desc = true;
		for ( $i = 1; $i < $len; $i++ ) {
			$step = (int) $digits[ $i ] - (int) $digits[ $i - 1 ];
			if ( 1 !== $step ) {
				$asc = false;
			}
			if ( -1 !== $step ) {
				$desc = false;
			}
		}
		return $asc || $desc;
	}

	/**
	 * Owner-supplied disposable / VoIP prefix blocklist (E.164 digits, no +).
	 * Empty by default — the previous hard-coded list wrongly matched real US
	 * geographic area codes (415, 510, 650 …). Extend via the admin setting or
	 * the `mnotif_disposable_prefixes` filter.
	 *
	 * @return array<int,string>
	 */
	private static function disposable_prefixes() {
		$raw  = (string) Mersal_Notif_Settings::get( 'disposable_prefixes', '' );
		$list = array();
		foreach ( preg_split( '/[\s,]+/', $raw ) as $p ) {
			$p = preg_replace( '/\D/', '', (string) $p );
			if ( '' !== $p ) {
				$list[] = $p;
			}
		}
		return array_values( array_unique( (array) apply_filters( 'mnotif_disposable_prefixes', $list ) ) );
	}

	/**
	 * Carrier line-type check via numverify. Returns true/false when the API
	 * answers, or null when no key is set or the lookup fails (fail-open).
	 *
	 * @param string $phone Digits only.
	 * @return bool|null
	 */
	private static function phone_is_voip( $phone ) {
		$key = (string) apply_filters( 'mnotif_numverify_key', (string) Mersal_Notif_Settings::get( 'numverify_key', '' ) );
		if ( '' === $key ) {
			return null;
		}

		$cache = 'mnotif_lt_' . md5( $phone );
		$hit   = get_transient( $cache );
		if ( '1' === $hit ) {
			return true;
		}
		if ( '0' === $hit ) {
			return false;
		}

		$resp = wp_remote_get(
			'https://apilayer.net/api/validate?access_key=' . rawurlencode( $key ) . '&number=' . rawurlencode( $phone ),
			array( 'timeout' => 5 )
		);
		if ( is_wp_error( $resp ) || 200 !== (int) wp_remote_retrieve_response_code( $resp ) ) {
			return null;
		}
		$j = json_decode( wp_remote_retrieve_body( $resp ), true );
		if ( ! is_array( $j ) || empty( $j['valid'] ) ) {
			return null;
		}
		$is_voip = ( isset( $j['line_type'] ) && 'voip' === strtolower( (string) $j['line_type'] ) );
		set_transient( $cache, $is_voip ? '1' : '0', 7 * DAY_IN_SECONDS );
		return $is_voip;
	}

	/**
	 * Whether the request comes through a VPN / proxy / anonymiser.
	 *
	 * Layer 1: forwarding headers proxies commonly add (cheap, no network).
	 * Layer 2: IP-reputation lookup (ip-api.com keyless by default; override via
	 * the `mnotif_ip_reputation` filter for a paid provider). Results are cached
	 * per IP and the lookup fails open so an API outage never blocks real users.
	 *
	 * @return bool
	 */
	public static function looks_like_proxy() {
		foreach ( array( 'HTTP_VIA', 'HTTP_X_PROXY_ID', 'HTTP_PROXY_CONNECTION', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED', 'HTTP_X_BLUECOAT_VIA' ) as $h ) {
			if ( ! empty( $_SERVER[ $h ] ) ) {
				return (bool) apply_filters( 'mnotif_looks_like_proxy', true, self::client_ip() );
			}
		}
		$verdict = self::ip_is_anonymizer( self::client_ip() );
		return (bool) apply_filters( 'mnotif_looks_like_proxy', $verdict, self::client_ip() );
	}

	/**
	 * Cached IP-reputation verdict (true = proxy/VPN/hosting).
	 *
	 * @param string $ip Client IP.
	 * @return bool
	 */
	private static function ip_is_anonymizer( $ip ) {
		if ( ! $ip || 'unknown' === $ip ) {
			return false;
		}
		// Never flag private / reserved / local addresses.
		if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
			return false;
		}

		$cache  = 'mnotif_ipchk_' . md5( $ip );
		$cached = get_transient( $cache );
		if ( '1' === $cached ) {
			return true;
		}
		if ( '0' === $cached ) {
			return false;
		}

		$verdict = self::query_ip_reputation( $ip );
		if ( null === $verdict ) {
			// Lookup failed → fail open, but cache briefly to avoid hammering.
			set_transient( $cache, '0', 10 * MINUTE_IN_SECONDS );
			return false;
		}
		set_transient( $cache, $verdict ? '1' : '0', 12 * HOUR_IN_SECONDS );
		return $verdict;
	}

	/**
	 * Query an IP-reputation provider. Returns true/false, or null on failure.
	 *
	 * @param string $ip Client IP.
	 * @return bool|null
	 */
	private static function query_ip_reputation( $ip ) {
		// Let integrators plug a paid provider (proxycheck.io, IPQS, …).
		$custom = apply_filters( 'mnotif_ip_reputation', null, $ip );
		if ( is_bool( $custom ) ) {
			return $custom;
		}

		// Default: ip-api.com (keyless). `proxy` = anonymising proxy/VPN/Tor,
		// `hosting` = datacenter range (where most commercial VPNs live).
		$resp = wp_remote_get(
			'http://ip-api.com/json/' . rawurlencode( $ip ) . '?fields=status,proxy,hosting',
			array( 'timeout' => 4 )
		);
		if ( is_wp_error( $resp ) || 200 !== (int) wp_remote_retrieve_response_code( $resp ) ) {
			return null;
		}
		$j = json_decode( wp_remote_retrieve_body( $resp ), true );
		if ( ! is_array( $j ) || ( isset( $j['status'] ) && 'success' !== $j['status'] ) ) {
			return null;
		}
		$block_hosting = (bool) apply_filters( 'mnotif_block_hosting_ips', true );
		return ( ! empty( $j['proxy'] ) || ( $block_hosting && ! empty( $j['hosting'] ) ) );
	}

	/**
	 * @param string $identifier Phone or email.
	 * @return bool True when allowed.
	 */
	public static function check_rate_limit( $identifier ) {
		if ( ! Mersal_Notif_Settings::get( 'rate_limit_enabled', 1 ) ) {
			return true;
		}

		$ip = self::client_ip();
		$cd = get_transient( self::RL_COOLDOWN . md5( $ip ) );
		if ( false !== $cd ) {
			return false;
		}

		$max_ip = max( 1, (int) Mersal_Notif_Settings::get( 'rate_limit_ip_hour', 10 ) );
		$ip_key = self::RL_IP_HOUR . md5( $ip );
		$ip_cnt = (int) get_transient( $ip_key );
		if ( $ip_cnt >= $max_ip ) {
			self::set_cooldown( $ip );
			return false;
		}
		set_transient( $ip_key, $ip_cnt + 1, HOUR_IN_SECONDS );

		$id = preg_replace( '/\s+/', '', strtolower( (string) $identifier ) );
		if ( '' !== $id ) {
			$max_ph = max( 1, (int) Mersal_Notif_Settings::get( 'rate_limit_phone_day', 20 ) );
			$ph_key = self::RL_PHONE_DAY . md5( $id );
			$ph_cnt = (int) get_transient( $ph_key );
			if ( $ph_cnt >= $max_ph ) {
				self::set_cooldown( $ip );
				return false;
			}
			set_transient( $ph_key, $ph_cnt + 1, DAY_IN_SECONDS );
		}

		return true;
	}

	/**
	 * @param string $ip Client IP.
	 */
	private static function set_cooldown( $ip ) {
		$hours = max( 1, (int) Mersal_Notif_Settings::get( 'rate_limit_cooldown', 1 ) );
		set_transient( self::RL_COOLDOWN . md5( $ip ), 1, $hours * HOUR_IN_SECONDS );
	}

	/**
	 * @param string $token Client token.
	 * @return bool
	 */
	public static function verify_captcha( $token ) {
		if ( ! Mersal_Notif_Settings::get( 'recaptcha_enabled' ) ) {
			return true;
		}
		$secret = Mersal_Notif_Settings::get( 'recaptcha_secret' );
		if ( ! $secret ) {
			return true;
		}
		if ( ! $token ) {
			return false;
		}

		$provider = Mersal_Notif_Settings::get( 'captcha_provider', 'recaptcha' );
		if ( 'turnstile' === $provider ) {
			return self::verify_turnstile( $token, $secret );
		}
		if ( 'hcaptcha' === $provider ) {
			return self::verify_hcaptcha( $token, $secret );
		}
		return self::verify_recaptcha_v3( $token, $secret );
	}

	/**
	 * @param string $token  Token.
	 * @param string $secret Secret.
	 * @return bool
	 */
	private static function verify_recaptcha_v3( $token, $secret ) {
		$resp = wp_remote_post(
			'https://www.google.com/recaptcha/api/siteverify',
			array(
				'timeout' => 10,
				'body'    => array(
					'secret'   => $secret,
					'response' => $token,
					'remoteip' => self::client_ip(),
				),
			)
		);
		if ( is_wp_error( $resp ) ) {
			// Fail closed: the site owner explicitly enabled a captcha, so a
			// provider/network outage must not silently disable it. (Previously
			// returned true, which let anyone who could block the outbound
			// verification request bypass the captcha entirely.)
			return false;
		}
		$j = json_decode( wp_remote_retrieve_body( $resp ), true );
		return ! empty( $j['success'] ) && ( ! isset( $j['score'] ) || (float) $j['score'] >= 0.3 );
	}

	/**
	 * @param string $token  Token.
	 * @param string $secret Secret.
	 * @return bool
	 */
	private static function verify_turnstile( $token, $secret ) {
		$resp = wp_remote_post(
			'https://challenges.cloudflare.com/turnstile/v0/siteverify',
			array(
				'timeout' => 10,
				'body'    => array(
					'secret'   => $secret,
					'response' => $token,
					'remoteip' => self::client_ip(),
				),
			)
		);
		if ( is_wp_error( $resp ) ) {
			// Fail closed: the site owner explicitly enabled a captcha, so a
			// provider/network outage must not silently disable it. (Previously
			// returned true, which let anyone who could block the outbound
			// verification request bypass the captcha entirely.)
			return false;
		}
		$j = json_decode( wp_remote_retrieve_body( $resp ), true );
		return ! empty( $j['success'] );
	}

	/**
	 * @param string $token  Token.
	 * @param string $secret Secret.
	 * @return bool
	 */
	private static function verify_hcaptcha( $token, $secret ) {
		$resp = wp_remote_post(
			'https://api.hcaptcha.com/siteverify',
			array(
				'timeout' => 10,
				'body'    => array(
					'secret'   => $secret,
					'response' => $token,
					'remoteip' => self::client_ip(),
				),
			)
		);
		if ( is_wp_error( $resp ) ) {
			// Fail closed: the site owner explicitly enabled a captcha, so a
			// provider/network outage must not silently disable it. (Previously
			// returned true, which let anyone who could block the outbound
			// verification request bypass the captcha entirely.)
			return false;
		}
		$j = json_decode( wp_remote_retrieve_body( $resp ), true );
		return ! empty( $j['success'] );
	}

	/**
	 * @param string $raw Multiline IPs.
	 * @return array
	 */
	public static function parse_lines( $raw ) {
		$out = array();
		foreach ( preg_split( '/[\r\n]+/', (string) $raw ) as $line ) {
			$line = trim( $line );
			if ( '' !== $line && filter_var( $line, FILTER_VALIDATE_IP ) ) {
				$out[] = $line;
			}
		}
		return array_unique( $out );
	}

	/**
	 * @param string $raw Comma-separated ISO codes.
	 * @return array
	 */
	public static function parse_countries( $raw ) {
		$out = array();
		foreach ( preg_split( '/[,\s]+/', strtoupper( (string) $raw ) ) as $cc ) {
			$cc = sanitize_key( $cc );
			if ( strlen( $cc ) === 2 ) {
				$out[] = strtoupper( $cc );
			}
		}
		return array_unique( $out );
	}

	/**
	 * Country list for admin picker (ISO => label).
	 *
	 * @param string $lang ar|en.
	 * @return array
	 */
	public static function country_options( $lang = 'ar' ) {
		$en = array(
			'RU' => 'Russia',
			'CN' => 'China',
			'KP' => 'North Korea',
			'IR' => 'Iran',
			'SY' => 'Syria',
			'CU' => 'Cuba',
			'BY' => 'Belarus',
			'VE' => 'Venezuela',
			'AF' => 'Afghanistan',
			'IQ' => 'Iraq',
			'LY' => 'Libya',
			'SD' => 'Sudan',
			'YE' => 'Yemen',
			'US' => 'United States',
			'GB' => 'United Kingdom',
			'EG' => 'Egypt',
			'SA' => 'Saudi Arabia',
			'AE' => 'UAE',
			'IN' => 'India',
			'PK' => 'Pakistan',
		);
		$ar = array(
			'RU' => 'روسيا',
			'CN' => 'الصين',
			'KP' => 'كوريا الشمالية',
			'IR' => 'إيران',
			'SY' => 'سوريا',
			'CU' => 'كوبا',
			'BY' => 'بيلاروس',
			'VE' => 'فنزويلا',
			'AF' => 'أفغانستان',
			'IQ' => 'العراق',
			'LY' => 'ليبيا',
			'SD' => 'السودان',
			'YE' => 'اليمن',
			'US' => 'الولايات المتحدة',
			'GB' => 'بريطانيا',
			'EG' => 'مصر',
			'SA' => 'السعودية',
			'AE' => 'الإمارات',
			'IN' => 'الهند',
			'PK' => 'باكستان',
		);
		return 'ar' === $lang ? $ar : $en;
	}

	/**
	 * @return array<string, string> Prefix => ISO.
	 */
	private static function dialing_prefix_map() {
		return array(
			'20'   => 'EG',
			'966'  => 'SA',
			'971'  => 'AE',
			'964'  => 'IQ',
			'963'  => 'SY',
			'98'   => 'IR',
			'7'    => 'RU',
			'86'   => 'CN',
			'850'  => 'KP',
			'53'   => 'CU',
			'375'  => 'BY',
			'58'   => 'VE',
			'93'   => 'AF',
			'218'  => 'LY',
			'249'  => 'SD',
			'967'  => 'YE',
			'1'    => 'US',
			'44'   => 'GB',
			'91'   => 'IN',
			'92'   => 'PK',
		);
	}
}
