<?php
/**
 * Plugin Name: Lang Forge
 * Plugin URI: https://langforgewp.com
 * Description: Multilingual plugin for WordPress — language routing (4 URL formats), manual translation, language switcher, string translation, hreflang SEO, Glossary, WooCommerce, unlimited languages, REST/WP-CLI, WPML compatibility, and one-click migration. AI translation available via the Lang Forge Pro add-on.
 * Version: 1.0.15
 * Author: Avakode
 * Author URI: https://avakode.com
 * Text Domain: langforge
 * Domain Path: /languages
 * Requires at least: 6.5
 * Requires PHP: 7.4
 * License: GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 */

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

define('LF_VERSION', '1.0.15');
// Schema version is independent from plugin version. Bump only when one of
// the lf_* tables changes shape; LF_Schema::maybe_upgrade compares this
// against the `lf_db_version` option on every plugins_loaded.
define('LF_DB_VERSION', '1.0.1');
define('LF_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('LF_PLUGIN_URL', plugin_dir_url(__FILE__));
define('LF_PLUGIN_BASENAME', plugin_basename(__FILE__));
// Shared ed25519 public key for verifying signed auto-update packages (#8/M6).
// Same value across all Forge plugins → guard against double-define when several
// are active. Verification stays inert until a release manifest carries a signature.
if (!defined('FORGE_UPDATE_PUBKEY')) define('FORGE_UPDATE_PUBKEY', '54EEDBTTEytITaNTDKTcP+ugpv9JFBOLAJRJMeLGaQo=');

/**
 * Safe replacement for maybe_unserialize: mirrors its semantics
 * (non-serialized values pass through) but never instantiates objects,
 * blocking PHP object-injection via unserialize gadget chains. Shared
 * across all Forge plugins behind a function_exists guard.
 */
if (!function_exists('forge_safe_unserialize')) {
    function forge_safe_unserialize($value) {
        if (!is_string($value)) {
            return $value;
        }
        return is_serialized($value)
            ? unserialize($value, ['allowed_classes' => false])
            : $value;
    }
}

// Autoload classes
spl_autoload_register(function ($class) {
    $prefix = 'LF_';
    if (strpos($class, $prefix) !== 0) {
        return;
    }
    
    $class_name = str_replace($prefix, '', $class);
    $class_name = strtolower(str_replace('_', '-', $class_name));
    
    // Check includes directory first
    $file = LF_PLUGIN_DIR . 'includes/class-' . $class_name . '.php';
    
    if (file_exists($file)) {
        require_once $file;
        return;
    }
    
    // Check admin directory
    $admin_file = LF_PLUGIN_DIR . 'admin/class-' . $class_name . '.php';
    if (file_exists($admin_file)) {
        require_once $admin_file;
    }
});


// Pro Bridge — the Free side of the Free/Pro extension contract.
// Loaded before any other class so License + everything downstream can
// consult it. See SPLIT_PLAN.md §5.3.

require_once LF_PLUGIN_DIR . 'includes/class-pro-bridge.php';
LF_Pro_Bridge::instance();

// FC-63: shared license resolver — single source of truth for licensing UI +
// gating. Loaded right after the Pro bridge so License + every downstream
// surface (dashboard, credits widget, License page) consult one object. First
// Forge plugin to load wins via the class_exists wrap inside the file.
if (!class_exists('Forge_License_Resolver')) {
    require_once LF_PLUGIN_DIR . 'includes/class-forge-license-resolver.php';
}

// Load Forge Queue (shared across Forge Suite — first plugin to load wins)
if (!class_exists('Forge_Queue')) {
    require_once __DIR__ . '/includes/class-forge-queue.php';
}

// Shared transactional email renderer (first Forge plugin to load wins).
if (!class_exists('Forge_Email_Template')) {
    require_once __DIR__ . '/includes/class-forge-email.php';
}

// Shared SSRF-safe wp_remote_* wrapper (first Forge plugin to load wins).
if (!class_exists('Forge_Net')) {
    require_once __DIR__ . '/includes/class-forge-net.php';
}

/**
 * One-time migration: consolidate LangForge's license data onto the native
 * `lf_license_data` option.
 *
 * LangForge predates the `<slug>_license_data` convention — its runtime client
 * (LF_Forge_API) and LF_License read/write `lf_license_data`. The shared
 * cross-plugin glue (Forge_Connect SSO, credits widget, dashboard
 * health/activation) historically used the directory-slug name
 * `langforge_license_data`, so an Avakode connect saved the license key +
 * site_token where the runtime never looked and the two stores diverged. Those
 * maps now all point at lf_license_data; this merges anything left in the old
 * option into the native one (preserving a pre-existing trial_token), then
 * deletes the orphan. Guarded so it runs at most once.
 */
function lf_migrate_license_option() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    if (get_option('lf_license_option_unified')) {
        return;
    }
    $legacy = get_option('langforge_license_data', null);
    if (is_array($legacy) && !empty($legacy)) {
        $native = get_option('lf_license_data', []);
        if (!is_array($native)) {
            $native = [];
        }
        // Connect/runtime fields come from the legacy store; trial_* fields
        // (owned by LF_Forge_API in lf_license_data) are preserved by only
        // filling fields the native store does not already have.
        foreach ([
            'key', 'site_token', 'site_token_refreshed_at', 'connected_at',
            'source', 'site_url', 'bundle_licenses',
        ] as $field) {
            if (isset($legacy[$field]) && $legacy[$field] !== '' && empty($native[$field])) {
                $native[$field] = $legacy[$field];
            }
        }
        update_option('lf_license_data', $native, false);
        delete_option('langforge_license_data');
    }
    update_option('lf_license_option_unified', 1, false);
}

// Initialize plugin
function lf_init() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    // Load text domain
    load_plugin_textdomain('langforge', false, dirname(LF_PLUGIN_BASENAME) . '/languages'); // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- needed for bundled .mo on the Avakode bridge/Pro channel
    // Shared Forge UI (class-forge-*.php) uses the 'forge' textdomain — alias it
    // to this plugin's own compiled catalog so the shared dashboard/credits/
    // connect/feedback/updater strings translate (they're extracted into this
    // plugin's .po/.mo too). Without this, 'forge' loads nowhere → English-only.
    $lf_forge_mo = LF_PLUGIN_DIR . 'languages/langforge-' . determine_locale() . '.mo';
    if (is_readable($lf_forge_mo)) { load_textdomain('forge', $lf_forge_mo); }

    // Schema migrations — runs only when stored `lf_db_version` differs from
    // the LF_DB_VERSION constant. Cold installs hit this on the very first
    // page load after activation; warm updates from Freemius / wp.org get
    // their schema bump here (the activation hook itself doesn't fire on
    // updates, so a separate path is mandatory).
    require_once LF_PLUGIN_DIR . 'includes/class-schema.php';
    LF_Schema::maybe_upgrade();

    // One-time: consolidate the license store onto lf_license_data (see
    // lf_migrate_license_option). Runs before LF_License / LF_Forge_API read
    // the option so a freshly-migrated key is visible on the same request.
    lf_migrate_license_option();

    // Initialize license (must be first — other classes check license state)
    LF_License::instance();

    // Initialize core
    LF_Core::instance();

    // Initialize Free components.
    // Pro killer features (Translation Memory, Visual Editor, Frontend Editor,
    // Analytics, Import/Export, Bulk Actions, Translation QA, Content Diff,
    // Live Preview, Media/Comment Translation, Roles, Integrations-Extra,
    // LearnDash mapper) live in langforge-pro and are booted via
    // LF_Pro_Loader::boot. See SPLIT_PLAN.md §5.3.
    // 2026-05-29 Free/Pro rebalance: Glossary + WooCommerce moved into the
    // Free core (pure-local, free in Weglot / TranslatePress). They self-gate
    // via LF_License::can — now in FREE_FEATURES — and the glossary table is
    // created by LF_Schema::run_migrations (class_exists check now true).
    LF_Post_Translation::instance();
    LF_ACF_Integration::instance();
    LF_String_Translation::instance();
    LF_Menu_Translation::instance();
    LF_Media_Sync::instance();
    LF_Language_Switcher::instance();
    LF_SEO::instance();
    LF_Taxonomy_Translation::instance();
    LF_Rest_Api::instance();
    LF_Elementor_Integration::instance();
    LF_Divi_Integration::instance();

    // Initialize Forge Queue singleton. Pro registers the translate_post
    // handler when LF_Pro_Loader::boot runs.
    Forge_Queue::instance();

    LF_Sitemap::instance();

    // 2026-05-29 Free/Pro rebalance — Glossary + WooCommerce are Free now.
    if (class_exists('LF_Glossary'))    { LF_Glossary::instance(); }
    if (class_exists('LF_WooCommerce')) { LF_WooCommerce::instance(); }

    // v7 (2026-05-05): explicitly instantiate LF_Upgrade_Prompt so
    // its `__construct` runs and registers all the admin-side hooks
    // (admin_body_class for the `lf-admin-page` body marker,
    // admin_print_styles for the notice-hide CSS, admin_print_footer_scripts
    // for the JS DOM walker, current_screen + admin_notices ob-buffer).
    // Without this call, the singleton was only created late (on demand,
    // inside `LF_Upgrade_Prompt::locked_page`), AFTER admin-header.php
    // had already fired admin_notices — so every prior fix layer (v3 callback
    // strip, v4 ob-buffer, v5 CSS hide, v6 JS walker) silently never ran on
    // any LangForge admin page. That's why Sergei kept reporting "still
    // visible in runtime" through five retest cycles. Two-minute fix; one
    // line of code; closes the entire layered defense chain in one shot.
    if (class_exists('LF_Upgrade_Prompt')) { LF_Upgrade_Prompt::instance(); }

    // Cross-plugin integrations (loaded after String Translation so the
    // listener can rely on LF_String_Translation::instance). Form Forge
    // listener auto-registers form labels/options/buttons for translation.
    if (file_exists(LF_PLUGIN_DIR . 'includes/class-formforge-integration.php')) {
        require_once LF_PLUGIN_DIR . 'includes/class-formforge-integration.php';
        LF_FormForge_Integration::instance();
    }

    // WP-CLI commands
    if (defined('WP_CLI') && WP_CLI) {
        require_once LF_PLUGIN_DIR . 'includes/class-cli.php';
        WP_CLI::add_command('langforge', 'LF_CLI');
    }
    
    // Admin
    if (is_admin()) {
        // Pro-side updater + Connect + Credits widget + Telemetry are loaded
        // by LF_Pro_Loader::boot (langforge-pro) per SPLIT_PLAN §5.4 —
        // wp.org forbids external update channels and license-bearing telemetry
        // in Free packages.
        $lf_dashboard_file = LF_PLUGIN_DIR . 'includes/class-forge-dashboard.php';
        if (is_readable($lf_dashboard_file)) {
            require_once $lf_dashboard_file;
            if (class_exists('Forge_Suite_Dashboard')) {
                Forge_Suite_Dashboard::instance();
            }
        }
        require_once LF_PLUGIN_DIR . 'includes/class-forge-docs-link.php';
        $lf_feedback_file = LF_PLUGIN_DIR . 'includes/class-forge-feedback-widget.php';
        if (is_readable($lf_feedback_file)) {
            require_once $lf_feedback_file;
        }
        LF_Admin::instance();
        LF_License_Admin::instance();
        require_once LF_PLUGIN_DIR . 'includes/class-migration.php';
        LF_Migration::instance();
    }
}
add_action('plugins_loaded', 'lf_init', 5);

// Activation hook
register_activation_hook(__FILE__, function() {
    require_once LF_PLUGIN_DIR . 'includes/class-schema.php';

    // Single source of truth for schema creation. LF_Schema::run_migrations
    // is the same routine maybe_upgrade calls on plugins_loaded, so cold
    // activation and warm version bump share one code path. Running it here
    // (rather than calling maybe_upgrade) skips the stored-version check and
    // re-asserts the schema, which is what activation should always do.
    LF_Schema::run_migrations();
    update_option(LF_Schema::OPTION_KEY, LF_DB_VERSION, false);

    // LF_Roles + Forge_Telemetry live in langforge-pro after the split. On
    // Pro-active sites the activation hook fires for Pro separately and Pro
    // handles its own role registration / telemetry there. On Free-only sites
    // these calls are simply skipped — no-op.
    if (class_exists('LF_Roles')) {
        LF_Roles::register_roles();
    }

    if (class_exists('Forge_Telemetry')) {
        $prev = (string) get_option('lf_previous_version', '');
        Forge_Telemetry::track('plugin.activated', [
            'first_install'    => $prev === '',
            'previous_version' => $prev,
        ]);
        update_option('lf_previous_version', LF_VERSION, false);
    }
});

// Deactivation hook
register_deactivation_hook(__FILE__, function() {
    if (class_exists('LF_Roles')) {
        LF_Roles::remove_roles();
    }

    if (class_exists('Forge_Telemetry')) {
        $installed_at = (int) get_option('lf_installed_at', time());
        Forge_Telemetry::track('plugin.deactivated', [
            'uptime_days' => max(0, (int) floor((time() - $installed_at) / DAY_IN_SECONDS)),
        ]);
        // Flush synchronously so the event makes it out before the plugin
        // stops running.
        Forge_Telemetry::flush();
    }
});

// First-install timestamp — used for plugin.deactivated uptime.
add_action('admin_init', function() {
    if (!get_option('lf_installed_at')) {
        update_option('lf_installed_at', time(), false);
    }
});

// WPML compatibility layer - provide same hooks/functions
function lf_wpml_compatibility() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional ACF/WPML/core compatibility
    // icl_language_selector action for theme compatibility
    if (!has_action('icl_language_selector')) {
        add_action('icl_language_selector', function() {
            LF_Language_Switcher::instance()->render();
        });
    }

    // don't declare the WPML shim functions if the real WPML plugin
    // is active OR is being activated in the current request. WPML's
    // template-functions.php declares icl_get_languages, icl_object_id,
    // icl_get_current_language, icl_get_default_language unconditionally
    // (no function_exists guard). If LangForge declared them first, WPML's
    // include fatals with "Cannot redeclare". Skipping the shim entirely
    // when WPML is in play lets WPML own the functions; LangForge's other
    // hooks (wpml_object_id filter, icl_language_selector action) still
    // run and hand off correctly. The function_exists guards remain as
    // belt-and-suspenders for race conditions during plugins_loaded.
    if (lf_wpml_real_plugin_in_play()) {
        return;
    }

    // WPML API functions
    if (!function_exists('icl_get_languages')) {
        function icl_get_languages($args = '') { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional ACF/WPML/core compatibility
            return LF_Core::instance()->get_languages_for_switcher($args);
        }
    }

    if (!function_exists('icl_object_id')) {
        function icl_object_id($element_id, $element_type = 'post', $return_original = false, $ulanguage_code = null) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional ACF/WPML/core compatibility
            return LF_Post_Translation::instance()->get_translation_id($element_id, $element_type, $ulanguage_code, $return_original);
        }
    }

    if (!function_exists('icl_get_current_language')) {
        function icl_get_current_language() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional ACF/WPML/core compatibility
            return LF_Core::instance()->get_current_language();
        }
    }

    if (!function_exists('icl_get_default_language')) {
        function icl_get_default_language() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional ACF/WPML/core compatibility
            return LF_Core::instance()->get_default_language();
        }
    }

    // WPML API filters — modern themes prefer apply_filters('wpml_object_id', $id, $type)
    // over the legacy icl_object_id function. Without these LangForge can't be a drop-in
    // WPML replacement: themes using `wpml_object_id` to translate page IDs render links
    // pointing to the original-language post regardless of the visitor's current language.
    add_filter('wpml_object_id', function ($element_id, $element_type = 'post', $return_original = false, $language_code = null) {
        if (!$language_code) {
            $language_code = LF_Core::instance()->get_current_language();
        }

        // Decide which translation source to consult based on element type.
        
        // - Taxonomy terms (`category`, `post_tag`, `tax_*`, any registered taxonomy):
        // prefer the legacy WPML table `wp_icl_translations`. LangForge's own
        // `get_translation_id` doesn't filter by element_type, so a stray row
        // with the same element_id under a different element_type (e.g. an
        // acf-field-group row carrying a numeric ID that happens to match a
        // term_id) returns the wrong translation. Filtering by element_type
        // via wp_icl_translations gives a clean lookup that matches WPML's
        // behavior even on partly-migrated sites.
        
        // - Posts / pages / custom post types: consult LangForge's native table
        // first. Fall back to the WPML legacy table only when LF has no row
        // (covers half-migrated sites).
        global $wpdb;
        $is_term_type = is_string($element_type) && (
            strpos($element_type, 'tax_') === 0
            || in_array($element_type, ['category', 'post_tag', 'nav_menu'], true)
            || taxonomy_exists($element_type)
        );

        $wpml_lookup = function ($id, $type) use ($wpdb, $language_code) {
            if (!$wpdb) return null;
            $icl = $wpdb->prefix . 'icl_translations';
            static $has_table = null;
            if ($has_table === null) {
                $has_table = $wpdb->get_var("SHOW TABLES LIKE '{$icl}'") === $icl; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; one-time/admin read
            }
            if (!$has_table) return null;
            $trid = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; one-time/admin read
                "SELECT trid FROM {$icl} WHERE element_id = %d AND element_type = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
                (int) $id, $type
            ));
            if (!$trid) return null;
            $translated = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; one-time/admin read
                "SELECT element_id FROM {$icl} WHERE trid = %d AND language_code = %s AND element_type = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
                (int) $trid, $language_code, $type
            ));
            return $translated ? (int) $translated : null;
        };

        if ($is_term_type) {
            $wpml_type = (strpos($element_type, 'tax_') === 0) ? $element_type : 'tax_' . $element_type;
            $found = $wpml_lookup($element_id, $wpml_type);
            if ($found !== null) return $found;
        } else {
            $tr = LF_Post_Translation::instance()->get_translation_id($element_id, $element_type, $language_code, false);
            if ($tr !== null) return $tr;
            // Post fallback to WPML legacy table for half-migrated sites.
            $wpml_type = (strpos($element_type, 'post_') === 0) ? $element_type : 'post_' . $element_type;
            $found = $wpml_lookup($element_id, $wpml_type);
            if ($found !== null) return $found;
        }

        return $return_original ? $element_id : null;
    }, 10, 4);

    add_filter('wpml_current_language', function ($current = null) {
        return LF_Core::instance()->get_current_language();
    });

    add_filter('wpml_default_language', function ($default = null) {
        return LF_Core::instance()->get_default_language();
    });

    add_filter('wpml_active_languages', function ($languages, $args = '') {
        return LF_Core::instance()->get_languages_for_switcher($args);
    }, 10, 2);

    // Resolve the language a given post belongs to. WPML's contract: returns an
    // associative array { language_code, locale, text_direction, display_name, ... }
    // or false when the post isn't tracked. We hand back the minimum shape that
    // theme code typically reads (language_code) plus a few common keys.
    add_filter('wpml_post_language_details', function ($details, $post_id = 0) {
        $code = LF_Post_Translation::instance()->get_post_language($post_id);
        if (!$code) return $details;
        $info = LF_Core::instance()->get_language_info($code);
        return [
            'language_code' => $code,
            'locale' => $info['locale'] ?? $code,
            'text_direction' => !empty($info['rtl']) ? 'rtl' : 'ltr',
            'display_name' => $info['display_name'] ?? ($info['name'] ?? $code),
            'native_name' => $info['native_name'] ?? $code,
            'different_language' => $code !== LF_Core::instance()->get_current_language(),
        ];
    }, 10, 2);
}
add_action('init', 'lf_wpml_compatibility', 1);

/**
 * Detect whether the real WPML (sitepress-multilingual-cms) is or is
 * about to be active in the current request. Used by the WPML
 * compatibility shim above to avoid declaring functions WPML's
 * template-functions.php declares unconditionally.
 *
 * - Already loaded: ICL_SITEPRESS_VERSION constant or SitePress class
 * are present after plugins_loaded.
 * - Active in registry: is_plugin_active reads wp_options.
 * - Being activated this request: WP's plugins.php POST carries
 * `action=activate&plugin=...` (single) or
 * `action=activate-selected&checked=...` (bulk). LangForge runs
 * on init priority 1, BEFORE WP's activation handler invokes the
 * plugin's main file via plugin_sandbox_scrape, so we have to
 * detect from the request.
 */
function lf_wpml_real_plugin_in_play() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    if (defined('ICL_SITEPRESS_VERSION')) return true;
    if (class_exists('SitePress', false)) return true;

    $wpml_main = 'sitepress-multilingual-cms/sitepress.php';

    // is_plugin_active is in wp-admin/includes/plugin.php — load it on
    // demand. plugins_loaded → init: in admin context plugin.php is
    // already loaded, but we guard anyway for cron / REST contexts.
    if (!function_exists('is_plugin_active')) {
        $admin_plugin = ABSPATH . 'wp-admin/includes/plugin.php';
        if (is_readable($admin_plugin)) require_once $admin_plugin;
    }
    if (function_exists('is_plugin_active') && is_plugin_active($wpml_main)) {
        return true;
    }

    // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only detection of an in-progress WPML activation request, no data mutation.
    if (empty($_REQUEST)) return false;
    $action = isset($_REQUEST['action']) ? sanitize_text_field(wp_unslash($_REQUEST['action'])) : '';
    if ($action === 'activate' || $action === 'activate-selected') {
        $plugin = isset($_REQUEST['plugin']) ? sanitize_text_field(wp_unslash($_REQUEST['plugin'])) : '';
        if ($plugin === $wpml_main) return true;
        $checked = isset($_REQUEST['checked']) && is_array($_REQUEST['checked'])
            ? array_map('sanitize_text_field', wp_unslash($_REQUEST['checked'])) : [];
        foreach ($checked as $c) {
            if ((string) $c === $wpml_main) return true;
        }
    }
    // phpcs:enable WordPress.Security.NonceVerification.Recommended
    return false;
}

// Global helper functions
function lf_get_current_language() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    return LF_Core::instance()->get_current_language();
}

function lf_get_default_language() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    return LF_Core::instance()->get_default_language();
}

function lf_get_languages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    return LF_Core::instance()->get_active_languages();
}

function lf_translate_string($string, $name, $domain = 'default') { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    return LF_String_Translation::instance()->translate($string, $name, $domain);
}

function lf_register_string($string, $name, $domain = 'default') { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    return LF_String_Translation::instance()->register_string($string, $name, $domain);
}

function lf_get_post_language($post_id) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    return LF_Post_Translation::instance()->get_post_language($post_id);
}

function lf_get_post_translations($post_id) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    return LF_Post_Translation::instance()->get_translations($post_id);
}

function lf_get_translation($post_id, $language) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    return LF_Post_Translation::instance()->get_translation($post_id, $language);
}

// Shortcode for string translation
add_shortcode('lf_string', function($atts, $content = '') {
    $atts = shortcode_atts([
        'name' => '',
        'domain' => 'default'
    ], $atts);
    
    return lf_translate_string($content, $atts['name'], $atts['domain']);
});

/**
 * Translate a string. Kept for backward compatibility.
 *
 * Strings are now auto-registered via the gettext filter when standard
 * __ / _e / esc_html__ are used with an allowed domain.
 * Prefer using standard WordPress i18n functions instead of lf__.
 *
 * @deprecated Use __($string, $domain) instead. Strings from allowed domains
 * are auto-registered by Lang Forge's gettext filter.
 *
 * @param string $string The string to translate.
 * @param string $domain Text domain (default: 'default').
 * @param string $source_lang Source language code (ignored, kept for compat).
 * @return string Translated string.
 */
function lf__($string, $domain = '', $source_lang = null) {
    if (empty($domain)) {
        $domain = 'default';
    }
    // Delegate to WordPress __ which fires the gettext filter.
    // Lang Forge's filter_gettext will handle translation + auto-registration.
    // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain -- Dynamic runtime string translation (core feature); not gettext-extractable by design.
    return __($string, $domain);
}

/**
 * Echo a translated string. Backward-compatible wrapper.
 *
 * @deprecated Use _e($string, $domain) instead.
 */
function lf_e($string, $domain = '', $source_lang = null) {
    echo esc_html(lf__($string, $domain, $source_lang));
}

/**
 * Language switcher
 */
/**
 * Language switcher output.
 * Accepts optional args: style, show_flags, show_names, show_current.
 * Defaults from LF Settings → Language Switcher options.
 *
 * @param array $args Optional overrides.
 * @return string HTML output.
 */
function lf_language_switcher($args = []) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional plugin-prefixed (lf_) public function
    // Merge with saved switcher settings (from Settings page or widget defaults)
    $defaults = [
        'style'        => get_option('lf_switcher_style', 'dropdown'),
        'show_flags'   => (bool) get_option('lf_switcher_show_flags', '1'),
        'show_names'   => (bool) get_option('lf_switcher_show_names', '1'),
        'show_current' => (bool) get_option('lf_switcher_show_current', '1'),
    ];
    $args = wp_parse_args($args, $defaults);

    ob_start();
    LF_Language_Switcher::instance()->render($args);
    return ob_get_clean();
}

/**
 * Hook for icl_language_selector action (WPML compatibility)
 */
add_action('icl_language_selector', function() {
    echo lf_language_switcher(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup
});

// Legacy backward compatibility aliases removed (were sml_* -> avl_*, now unified as lf_*)
