<?php
/**
 * File Path: includes/helpers/class-mbs-geo-redirect.php
 *
 * Geo-Redirect / Link Localization — My Book Showroom
 *
 * Provides `MBS_Geo_Redirect::resolve()` — a static method that returns
 * the locale-specific variant of a buy-link URL when:
 *   a) The site has a PRO license.
 *   b) The visitor's Accept-Language header identifies a supported market.
 *   c) The buy link has a non-empty locale URL for that market.
 *
 * If any of these conditions are not met the original URL is returned unchanged.
 * The fallback is always silent — this method never returns empty string or null.
 *
 * Supported locale markets (columns in mbs_buy_links, confirmed from MBS_Buy_Link_DAO):
 *   locale_us  → US (United States)
 *   locale_uk  → GB (United Kingdom)
 *   locale_ca  → CA (Canada)
 *   locale_au  → AU (Australia)
 *   locale_de  → DE (Germany); also matched when language tag is just 'de'
 *   locale_jp  → JP (Japan); also matched when language tag is just 'ja'
 *
 * No external geoIP service is used. Localisation is derived solely from the
 * HTTP Accept-Language header and the buy-link locale columns in the database.
 *
 * `require_once` in `class-mbs-plugin.php` — OUTSIDE is_admin():
 *   require_once MBS_INCLUDES_DIR . 'helpers/class-mbs-geo-redirect.php';
 * No instantiation needed — resolve() is purely static.
 *
 * Story 14-3 — Sprint 14.
 *
 * @package    My_Book_Showroom
 * @subpackage Includes/Helpers
 * @since      1.0.0
 */

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

class MBS_Geo_Redirect {

	/**
	 * Map from ISO country / language code (uppercase) to buy-link locale column name.
	 *
	 * Country codes (from the region segment of Accept-Language):
	 *   en-GB → GB → locale_uk
	 *   en-CA → CA → locale_ca
	 *
	 * Language-only codes (when no region segment is present, normalised to uppercase):
	 *   de    → DE → locale_de
	 *   ja    → JA → locale_jp
	 */
	const LOCALE_COLUMN_MAP = array(
		'US' => 'locale_us',
		'GB' => 'locale_uk',
		'UK' => 'locale_uk', // defensive alias — some UAs send 'en-UK'.
		'CA' => 'locale_ca',
		'AU' => 'locale_au',
		'DE' => 'locale_de',
		'JP' => 'locale_jp',
		'JA' => 'locale_jp', // language code 'ja' normalised to 'JA'.
	);

	// ── Public API ────────────────────────────────────────────────────────────

	/**
	 * Return the locale-specific URL for a buy link, or the original URL if no
	 * locale match is found.
	 *
	 * @param string $url     The canonical buy-link URL (stored in the `url` column).
	 * @param int    $post_id The WP post ID the buy link belongs to.
	 * @return string         Locale URL (non-empty) or original URL — never empty.
	 */
	public static function resolve( string $url, int $post_id ): string {
		// FREE tier: short-circuit immediately. No DB query, no header read.
		if ( ! mbs_is_pro() ) {
			return $url;
		}

		// ── Parse Accept-Language header ──────────────────────────────────────
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$header = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] )
			? (string) wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- input sanitized via sanitize_* helpers or validated before use; see inline sanitization
			: '';

		if ( '' === $header ) {
			return $url; // Absent header — silent fallback.
		}

		$code = self::extract_locale_code( $header );
		if ( '' === $code ) {
			return $url; // Could not parse a usable code — silent fallback.
		}

		// ── Map code to locale column ─────────────────────────────────────────
		$column = self::LOCALE_COLUMN_MAP[ $code ] ?? '';
		if ( '' === $column ) {
			return $url; // Unsupported market — silent fallback.
		}

		// ── Find matching buy link and read locale column ─────────────────────
		$buy_links = MBS_Buy_Link_DAO::get_all_for_post( $post_id );
		foreach ( $buy_links as $link ) {
			// Match on canonical URL.
			if ( ! isset( $link->url ) || $link->url !== $url ) {
				continue;
			}
			// Return locale URL if non-empty.
			if ( ! empty( $link->$column ) ) {
				return esc_url_raw( (string) $link->$column );
			}
			// Found the row but no locale URL for this market — stop looking.
			break;
		}

		// No match or no locale URL — silent fallback.
		return $url;
	}

	// ── Private helpers ───────────────────────────────────────────────────────

	/**
	 * Extract the primary locale code from an Accept-Language header string.
	 *
	 * Examples:
	 *   'en-GB,en;q=0.9,fr;q=0.8' → 'GB'
	 *   'de-DE,de;q=0.9'           → 'DE'
	 *   'ja'                        → 'JA'
	 *   'en'                        → 'EN'  (not in LOCALE_COLUMN_MAP → fallback)
	 *   ''                          → ''    (caller handles)
	 *
	 * @param  string $header Raw HTTP_ACCEPT_LANGUAGE value.
	 * @return string Uppercase code (country or language), or '' on failure.
	 */
	private static function extract_locale_code( string $header ): string {
		// Take the first locale before any comma.
		$primary = strtok( $header, ',' );
		if ( false === $primary ) {
			return '';
		}

		// Strip quality factor (e.g. ';q=0.9').
		$primary = (string) strtok( $primary, ';' );
		$primary = trim( $primary );

		if ( '' === $primary ) {
			return '';
		}

		// Split on '-': 'en-GB' → ['en', 'GB'].
		if ( strpos( $primary, '-' ) !== false ) {
			$parts = explode( '-', $primary, 2 );
			return strtoupper( trim( $parts[1] ) ); // Region code.
		}

		// Language-only tag: 'de' → 'DE'.
		return strtoupper( $primary );
	}
	// ── Hook registration ─────────────────────────────────────────────────────

	/**
	 * Register the buy-link URL filter.
	 *
	 * Called explicitly by load_display_helpers() after the file is required.
	 * Must be registered outside is_admin() so it fires on front-end requests and REST.
	 *
	 * @return void
	 */
	public static function register(): void {
		add_filter( 'mbs_buy_link_url', array( 'MBS_Geo_Redirect', 'resolve' ), 10, 2 );
	}
}
