<?php
/**
 * Language utility — single source of truth for language sanitization,
 * validation, and primary language resolution.
 *
 * Every language value flowing through the plugin should pass through
 * this class. Use sanitize() on read paths (queries) and with_fallback()
 * on write paths (inserts/updates).
 *
 * @package BoostBot
 * @since   1.3.0
 */

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

class BoostBot_Language {

    /**
     * ISO 639 language code pattern (2–5 lowercase letters).
     */
    const CODE_PATTERN = '/^[a-z]{2,5}$/';

    /**
     * Option key for the admin-configured primary language.
     */
    const PRIMARY_OPTION = 'boostbot_primary_language';

    /**
     * Values that multilingual plugins use to mean "all languages / no filter".
     */
    private static array $no_filter_values = array( 'all', '' );

    /**
     * Cached primary language (cleared on setting change).
     */
    private static ?string $primary_cache = null;

    /**
     * Sanitize a language value for READ paths (queries / filters).
     *
     * Returns a valid ISO 639 code, or empty string meaning "no filter".
     * Rejects 'all', empty, and anything that doesn't match the ISO pattern.
     *
     * @param mixed $raw  Raw language value (from request param, cookie, etc.).
     * @return string Valid language code or '' (no filter).
     */
    public static function sanitize( $raw ): string {
        if ( null === $raw || '' === $raw ) {
            return '';
        }

        $value = sanitize_text_field( (string) $raw );
        $value = strtolower( trim( $value ) );

        if ( in_array( $value, self::$no_filter_values, true ) ) {
            return '';
        }

        if ( ! preg_match( self::CODE_PATTERN, $value ) ) {
            return '';
        }

        return $value;
    }

    /**
     * Sanitize a language value for WRITE paths (creates / updates).
     *
     * Always returns a concrete language code — never empty.
     * Falls back to the site's primary language if the input is invalid.
     *
     * @param mixed       $raw      Raw language value.
     * @param string|null $fallback Optional explicit fallback (e.g. from a bot's language).
     *                              When null, uses get_primary().
     * @return string Valid language code (never empty).
     */
    public static function with_fallback( $raw, ?string $fallback = null ): string {
        $sanitized = self::sanitize( $raw );

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

        if ( null !== $fallback && '' !== $fallback ) {
            $fb = self::sanitize( $fallback );
            if ( '' !== $fb ) {
                return $fb;
            }
        }

        return self::get_primary();
    }

    /**
     * Get the site's primary language.
     *
     * Resolution order:
     * 1. Admin-configured `boostbot_primary_language` option
     * 2. First enabled language from BoostBot_Language_Manager
     * 3. WordPress locale (e.g. 'he_IL' → 'he')
     *
     * @return string Two-letter language code (never empty).
     */
    public static function get_primary(): string {
        if ( null !== self::$primary_cache ) {
            return self::$primary_cache;
        }

        $option = get_option( self::PRIMARY_OPTION, '' );
        if ( is_string( $option ) && '' !== $option ) {
            $clean = self::sanitize( $option );
            if ( '' !== $clean ) {
                self::$primary_cache = $clean;
                return self::$primary_cache;
            }
        }

        if ( class_exists( 'BoostBot_Language_Manager' ) ) {
            $enabled = BoostBot_Language_Manager::get_instance()->get_enabled_languages();
            if ( ! empty( $enabled ) ) {
                self::$primary_cache = $enabled[0];
                return self::$primary_cache;
            }
        }

        self::$primary_cache = self::locale_to_code( get_locale() );
        return self::$primary_cache;
    }

    /**
     * Get the current frontend language.
     *
     * Delegates to BoostBot_Language_Manager for multilingual plugin detection,
     * falls back to get_primary().
     *
     * @return string Two-letter language code.
     */
    public static function get_current(): string {
        if ( class_exists( 'BoostBot_Language_Manager' ) ) {
            return BoostBot_Language_Manager::get_instance()->get_frontend_language();
        }
        return self::get_primary();
    }

    /**
     * Check whether a string is a valid ISO 639 language code.
     *
     * @param mixed $value Value to check.
     * @return bool
     */
    public static function is_valid( $value ): bool {
        if ( ! is_string( $value ) || '' === $value ) {
            return false;
        }
        return (bool) preg_match( self::CODE_PATTERN, strtolower( trim( $value ) ) );
    }

    /**
     * Validate that a language code is in the enabled languages list.
     *
     * @param string $code Language code to validate.
     * @return true|\WP_Error True if valid, WP_Error otherwise.
     */
    public static function validate_enabled( string $code ) {
        if ( ! self::is_valid( $code ) ) {
            return new \WP_Error(
                'boostbot_invalid_language',
                sprintf( 'Invalid language code: "%s". Must be a 2–5 letter ISO 639 code.', esc_html( $code ) ),
                array( 'status' => 400 )
            );
        }

        if ( class_exists( 'BoostBot_Language_Manager' ) ) {
            $enabled = BoostBot_Language_Manager::get_instance()->get_enabled_languages();
            if ( ! in_array( $code, $enabled, true ) ) {
                return new \WP_Error(
                    'boostbot_language_not_enabled',
                    sprintf(
                        'Language "%s" is not enabled. Enabled languages: %s',
                        esc_html( $code ),
                        implode( ', ', $enabled )
                    ),
                    array( 'status' => 400 )
                );
            }
        }

        return true;
    }

    /**
     * Clear the cached primary language (call after the setting is changed).
     */
    public static function clear_cache(): void {
        self::$primary_cache = null;
    }

    /**
     * Convert a WordPress locale (e.g. 'he_IL', 'en_US') to a 2-letter code.
     *
     * @param string $locale WordPress locale string.
     * @return string Two-letter language code.
     */
    public static function locale_to_code( string $locale ): string {
        $code = strtolower( substr( $locale, 0, 2 ) );
        return preg_match( self::CODE_PATTERN, $code ) ? $code : 'en';
    }
}
