<?php
/**
 * Cross-plugin integration: LangForge × Form Forge string translation.
 *
 * Listens on the narrow integration hooks Form Forge emits:
 * - do_action('formforge/strings_registered', $form_id, $strings_array)
 * - do_action('formforge/strings_unregistered', $form_id)
 * - apply_filters('formforge_translate_string', $string, $key, $field_id, $form_id)
 *
 * Every form gets its own `string_domain` of the form `formforge:<form_id>`
 * so the String Translation admin page lists per-form strings together.
 *
 * Sites without LangForge keep the default behavior (filter returns the
 * original string, register/unregister actions are unhandled — no-op).
 *
 * Approach B (narrow): we do NOT add WPML's `icl_register_string` /
 * `icl_t` shims. Form Forge calls our filter directly. If a third plugin
 * needs the same integration, it can copy this pattern (one filter, two
 * actions, ~150 lines).
 *
 * @package LangForge
 */

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

if (false) {
    class LF_Core {}
    class LF_String_Translation {}
}

class LF_FormForge_Integration {

    private static $instance = null;

    public static function instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        // Register strings on form save (idempotent — safe to fire on every save).
        add_action('formforge/strings_registered', [$this, 'on_strings_registered'], 10, 2);
        // Clean up the per-form domain when a form is deleted.
        add_action('formforge/strings_unregistered', [$this, 'on_strings_unregistered'], 10, 1);
        // Translate strings at render time.
        add_filter('formforge_translate_string', [$this, 'translate'], 10, 4);
        // Tell Form Forge what language the visitor is on at render time so
        // the success_message round-trips through admin-ajax cleanly.
        add_filter('formforge_current_language', [$this, 'current_language_hint'], 10, 2);
        // Honor the language hint Form Forge ships in submit payloads.
        add_filter('formforge_translate_lang_override', [$this, 'submit_lang_override'], 10, 2);
    }

    /**
 * Emitted into a hidden field on the rendered form so the AJAX submit
 * handler knows which language the visitor saw the form in. AJAX
 * requests don't carry the URL-prefix or `?lang=` signal LF normally
 * uses, so without this hint the success_message would always come
 * back in the site default language.
 */
    public function current_language_hint($default, $form_id) {
        if (!class_exists('LF_Core')) return $default;
        $lang = LF_Core::instance()->get_current_language();
        return $lang ?: $default;
    }

    /**
 * Returns the language Form Forge passed in the submit payload. The
 * form-handler invokes this filter to override the AJAX-context
 * default language for the duration of a submit response.
 */
    public function submit_lang_override($lang, $form_id) {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified by Form Forge's submit handler before this filter fires; read-only language hint.
        if (!empty($_POST['formforge_lang']) && is_string($_POST['formforge_lang'])) {
            $candidate = sanitize_text_field(wp_unslash($_POST['formforge_lang'])); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified by Form Forge's submit handler before this filter fires; read-only language hint.
            if (class_exists('LF_Core')) {
                $active = LF_Core::instance()->get_active_languages();
                if (isset($active[$candidate])) {
                    return $candidate;
                }
            }
        }
        return $lang;
    }

    /**
 * Build the string_name for a given (key, field_id). Field-level keys
 * include the field id so labels for different fields don't collide.
 * Form-level keys (submit_text, success_message, schedule_message)
 * have no field_id, so they get a bare key.
 *
 * @param string $key
 * @param string $field_id
 * @return string
 */
    private function build_string_name($key, $field_id) {
        $key = (string) $key;
        $field_id = (string) $field_id;
        if ($field_id !== '') {
            return 'field_' . $field_id . '_' . $key;
        }
        return $key;
    }

    /**
 * Build the per-form string_domain. We deliberately keep `formforge:<id>`
 * narrow so a form-deletion cleanup just deletes that single domain.
 *
 * @param int $form_id
 * @return string
 */
    private function build_domain($form_id) {
        return 'formforge:' . (int) $form_id;
    }

    
    public function on_strings_registered($form_id, $strings) {
        if (!is_array($strings)) return;
        if (!class_exists('LF_String_Translation')) return;

        $st = LF_String_Translation::instance();
        $domain = $this->build_domain($form_id);
        $source_lang = class_exists('LF_Core')
            ? LF_Core::instance()->get_default_language()
            : null;

        // Build authoritative (name → value) map for the source language.
        $new_names = [];
        foreach ($strings as $entry) {
            if (!is_array($entry)) continue;
            $value = isset($entry['value']) ? (string) $entry['value'] : '';
            if ($value === '') continue;
            $name = $this->build_string_name(
                $entry['key'] ?? '',
                $entry['field_id'] ?? ''
            );
            if ($name === '' || $name === '_') continue;
            $new_names[$name] = $value;
        }

        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';

        // Prune rows that are no longer in the form's translatable set.
        if (empty($new_names)) {
            $wpdb->delete($table, ['string_domain' => $domain]); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
        } else {
            $placeholders = implode(',', array_fill(0, count($new_names), '%s'));
            $sql = "DELETE FROM $table WHERE string_domain = %s AND string_name NOT IN ($placeholders)"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- own table from $wpdb->prefix; %s placeholders prepared below
            $params = array_merge([$domain], array_keys($new_names));
            $wpdb->query($wpdb->prepare($sql, $params)); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; $sql prepared with %s placeholders
        }

        // Upsert source rows + ensure missing ones get inserted via
        // register_string (which guards table existence + activates).
        foreach ($new_names as $name => $value) {
            $existing_id = $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; direct query intended.
                "SELECT id FROM $table WHERE string_domain = %s AND string_name = %s AND string_language = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
                $domain, $name, (string) $source_lang
            ));
            if ($existing_id) {
                $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
                    $table,
                    ['string_value' => $value],
                    ['id' => (int) $existing_id]
                );
            } else {
                $st->register_string($value, $name, $domain, $source_lang);
            }
        }
    }

    /**
 * Listener for `formforge/strings_unregistered`. Removes every row in
 * the per-form domain so the String Translation page stays tidy.
 *
 * @param int $form_id
 */
    public function on_strings_unregistered($form_id) {
        if (!class_exists('LF_String_Translation')) return;
        $domain = $this->build_domain($form_id);
        LF_String_Translation::instance()->delete_domain($domain);
    }

    /**
 * Filter listener for `formforge_translate_string`. Returns the
 * translation for the current visitor language, or the original string
 * if no translation is registered.
 *
 * The lookup order is:
 * 1. If current_lang === source_lang → return original (cheapest).
 * 2. Look up `(domain, name, current_lang)` in `lf_strings`.
 * 3. Fallback to original.
 *
 * @param string $string
 * @param string $key
 * @param string $field_id
 * @param int $form_id
 * @return string
 */
    public function translate($string, $key, $field_id, $form_id) {
        if (!is_string($string) || $string === '') return $string;
        if (!class_exists('LF_Core') || !class_exists('LF_String_Translation')) {
            return $string;
        }

        $core = LF_Core::instance();
        $current_lang = $core->get_current_language();
        $default_lang = $core->get_default_language();

        // AJAX context: the form-handler echoes the render-time language back
        // in the submit payload. This filter lets the handler / integration
        // override the runtime current_language so success_message comes out
        // in the visitor's language, not the site default.
        $current_lang = apply_filters('formforge_translate_lang_override', $current_lang, $form_id); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional ACF/WPML/core compatibility

        if (!$current_lang || $current_lang === $default_lang) {
            // Optimization: when visitor is on the source-lang URL, no DB
            // lookup is needed. Saves ~6 queries on a 6-field form.
            return $string;
        }

        $domain = $this->build_domain((int) $form_id);
        $name = $this->build_string_name($key, $field_id);
        if ($name === '' || $name === '_') return $string;

        // Direct DB lookup keyed on our resolved $current_lang. We can't
        // delegate to LF_String_Translation::translate because it would
        // call LF_Core::get_current_language internally and miss the
        // AJAX-context override the form-handler ships. The cache is
        // private — fine, we write through a fresh prepared query.
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';
        $translation = $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; direct query intended.
            "SELECT string_value FROM $table WHERE string_domain = %s AND string_name = %s AND string_language = %s AND status = 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
            $domain, $name, $current_lang
        ));
        return ($translation !== null && $translation !== '') ? (string) $translation : $string;
    }
}
