<?php
/**
 * Plugin language: follows WordPress, with a per user override.
 *
 * Source strings are English, Czech ships as a cs_CZ translation. A site in
 * Czech therefore sees Czech without doing anything, a site in any other
 * language falls back to English instead of showing Czech it cannot read.
 *
 * The switcher is deliberately per user, not per site: an agency admin can
 * work in English while the client keeps the whole administration in Czech.
 *
 * @package MarketingConsole
 */

namespace MarketingConsole\Core;

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

/**
 * Loads the text domain and applies the user's language choice.
 */
final class Language {

	public const USER_META  = 'speedlyst_locale';
	public const TEXTDOMAIN = 'speedlyst';

	/**
	 * Supported WordPress locales and their native labels.
	 */
	private const SUPPORTED = array(
		'en_US' => 'English',
		'de_DE' => 'Deutsch',
		'es_ES' => 'Español',
		'pt_BR' => 'Português do Brasil',
		'fr_FR' => 'Français',
		'it_IT' => 'Italiano',
		'ja'    => '日本語',
		'nl_NL' => 'Nederlands',
		'pl_PL' => 'Polski',
		'ru_RU' => 'Русский',
		'es_MX' => 'Español de México',
		'id_ID' => 'Bahasa Indonesia',
		'tr_TR' => 'Türkçe',
		'uk'    => 'Українська',
		'cs_CZ' => 'Čeština',
		'pt_PT' => 'Português',
		'ro_RO' => 'Română',
		'hu_HU' => 'Magyar',
		'zh_CN' => '简体中文',
		'zh_TW' => '繁體中文',
		'ko_KR' => '한국어',
	);

	/** @var bool */
	private $wporg_build;

	/**
	 * @param bool|null $wporg_build Distribution override for deterministic tests.
	 */
	public function __construct( ?bool $wporg_build = null ) {
		// Plugin ma jedinou distribucni linii (wordpress.org), takze vychozi
		// chovani je language pack first s bundled fallbackem. Parametr
		// zustava kvuli deterministickym testum obou vetvi.
		$this->wporg_build = null === $wporg_build ? true : $wporg_build;
	}

	/**
	 * @return void
	 */
	public function register(): void {
		add_filter( 'plugin_locale', array( $this, 'filter_locale' ), 10, 2 );
		// Self-hosted distribuce používá zmrazené bundled katalogy. WordPress.org
		// distribuce ponechává core loaderu language pack first a bundled fallback.
		// Explicitní English se odpojí od všech katalogů a použije source strings.
		add_filter( 'load_textdomain_mofile', array( $this, 'filter_mofile' ), 10, 2 );
		// Since WordPress 6.5, WP_Translations resolves through one shared
		// controller locale. Another domain loaded later in the request can
		// therefore switch that controller back to the site language after our
		// selected catalog was loaded. Keep this plugin's per-user choice scoped
		// to its own domain at lookup time as well as at catalog-load time.
		add_filter( 'gettext_' . self::TEXTDOMAIN, array( $this, 'filter_gettext' ), 10, 3 );
		add_filter( 'gettext_with_context_' . self::TEXTDOMAIN, array( $this, 'filter_gettext_with_context' ), 10, 4 );
		add_filter( 'ngettext_' . self::TEXTDOMAIN, array( $this, 'filter_ngettext' ), 10, 5 );
		add_filter( 'ngettext_with_context_' . self::TEXTDOMAIN, array( $this, 'filter_ngettext_with_context' ), 10, 6 );
		add_action( 'init', array( $this, 'load_textdomain' ) );
		add_action( 'admin_post_speedlyst_free_set_locale', array( $this, 'handle_switch' ) );
	}

	/**
	 * Restore the selected locale when the WordPress translation controller was
	 * changed by another text domain later in the request.
	 *
	 * @param string $translation WordPress' default translation.
	 * @param string $text Source string.
	 * @param string $domain Text domain.
	 * @return string
	 */
	public function filter_gettext( $translation, $text, $domain ): string {
		return $this->translate_for_locale( $translation, $text );
	}

	/**
	 * Context-aware counterpart of filter_gettext().
	 *
	 * @param string $translation WordPress' default translation.
	 * @param string $text Source string.
	 * @param string $context Gettext context.
	 * @param string $domain Text domain.
	 * @return string
	 */
	public function filter_gettext_with_context( $translation, $text, $context, $domain ): string {
		return $this->translate_for_locale( $translation, $text, $context );
	}

	/**
	 * Plural-aware counterpart of filter_gettext().
	 *
	 * @param string $translation WordPress' default translation.
	 * @param string $single Singular source string.
	 * @param string $plural Plural source string.
	 * @param int    $number Selected quantity.
	 * @param string $domain Text domain.
	 * @return string
	 */
	public function filter_ngettext( $translation, $single, $plural, $number, $domain ): string {
		return $this->translate_plural_for_locale( $translation, $single, $plural, $number );
	}

	/**
	 * Context-aware counterpart of filter_ngettext().
	 *
	 * @param string $translation WordPress' default translation.
	 * @param string $single Singular source string.
	 * @param string $plural Plural source string.
	 * @param int    $number Selected quantity.
	 * @param string $context Gettext context.
	 * @param string $domain Text domain.
	 * @return string
	 */
	public function filter_ngettext_with_context( $translation, $single, $plural, $number, $context, $domain ): string {
		return $this->translate_plural_for_locale( $translation, $single, $plural, $number, $context );
	}

	/**
	 * @param string $translation WordPress' default translation.
	 * @param string $text Source string.
	 * @param string $context Gettext context.
	 * @return string
	 */
	private function translate_for_locale( $translation, $text, $context = '' ): string {
		if ( ! class_exists( '\\WP_Translation_Controller' ) ) {
			return $translation;
		}

		$locale = $this->effective_locale();
		if ( 'en_US' === $locale ) {
			return $text;
		}

		$localized = \WP_Translation_Controller::get_instance()->translate( $text, $context, self::TEXTDOMAIN, $locale );

		return false === $localized ? $text : $localized;
	}

	/**
	 * @param string $translation WordPress' default translation.
	 * @param string $single Singular source string.
	 * @param string $plural Plural source string.
	 * @param int    $number Selected quantity.
	 * @param string $context Gettext context.
	 * @return string
	 */
	private function translate_plural_for_locale( $translation, $single, $plural, $number, $context = '' ): string {
		if ( ! class_exists( '\\WP_Translation_Controller' ) ) {
			return $translation;
		}

		$locale = $this->effective_locale();
		if ( 'en_US' === $locale ) {
			return 1 === (int) $number ? $single : $plural;
		}

		$localized = \WP_Translation_Controller::get_instance()->translate_plural( array( $single, $plural ), (int) $number, $context, self::TEXTDOMAIN, $locale );

		return false === $localized ? ( 1 === (int) $number ? $single : $plural ) : $localized;
	}

	/**
	 * Apply the catalog precedence contract for the active distribution.
	 *
	 * @param string $mofile Resolved .mo path.
	 * @param string $domain Text domain being loaded.
	 * @return string
	 */
	public function filter_mofile( $mofile, $domain ) {
		if ( self::TEXTDOMAIN !== $domain || ! defined( 'MARKETING_CONSOLE_DIR' ) ) {
			return $mofile;
		}

		$chosen = $this->chosen_locale();
		if ( $this->wporg_build && '' === $chosen ) {
			return $mofile;
		}

		$locale = '' === $chosen ? $this->effective_locale() : $chosen;

		if ( 'en_US' === $locale ) {
			return constant( 'MARKETING_CONSOLE_DIR' ) . '/languages/.' . self::TEXTDOMAIN . '-en_US.disabled.mo';
		}

		if ( $this->wporg_build ) {
			return $mofile;
		}

		return constant( 'MARKETING_CONSOLE_DIR' ) . '/languages/' . self::TEXTDOMAIN . '-' . $locale . '.mo';
	}

	/**
	 * @return void
	 */
	public function load_textdomain(): void {
		if ( ! defined( 'MARKETING_CONSOLE_FILE' ) ) {
			return;
		}

		// Register the custom bundled path for WordPress just-in-time loading.
		// phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Required for bundled fallback and the explicit per-user locale override.
		load_plugin_textdomain( self::TEXTDOMAIN, false, dirname( plugin_basename( constant( 'MARKETING_CONSOLE_FILE' ) ) ) . '/languages' );

		$locale = $this->effective_locale();
		if ( 'en_US' === $locale ) {
			return;
		}

		// WordPress does not perform a per-string fallback from an incomplete
		// directory pack to the plugin's bundled catalog automatically. Loading
		// the pack first and the complete bundled catalog second creates the
		// intended union: approved pack strings win, bundled strings fill gaps.
		if ( $this->wporg_build && defined( 'WP_LANG_DIR' ) ) {
			$pack = constant( 'WP_LANG_DIR' ) . '/plugins/' . self::TEXTDOMAIN . '-' . $locale . '.mo';
			load_textdomain( self::TEXTDOMAIN, $pack, $locale );
		}

		$bundled = constant( 'MARKETING_CONSOLE_DIR' ) . '/languages/' . self::TEXTDOMAIN . '-' . $locale . '.mo';
		load_textdomain( self::TEXTDOMAIN, $bundled, $locale );
	}

	/**
	 * Override the locale for this plugin only, never for the whole admin.
	 *
	 * @param string $locale Locale WordPress resolved.
	 * @param string $domain Text domain being loaded.
	 * @return string
	 */
	public function filter_locale( $locale, $domain ) {
		if ( self::TEXTDOMAIN !== $domain ) {
			return $locale;
		}

		$chosen = $this->chosen_locale();

		return '' === $chosen ? $locale : $chosen;
	}

	/**
	 * The current user's explicit choice, or an empty string for "follow WordPress".
	 *
	 * @return string
	 */
	public function chosen_locale(): string {
		if ( ! function_exists( 'get_current_user_id' ) ) {
			return '';
		}

		$user_id = get_current_user_id();
		if ( 0 === $user_id ) {
			return '';
		}

		$stored = get_user_meta( $user_id, self::USER_META, true );

		return is_string( $stored ) && array_key_exists( $stored, self::SUPPORTED ) ? $stored : '';
	}

	/**
	 * The locale the plugin actually renders in, including "follow WordPress".
	 *
	 * The selector state has to follow what the reader sees, not only what is
	 * stored, so an empty choice resolves to the WordPress locale.
	 *
	 * @return string
	 */
	public function effective_locale(): string {
		$chosen = $this->chosen_locale();

		if ( '' !== $chosen ) {
			return $chosen;
		}

		$locale = function_exists( 'determine_locale' ) ? determine_locale() : '';

		if ( '' === $locale && function_exists( 'get_locale' ) ) {
			$locale = get_locale();
		}

		return is_string( $locale ) && array_key_exists( $locale, self::SUPPORTED ) ? $locale : 'en_US';
	}

	/**
	 * Store the choice; an unknown value means "follow WordPress".
	 *
	 * @return void
	 */
	public function handle_switch(): void {
		$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : '';

		if ( 'POST' !== $method ) {
			wp_die( esc_html__( 'This action requires POST.', 'speedlyst' ), '', array( 'response' => 405 ) );
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to perform this action.', 'speedlyst' ), '', array( 'response' => 403 ) );
		}

		check_admin_referer( 'speedlyst_free_set_locale' );

		$requested = isset( $_POST['mc_locale'] ) ? sanitize_text_field( wp_unslash( $_POST['mc_locale'] ) ) : '';
		$user_id   = get_current_user_id();

		if ( array_key_exists( $requested, self::SUPPORTED ) ) {
			update_user_meta( $user_id, self::USER_META, $requested );
		} else {
			delete_user_meta( $user_id, self::USER_META );
		}

		wp_safe_redirect( add_query_arg( array( 'page' => 'speedlyst' ), admin_url( 'admin.php' ) ) );
		exit;
	}

	/**
	 * Options for the switcher: label per supported locale plus "follow WordPress".
	 *
	 * @return array<string,string>
	 */
	public function options(): array {
		return array( '' => __( 'System', 'speedlyst' ) ) + self::SUPPORTED;
	}
}
