<?php
/**
 * Settings I18n integration for WPML / Polylang.
 *
 * - Registers user-entered option strings for translation.
 * - Returns translated values when reading options.
 * - Prevents overwriting default-language option values when admin is viewing another language.
 *
 * @package KimaAI\Admin
 */

declare(strict_types=1);

namespace KimaAI\Admin;

use WP_Post;

defined('ABSPATH') || exit;

final class SettingsI18n {

  /**
   * WPML "domain" for String Translation.
   * This appears as the "context/domain" in WPML String Translation UI.
   *
   * @var string
   */
  private string $wpml_domain = 'kimaai';

  /**
   * Polylang group name for Strings translations UI.
   *
   * @var string
   */
  private string $polylang_group = 'KimaAI';

  /**
   * Map of option_name => list of translatable items:
   * [
   *   'kimaai_options' => [
   *     ['path' => 'chatbot.appearance.greeting', 'name' => 'settings:kimaai_options.chatbot.appearance.greeting', 'multiline' => false],
   *   ],
   *   'kimaai_some_extra_option' => [...],
   * ]
   *
   * @var array<string, array<int, array{path:string,name:string,multiline:bool}>>
   */
  private array $strings_by_option = [];

  /**
   * Track which option hooks we attached so we can temporarily disable them to fetch raw.
   *
   * @var array<string, callable>
   */
  private array $option_filters = [];

  /**
   * Call once after you have main + extra schemas.
   *
   * @param array $main_schema Main schema for option "kimaai_options".
   * @param array<string, array> $extra_schemas Schemas keyed by save_key option name.
   */
  public function bootstrap(array $main_schema, array $extra_schemas): void {
    // Build map from schemas.
    $this->strings_by_option = [];

    $this->strings_by_option['kimaai_options'] = $this->collectStringsFromSchema($main_schema, 'kimaai_options');

    foreach ($extra_schemas as $option_name => $schema) {
      $this->strings_by_option[$option_name] = $this->collectStringsFromSchema($schema, $option_name);
    }

    // Attach option filters that translate on read.
    foreach (array_keys($this->strings_by_option) as $option_name) {
      $cb = function ($value) use ($option_name) {
        return $this->translateOptionValue($option_name, $value);
      };
      $this->option_filters[$option_name] = $cb;
      add_filter('option_' . $option_name, $cb, 20, 1);
    }

    // Register strings (admin + frontend safe; but we keep it on init).
    add_action('init', [$this, 'registerAllStrings'], 20);
  }

  /**
   * True when either WPML or Polylang is active.
   */
  public static function isIntegrationActive(): bool {
    return self::isWpmlActive() || self::isPolylangActive();
  }

  /**
   * Register all translatable strings to WPML / Polylang.
   * The values used for registration are the RAW stored (default-language) values.
   */
  public function registerAllStrings(): void {
    if (!self::isIntegrationActive()) {
      return;
    }

    foreach ($this->strings_by_option as $option_name => $items) {
      if (empty($items)) {
        continue;
      }

      $raw = $this->getRawOption($option_name, []);
      if (!is_array($raw)) {
        continue;
      }

      foreach ($items as $item) {
        $value = Utils::getByPath($raw, $item['path']);
        if (!is_string($value) || $value === '') {
          continue;
        }

        if (self::isWpmlActive()) {
          // WPML registers dynamic user-entered strings for String Translation.
          do_action('wpml_register_single_string', $this->wpml_domain, $item['name'], $value);
        }

        if (self::isPolylangActive() && function_exists('pll_register_string')) {
          pll_register_string($item['name'], $value, $this->polylang_group, $item['multiline']);
        }
      }
    }
  }

  /**
   * Protect default-language values from being overwritten when viewing a secondary language.
   *
   * Passing the sanitized data. It will revert translatable fields back to the
   * stored raw default-language values when current_lang !== default_lang.
   *
   * @param string $option_name Option name being saved.
   * @param array $sanitized Sanitized new value.
   * @return array Updated value safe to persist.
   */
  public function protectDefaultLanguageWrites(string $option_name, array $sanitized): array {
    $default = self::getDefaultLang();
    $current = self::getCurrentLang();

    if (!$default || !$current || $default === $current) {
      return $sanitized;
    }

    $items = $this->strings_by_option[$option_name] ?? [];
    if (empty($items)) {
      return $sanitized;
    }

    // Use RAW stored options so we restore the canonical default-language fields.
    $raw_existing = $this->getRawOption($option_name, []);
    $raw_existing = is_array($raw_existing) ? $raw_existing : [];

    foreach ($items as $item) {
      // If the incoming payload contains the field, revert it.
      $incoming = Utils::getByPath($sanitized, $item['path']);
      if ($incoming === null) {
        continue;
      }

      $base = Utils::getByPath($raw_existing, $item['path']);
      if ($base === null) {
        continue;
      }

      $sanitized = Utils::setByPath($sanitized, $item['path'], $base);
    }

    return $sanitized;
  }

  /**
   * Translate an option array (on read) if current language differs from default.
   *
   * @param string $option_name
   * @param mixed $value
   * @return mixed
   */
  private function translateOptionValue(string $option_name, $value) {
    if (!is_array($value)) {
      return $value;
    }

    $default = self::getDefaultLang();
    $current = self::getCurrentLang();

    if (!$default || !$current || $default === $current) {
      return $value;
    }

    $items = $this->strings_by_option[$option_name] ?? [];
    if (empty($items)) {
      return $value;
    }

    $out = $value;

    foreach ($items as $item) {
      $raw = Utils::getByPath($out, $item['path']);
      if (!is_string($raw) || $raw === '') {
        continue;
      }

      $translated = $this->translateString($raw, $item['name'], $current);
      if (is_string($translated) && $translated !== '') {
        $out = Utils::setByPath($out, $item['path'], $translated);
      }
    }

    return $out;
  }

  /**
   * Translate a single string via WPML or Polylang.
   *
   * @param string $value Original value.
   * @param string $name Stable name/id for translation lookup.
   * @param string $lang Target language code.
   * @return string
   */
  private function translateString(string $value, string $name, string $lang): string {
    if (self::isWpmlActive()) {
      $translated = apply_filters('wpml_translate_single_string', $value, $this->wpml_domain, $name, $lang);
      return is_string($translated) ? $translated : $value;
    }

    if (self::isPolylangActive() && function_exists('pll_translate_string')) {
      $translated = pll_translate_string($value, $lang);
      return is_string($translated) ? $translated : $value;
    }

    return $value;
  }

  /**
   * Collect translatable leaf strings from a schema.
   *
   * Convention:
   * - Only leaf fields with ['translate' => true] are collected.
   * - Only type === 'string' is collected (you can extend later).
   * - Path format uses dot-notation compatible with Utils::getByPath / setByPath.
   *
   * @param array $schema
   * @param string $option_name
   * @return array<int, array{path:string,name:string,multiline:bool}>
   */
  private function collectStringsFromSchema(array $schema, string $option_name): array {
    $items = [];
    $this->walkSchema($schema, '', $option_name, $items);

    // Deterministic order.
    usort($items, static fn($left, $right) => strcmp($left['name'], $right['name']));

    return $items;
  }

  /**
   * Walks a schema tree and collects translatable paths.
   *
   * @param array $node
   * @param string $path
   * @param string $option_name
   * @param array<int, array{path:string,name:string,multiline:bool}> &$out
   */
  private function walkSchema(array $node, string $path, string $option_name, array &$out): void {
    $type = $node['type'] ?? null;

    // Objects recurse into properties.
    if ($type === 'object' && !empty($node['properties']) && is_array($node['properties'])) {
      foreach ($node['properties'] as $k => $child) {
        if (!is_array($child)) {
          continue;
        }
        $child_path = ($path === '') ? (string) $k : ($path . '.' . $k);
        $this->walkSchema($child, $child_path, $option_name, $out);
      }
      return;
    }

    // Arrays: if you later want arrays of strings translated, implement here.
    if ($type === 'array' && isset($node['items']) && is_array($node['items'])) {
      // Common best practice: do not auto-translate arrays unless explicitly required,
      // because translation UIs become noisy quickly. Extend if needed.
      return;
    }

    // Leaf string fields marked translate => true.
    if (($node['translate'] ?? false) === true && $type === 'string' && $path !== '') {
      $multiline = (($node['component'] ?? '') === 'TextareaField')
      || (($node['sanitize_callback'] ?? '') === 'sanitize_textarea_field');

      $out[] = [
        'path' => $path,
        'name' => 'settings:' . $option_name . '.' . $path,
        'multiline' => (bool) $multiline,
      ];
    }
  }

  /**
   * Fetch the RAW stored option value (without our translation filter).
   *
   * @param string $option_name
   * @param mixed $default_value
   * @return mixed
   */
  private function getRawOption(string $option_name, $default_value) {
    $cb = $this->option_filters[$option_name] ?? null;
    if (is_callable($cb)) {
      remove_filter('option_' . $option_name, $cb, 20);
    }

    $raw = get_option($option_name, $default_value);

    if (is_callable($cb)) {
      add_filter('option_' . $option_name, $cb, 20, 1);
    }

    return $raw;
  }

  /**
   * Public wrapper to fetch RAW stored option value (without translation filter).
   *
   * @param string $option_name Option name.
   * @param mixed $default_value Fallback value.
   * @return mixed
   */
  public function getRawOptionValue(string $option_name, $default_value = null) {
    return $this->getRawOption($option_name, $default_value);
  }

  /**
   * Checks whether WPML is active.
   */
  public static function isWpmlActive(): bool {
    return defined('ICL_SITEPRESS_VERSION');
  }

  /**
   * Checks whether Polylang is active.
   */
  public static function isPolylangActive(): bool {
    return function_exists('pll_current_language') || defined('POLYLANG_VERSION');
  }

  /**
   * Gets the default language slug.
   */
  public static function getDefaultLang(): ?string {
    if (self::isWpmlActive()) {
      $lang = apply_filters('wpml_default_language', null);
      return is_string($lang) && $lang !== '' ? $lang : null;
    }

    if (self::isPolylangActive() && function_exists('pll_default_language')) {
      $lang = pll_default_language('slug');
      return is_string($lang) && $lang !== '' ? $lang : null;
    }

    return null;
  }

  /**
   * Gets the requested language slug.
   */
  public static function getRequestLang(): ?string {
    // Polylang recommends using `lang` in Ajax/requests to force the language.
    $lang = filter_input(INPUT_POST, 'lang', FILTER_UNSAFE_RAW);

    if ($lang === null || $lang === false || $lang === '') {
      $lang = filter_input(INPUT_GET, 'lang', FILTER_UNSAFE_RAW);
    }

    if ($lang === null || $lang === false || $lang === '') {
      return null;
    }

    return sanitize_text_field(wp_unslash((string) $lang));
  }

  /**
   * Gets the current language slug.
   */
  public static function getCurrentLang(): ?string {
    // WPML
    if (self::isWpmlActive()) {
      $lang = apply_filters('wpml_current_language', null);
      if (is_string($lang) && $lang !== '') {
        return $lang;
      }

      // REST fallback (WPML setups often also accept `lang` in requests)
      if (defined('REST_REQUEST') && REST_REQUEST) {
        $req = self::getRequestLang();
        if ($req) {
          return $req;
        }
      }

      return null;
    }

    // Polylang
    if (self::isPolylangActive() && function_exists('pll_current_language')) {
      $lang = pll_current_language('slug');
      if (is_string($lang) && $lang !== '') {
        return $lang;
      }

      // REST fallback: Polylang often has no current language in REST unless provided.
      if (defined('REST_REQUEST') && REST_REQUEST) {
        $req = self::getRequestLang();
        if ($req) {
          return $req;
        }
      }

      return null;
    }

    return null;
  }

  /**
   * Gets active languages.
   */
  public static function getActiveLangs(): array {
    $languages = [];
    if (self::isWpmlActive()) {
      $raw = apply_filters('wpml_active_languages', null, [
        'skip_missing' => 0,
        'orderby' => 'code',
      ]);

      if (is_array($raw)) {
        foreach ($raw as $code => $lang) {
          if (!is_array($lang)) {
            continue;
          }

          $languages[] = [
            'code' => (string) ($lang['code'] ?? $code),
            'name' => (string) ($lang['translated_name'] ?? $lang['native_name'] ?? $code),
            'locale' => (string) ($lang['default_locale'] ?? ''),
            'flag_url' => (string) ($lang['country_flag_url'] ?? ''),
          ];
        }
      }
    }

    if (self::isPolylangActive()) {
      $languages = array_map(function ($lang) {
        return [
          'code' => $lang->slug,
          'name' => $lang->name,
          'locale' => $lang->locale,
          'flag_url' => $lang->flag_url,
          'flag' => $lang->flag,
        ];
      }, PLL()->model->get_languages_list());
    }

    return $languages;
  }

  /**
   * Gets the active translation provider.
   */
  public static function getProvider(): string {
    if (self::isWpmlActive()) {
      return 'wpml';
    }

    if (self::isPolylangActive()) {
      return 'polylang';
    }

    return 'none';
  }

  /**
   * Get the language slug/code for a specific post.
   *
   * Returns values like `en`, `de`, `fr`.
   * Falls back to the website language when WPML / Polylang is not active.
   */
  public static function getPostLang(WP_Post $post): ?string {
    if (self::isWpmlActive()) {
      $details = apply_filters('wpml_post_language_details', null, $post->ID);
      $lang = is_array($details) ? ($details['language_code'] ?? null) : null;

      return is_string($lang) && $lang !== '' ? $lang : null;
    }

    if (self::isPolylangActive() && function_exists('pll_get_post_language')) {
      $lang = pll_get_post_language($post->ID, 'slug');
      return is_string($lang) && $lang !== '' ? $lang : null;
    }

    $locale = get_locale();
    if (!is_string($locale) || $locale === '') {
      return null;
    }

    $lang = strtolower((string) preg_replace('/[_-].*$/', '', $locale));

    return $lang !== '' ? $lang : null;
  }
}
