<?php
namespace TitanCart\Core;

defined( 'ABSPATH' ) || exit;

/**
 * I18n — Multilanguage abstraction layer.
 *
 * Works standalone via the `tc_languages` table, or defers to WPML / Polylang
 * when either is active.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * ⚠️ REWRITTEN S362 (KI-49). Every query in the previous version was wrong.
 *
 * The class had ZERO callers anywhere in the tree, which is precisely why nine
 * separate defects survived in it undetected. Corrections, all verified against
 * `src/DB/Schema.php` (#167-A) and `src/DB/Database.php` before writing:
 *
 *   1. Table names were hard-coded UNPREFIXED (`tc_languages`). The live tables
 *      are `{$wpdb->prefix}tc_languages`. Now resolved via `Database::table()`.
 *   2. `Database` has NO static `instance()`. It is constructed and resolved
 *      through the service container — the accessor is the global `\titancart_db()`
 *      (`titancart.php` L151 → `Plugin::service('db')`). `Database::instance()`
 *      is a fatal "call to undefined method".
 *   3. `Database` has NO `get_row()` / `get_results()` / `get_var()`. The ORM's
 *      methods are `getRow()`, `getArray()`, `getField()`.
 *   3. `Database::prepare()` takes `?s` / `?i` placeholders, NOT `%s` / `%d`.
 *   4. `getRow()` returns an ARRAY, not an object — `$row->lang_code` could
 *      never have worked even had the column existed.
 *   5. `tc_languages` has no `lang_code` column. It is `code` (Schema L255).
 *   6. `tc_languages` has no `sort_order` column at all. Ordering is now
 *      `is_default DESC, name ASC`.
 *   7. `status` is `ENUM('active','disabled')` (Schema L259) — NOT the CS-Cart
 *      `'A'` / `'D'` values the old code passed. CS-Cart is a FUNCTIONAL
 *      reference, never a schema one.
 *   8. The translation tables are `tc_product_translations` and
 *      `tc_category_translations` — NOT `*_descriptions`.
 *   9. Their language column is `language_code` (Schema L500), not `lang_code`.
 *
 * `lang_code` is not a typo of `language_code`. It is a REAL column — on
 * `tc_newsletter_subscribers` (Schema L2101). One word, correct in one table
 * and wrong in another. **The column's home table is a schema fact, not a
 * naming fact** (#449).
 *
 * CONTRACT CHANGE (safe: no callers existed): `get_translated_field()` and
 * `save_description()` now take a SHORT table name (`'product_translations'`),
 * matching `Database::insert()` / `update()` / `delete()`. They prefix it
 * internally. Never pass a prefixed name.
 * ─────────────────────────────────────────────────────────────────────────────
 *
 * @package TitanCart\Core
 */

use TitanCart\DB\Database;

class I18n {

	/** Language column on every `*_translations` table. */
	private const LANG_COL = 'language_code';

	/** @var string|null Cached current language code */
	private static ?string $current = null;

	/** @var array|null Cached language list */
	private static ?array $languages = null;

	/* ──────────────────────────────────────────────────────────────────────────
	 * Language resolution
	 * ────────────────────────────────────────────────────────────────────────── */

	/**
	 * Get the active language code for the current request.
	 * Checks WPML first, then Polylang, then falls back to the WP locale.
	 */
	public static function get_current_language(): string {
		if ( self::$current !== null ) {
			return self::$current;
		}

		// WPML
		if ( defined( 'ICL_LANGUAGE_CODE' ) && ICL_LANGUAGE_CODE ) {
			self::$current = (string) ICL_LANGUAGE_CODE;
			return self::$current;
		}

		// Polylang
		if ( function_exists( 'pll_current_language' ) ) {
			$lang = pll_current_language( 'slug' );
			if ( $lang ) {
				self::$current = (string) $lang;
				return self::$current;
			}
		}

		// WordPress locale → strip region suffix (e.g. en_US → en)
		$locale        = get_locale();
		self::$current = strstr( $locale, '_', true ) ?: $locale;
		return self::$current;
	}

	/**
	 * Override the detected language (useful in REST / CLI contexts).
	 */
	public static function set_current_language( string $language_code ): void {
		self::$current = $language_code;
	}

	/**
	 * Return the default language code from `tc_languages`.
	 *
	 * Falls back to 'en' when the table is empty or no row is flagged default —
	 * this method sits in the fallback path of `get_translated_field()`, so it
	 * must never return null.
	 */
	public static function get_default_language(): string {
		$db   = \titancart_db();
		$code = $db->getField(
			'SELECT code FROM `' . $db->table( 'languages' ) . '` WHERE is_default = 1 AND status = ?s LIMIT 1',
			'active'
		);

		return $code ? (string) $code : 'en';
	}

	/**
	 * Return all active languages, ordered with the default first.
	 *
	 * `tc_languages` has no `sort_order` column (Schema L253–262), so ordering
	 * is `is_default DESC, name ASC`.
	 *
	 * @return array<int, array{code: string, name: string, native_name: string, is_default: int}>
	 */
	public static function get_languages(): array {
		if ( self::$languages !== null ) {
			return self::$languages;
		}

		$db   = \titancart_db();
		$rows = $db->getArray(
			'SELECT code, name, native_name, is_default FROM `' . $db->table( 'languages' ) . '`'
			. ' WHERE status = ?s ORDER BY is_default DESC, name ASC',
			'active'
		);

		self::$languages = $rows;
		return self::$languages;
	}

	/**
	 * True when more than one language is configured and active.
	 */
	public static function is_multilang_active(): bool {
		return count( self::get_languages() ) > 1;
	}

	/* ──────────────────────────────────────────────────────────────────────────
	 * Translation fetch helpers
	 * ────────────────────────────────────────────────────────────────────────── */

	/**
	 * Fetch a translated field from any `*_translations` table.
	 *
	 * @param string      $table         SHORT table name, e.g. 'product_translations'.
	 * @param string      $id_col        Foreign-key column, e.g. 'product_id'.
	 * @param int         $object_id     The object's ID.
	 * @param string      $field         Column to retrieve.
	 * @param string|null $language_code Language code; null = current language.
	 * @return string|null
	 */
	public static function get_translated_field(
		string $table,
		string $id_col,
		int $object_id,
		string $field,
		?string $language_code = null
	): ?string {
		$lang = $language_code ?? self::get_current_language();
		$db   = \titancart_db();

		// $table / $id_col / $field are developer-supplied identifiers, never
		// user input; the ORM has no identifier placeholder. Backtick-quote them
		// and strip backticks so an identifier can't close its own quoting.
		$t = '`' . $db->table( str_replace( '`', '', $table ) ) . '`';
		$c = '`' . str_replace( '`', '', $id_col ) . '`';
		$f = '`' . str_replace( '`', '', $field ) . '`';
		$l = '`' . self::LANG_COL . '`';

		$value = $db->getField(
			"SELECT {$f} FROM {$t} WHERE {$c} = ?i AND {$l} = ?s LIMIT 1",
			$object_id,
			$lang
		);

		// Fallback to the default language.
		$default = self::get_default_language();
		if ( $value === null && $lang !== $default ) {
			$value = $db->getField(
				"SELECT {$f} FROM {$t} WHERE {$c} = ?i AND {$l} = ?s LIMIT 1",
				$object_id,
				$default
			);
		}

		return $value === null ? null : (string) $value;
	}

	/**
	 * Generic translate helper.
	 *
	 * @param string      $object_type   'product' or 'category'.
	 * @param int         $object_id
	 * @param string      $field         Column name in the translations table.
	 * @param string|null $language_code null = current language.
	 * @return string|null
	 */
	public static function translate(
		string $object_type,
		int $object_id,
		string $field,
		?string $language_code = null
	): ?string {
		$map = self::translation_tables();

		if ( ! isset( $map[ $object_type ] ) ) {
			return null;
		}

		[ $table, $id_col ] = $map[ $object_type ];
		return self::get_translated_field( $table, $id_col, $object_id, $field, $language_code );
	}

	/**
	 * object_type → [ short table name, FK column ].
	 *
	 * Both tables are created by `Schema::product_translations()` (L496) and
	 * `Schema::category_translations()`. The old `*_descriptions` names have
	 * never existed in this codebase.
	 *
	 * @return array<string, array{0: string, 1: string}>
	 */
	private static function translation_tables(): array {
		return [
			'product'  => [ 'product_translations',  'product_id' ],
			'category' => [ 'category_translations', 'category_id' ],
		];
	}

	/* ──────────────────────────────────────────────────────────────────────────
	 * Translation save helpers
	 * ────────────────────────────────────────────────────────────────────────── */

	/**
	 * Upsert a row in any `*_translations` table.
	 *
	 * @param string      $table  SHORT table name, e.g. 'product_translations'.
	 * @param string      $id_col FK column name.
	 * @param int         $id     Object ID.
	 * @param array       $fields Associative array of column => value.
	 * @param string|null $lang   null = current language.
	 * @return bool True when the row was written.
	 */
	public static function save_translation(
		string $table,
		string $id_col,
		int $id,
		array $fields,
		?string $lang = null
	): bool {
		if ( ! $fields ) {
			return false;
		}

		$lang = $lang ?? self::get_current_language();
		$db   = \titancart_db();

		$short = str_replace( '`', '', $table );
		$t     = '`' . $db->table( $short ) . '`';
		$c     = '`' . str_replace( '`', '', $id_col ) . '`';
		$l     = '`' . self::LANG_COL . '`';

		$where = [ $id_col => $id, self::LANG_COL => $lang ];

		$exists = (int) $db->getField(
			"SELECT COUNT(*) FROM {$t} WHERE {$c} = ?i AND {$l} = ?s",
			$id,
			$lang
		);

		if ( $exists > 0 ) {
			return $db->update( $short, $fields, $where ) !== false;
		}

		return $db->insert( $short, array_merge( $where, $fields ) ) > 0;
	}

	/**
	 * Back-compat alias. The tables are `*_translations`, never `*_descriptions`.
	 *
	 * @deprecated S362 — use save_translation().
	 */
	public static function save_description(
		string $table,
		string $id_col,
		int $id,
		array $fields,
		?string $lang = null
	): bool {
		return self::save_translation( $table, $id_col, $id, $fields, $lang );
	}

	/* ──────────────────────────────────────────────────────────────────────────
	 * WPML integration
	 * ────────────────────────────────────────────────────────────────────────── */

	/**
	 * Register TitanCart custom tables with WPML (if active).
	 * Called on 'plugins_loaded' after WPML initialises.
	 */
	public static function register_wpml_tables(): void {
		if ( ! defined( 'WPML_PLUGIN_PATH' ) ) {
			return;
		}

		// WPML reads wpml-config.xml from the plugin root; we also hook into
		// wpml_get_translatable_documents for programmatic registration.
		add_filter( 'wpml_translatable_documents', static function ( $types ) {
			$types['tc_product']  = [
				'labels'     => [ 'name' => __( 'TitanCart Products', 'titancart-lite' ) ],
				'taxonomies' => [],
			];
			$types['tc_category'] = [
				'labels' => [ 'name' => __( 'TitanCart Categories', 'titancart-lite' ) ],
			];
			return $types;
		} );
	}

	/* ──────────────────────────────────────────────────────────────────────────
	 * Language management (admin)
	 * ────────────────────────────────────────────────────────────────────────── */

	/**
	 * Add a language to `tc_languages`.
	 *
	 * `code` is `CHAR(2)` with a UNIQUE key (Schema L255/L261), so anything
	 * longer would be silently truncated into a collision with an existing row.
	 * ISO 639-1 codes are exactly two characters; reject everything else rather
	 * than let MySQL decide.
	 *
	 * @param string $code        ISO 639-1 code, e.g. 'fr'.
	 * @param string $name        Human name, e.g. 'French'.
	 * @param bool   $default     Set as the default language.
	 * @param string $native_name Optional endonym, e.g. 'Français'.
	 * @return bool
	 */
	public static function add_language(
		string $code,
		string $name,
		bool $default = false,
		string $native_name = ''
	): bool {
		$db   = \titancart_db();
		$code = strtolower( sanitize_key( $code ) );

		if ( strlen( $code ) !== 2 ) {
			return false;
		}

		if ( $default ) {
			$db->update( 'languages', [ 'is_default' => 0 ], [ 'is_default' => 1 ] );
		}

		$id = $db->insert( 'languages', [
			'code'        => $code,
			'name'        => sanitize_text_field( $name ),
			'native_name' => sanitize_text_field( $native_name ),
			'is_default'  => (int) $default,
			'status'      => 'active', // ENUM('active','disabled') — NOT CS-Cart's 'A'.
		] );

		self::flush_cache();
		return $id > 0;
	}

	/**
	 * Flush the in-memory language cache (e.g. after saving settings).
	 */
	public static function flush_cache(): void {
		self::$current   = null;
		self::$languages = null;
	}
}
