<?php

if (!defined('ABSPATH')) {
    exit;
}
/**
 * GravityOps_Search_Builder
 *
 * Handles the Shortcode Builder and Saved Shortcodes admin tabs for GravityOps Search.
 */
class GravityOps_Search_Builder
{
    const NONCE = 'gos_builder';
    const META_KEY = 'gos_sb_presets';
    const OPTION_KEY = 'gos_saved_shortcodes';
    use \GOS\GravityOps\Core\Traits\SingletonTrait;
    /**
     * Registers wp_ajax_* hooks. Called from GravityOps_Search::init() so AJAX requests are handled.
     */
    public function register_ajax_hooks(): void
    {
        add_action('wp_ajax_gosb_list_forms', [$this, 'ajax_list_forms']);
        add_action('wp_ajax_gosb_get_form', [$this, 'ajax_get_form']);
        add_action('wp_ajax_gosb_test_run', [$this, 'ajax_test_run']);
        add_action('wp_ajax_gosb_save_preset', [$this, 'ajax_save_preset']);
        add_action('wp_ajax_gosb_get_presets', [$this, 'ajax_get_presets']);
        add_action('wp_ajax_gosb_delete_preset', [$this, 'ajax_delete_preset']);
        add_action('wp_ajax_gosb_save_shortcode', [$this, 'ajax_save_shortcode']);
        add_action('wp_ajax_gosb_delete_shortcode', [$this, 'ajax_delete_shortcode']);
    }
    /**
     * Registers admin-only hooks (asset enqueueing). Called from GravityOps_Search::init_admin().
     */
    public function register_admin_hooks(): void
    {
        add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
    }
    /**
     * Enqueues CSS and JS only on the gravityops_search admin page.
     */
    public function enqueue_assets(): void
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
        if ('gravityops_search' !== $page) {
            return;
        }
        $base = plugin_dir_url(__DIR__);
        wp_enqueue_style('gos-shortcode-builder', $base . 'assets/css/shortcode-builder.css', [], '1.0.0');
        wp_enqueue_script('gos-shortcode-builder', $base . 'assets/js/shortcode-builder.js', [], '1.0.0', true);
    }
    /**
     * Renders the Shortcode Builder tab content.
     */
    public function render(): void
    {
        if (!current_user_can($this->cap())) {
            return;
        }
        $ajax = admin_url('admin-ajax.php');
        $nonce = wp_create_nonce(self::NONCE);
        echo '<div id="gosb-app" data-ajax="' . esc_attr($ajax) . '" data-nonce="' . esc_attr($nonce) . '"></div>';
    }
    /**
     * Renders the Saved Shortcodes tab content.
     */
    public function render_saved(): void
    {
        if (!current_user_can($this->cap())) {
            return;
        }
        $saved = get_option(self::OPTION_KEY, []);
        $nonce = wp_create_nonce(self::NONCE);
        $builder_url = add_query_arg(['page' => 'gravityops_search', 'tab' => 'builder'], admin_url('admin.php'));
        if (empty($saved)) {
            echo '<p class="gosb-empty" style="text-align:left;">No saved shortcodes yet. Build one in the Shortcode Builder tab and click "Save Shortcode".</p>';
            return;
        }
        echo '<div class="gosb-saved-list">';
        foreach ($saved as $item) {
            if (!is_array($item) || empty($item['id'])) {
                continue;
            }
            $state_json = wp_json_encode($item['state'] ?? []);
            echo '<div class="gosb-saved-card">';
            echo '<div class="gosb-saved-card__header">';
            echo '<strong>' . esc_html($item['name'] ?? '') . '</strong>';
            if (!empty($item['saved_at'])) {
                echo '<span class="gosb-pill">' . esc_html(wp_date(get_option('date_format'), strtotime($item['saved_at']))) . '</span>';
            }
            echo '</div>';
            echo '<code class="gosb-saved-code" title="Click to copy">' . esc_html($item['shortcode'] ?? '') . '</code>';
            echo '<div class="gosb-saved-actions">';
            echo '<button class="gosb-btn" data-gosb-edit="' . esc_attr($state_json) . '" data-gosb-builder-url="' . esc_url($builder_url) . '">Edit in Builder</button>';
            echo '<button class="gosb-btn gosb-btn-danger gosb-delete-sc"' . ' data-id="' . esc_attr($item['id']) . '"' . ' data-nonce="' . esc_attr($nonce) . '"' . ' data-ajax="' . esc_attr(admin_url('admin-ajax.php')) . '"' . '>Delete</button>';
            echo '</div>';
            echo '</div>';
        }
        echo '</div>';
    }
    /**
     * AJAX: Returns list of active Gravity Forms.
     */
    public function ajax_list_forms(): void
    {
        $this->check_cap_and_nonce();
        if (!class_exists('GFAPI')) {
            wp_send_json_error('gf_not_active');
        }
        $forms = GFAPI::get_forms();
        $out = [];
        foreach ($forms as $f) {
            $out[] = ['id' => (int) $f['id'], 'title' => $f['title']];
        }
        wp_send_json_success($out);
    }
    /**
     * AJAX: Returns field schema for a single form.
     */
    public function ajax_get_form(): void
    {
        $this->check_cap_and_nonce();
        if (!class_exists('GFAPI')) {
            wp_send_json_error('gf_not_active');
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce already verified via check_ajax_referer() in check_cap_and_nonce().
        $form_id = isset($_POST['form_id']) ? (int) $_POST['form_id'] : 0;
        if ($form_id <= 0) {
            wp_send_json_error('bad_id');
        }
        $form = GFAPI::get_form($form_id);
        if (!$form) {
            wp_send_json_error('not_found');
        }
        $fields = [];
        foreach ((array) $form['fields'] as $f) {
            $arr = is_array($f) ? $f : (array) $f;
            $type = $arr['type'] ?? '';
            $input_typ = $arr['inputType'] ?? '';
            $label = $arr['label'] ?? $arr['adminLabel'] ?? '';
            $id = $arr['id'] ?? '';
            if (in_array($type, ['page', 'section', 'html', 'captcha'], true)) {
                continue;
            }
            $entry = ['id' => (string) $id, 'label' => $label, 'type' => $type, 'inputType' => $input_typ, 'choices' => [], 'inputs' => [], 'isMulti' => $this->is_multi_input_field($arr)];
            if (!empty($arr['choices']) && is_array($arr['choices'])) {
                foreach ($arr['choices'] as $c) {
                    $ca = is_array($c) ? $c : (array) $c;
                    $entry['choices'][] = ['text' => $ca['text'] ?? '', 'value' => $ca['value'] ?? ''];
                }
            }
            if (!empty($arr['inputs']) && is_array($arr['inputs'])) {
                foreach ($arr['inputs'] as $i) {
                    $ia = is_array($i) ? $i : (array) $i;
                    if (!empty($ia['isHidden'])) {
                        continue;
                    }
                    $entry['inputs'][] = ['id' => (string) ($ia['id'] ?? ''), 'label' => (string) ($ia['label'] ?? '')];
                }
            }
            $fields[] = $entry;
        }
        wp_send_json_success(['id' => (int) $form['id'], 'title' => $form['title'], 'fields' => $fields]);
    }
    /**
     * AJAX: Runs the built shortcode server-side and returns rendered HTML.
     */
    public function ajax_test_run(): void
    {
        $this->check_cap_and_nonce();
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing -- nonce verified above; shortcode brackets/quotes would be corrupted by text sanitization; validated by regex below and output via esc_html().
        $shortcode = isset($_POST['shortcode']) ? wp_unslash($_POST['shortcode']) : '';
        if (!preg_match('/^\s*\[gravops_search\b/i', $shortcode)) {
            wp_send_json_error('bad_shortcode');
        }
        $rendered = do_shortcode($shortcode);
        $allowed = wp_kses_allowed_html('post');
        $allowed['a'] = array_merge($allowed['a'] ?? [], ['href' => true, 'title' => true, 'target' => true, 'rel' => true, 'class' => true, 'id' => true, 'style' => true]);
        wp_send_json_success(['html' => wp_kses($rendered, $allowed)]);
    }
    /**
     * AJAX: Saves a named preset to per-user meta.
     */
    public function ajax_save_preset(): void
    {
        $this->check_cap_and_nonce();
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce already verified via check_ajax_referer() in check_cap_and_nonce().
        $name = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : '';
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing -- nonce verified above; JSON string would be corrupted by text sanitization; decoded and type-checked via json_decode below.
        $state = isset($_POST['state']) ? wp_unslash($_POST['state']) : '';
        if ('' === $name) {
            wp_send_json_error('name_required');
        }
        $decoded = json_decode($state, true);
        if (!is_array($decoded)) {
            wp_send_json_error('bad_state');
        }
        $user_id = get_current_user_id();
        $all = get_user_meta($user_id, self::META_KEY, true);
        if (!is_array($all)) {
            $all = [];
        }
        $all[$name] = $decoded;
        update_user_meta($user_id, self::META_KEY, $all);
        wp_send_json_success(array_keys($all));
    }
    /**
     * AJAX: Returns all presets for the current user.
     */
    public function ajax_get_presets(): void
    {
        $this->check_cap_and_nonce();
        $all = get_user_meta(get_current_user_id(), self::META_KEY, true);
        wp_send_json_success(is_array($all) ? $all : []);
    }
    /**
     * AJAX: Deletes a named preset from per-user meta.
     */
    public function ajax_delete_preset(): void
    {
        $this->check_cap_and_nonce();
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce already verified via check_ajax_referer() in check_cap_and_nonce().
        $name = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : '';
        $all = get_user_meta(get_current_user_id(), self::META_KEY, true);
        if (is_array($all) && isset($all[$name])) {
            unset($all[$name]);
            update_user_meta(get_current_user_id(), self::META_KEY, $all);
        }
        $all = is_array($all) ? $all : [];
        wp_send_json_success(array_keys($all));
    }
    /**
     * AJAX: Saves a named shortcode to the site-wide option.
     */
    public function ajax_save_shortcode(): void
    {
        $this->check_cap_and_nonce();
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce already verified via check_ajax_referer() in check_cap_and_nonce().
        $name = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : '';
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing -- nonce verified above; shortcode brackets/quotes would be corrupted by text sanitization; validated by regex below and output via esc_html().
        $shortcode = isset($_POST['shortcode']) ? wp_unslash($_POST['shortcode']) : '';
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing -- nonce verified above; JSON string would be corrupted by text sanitization; decoded and type-checked via json_decode below.
        $state = isset($_POST['state']) ? wp_unslash($_POST['state']) : '';
        if ('' === $name) {
            wp_send_json_error('name_required');
        }
        if (!preg_match('/^\s*\[gravops_search\b/i', $shortcode)) {
            wp_send_json_error('bad_shortcode');
        }
        $decoded = json_decode($state, true);
        if (!is_array($decoded)) {
            wp_send_json_error('bad_state');
        }
        $all = get_option(self::OPTION_KEY, []);
        $all[] = ['id' => uniqid('sc_', true), 'name' => $name, 'shortcode' => $shortcode, 'state' => $decoded, 'saved_at' => gmdate('c'), 'saved_by' => get_current_user_id()];
        update_option(self::OPTION_KEY, $all, false);
        wp_send_json_success();
    }
    /**
     * AJAX: Removes one saved shortcode by ID from the site-wide option.
     */
    public function ajax_delete_shortcode(): void
    {
        $this->check_cap_and_nonce();
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce already verified via check_ajax_referer() in check_cap_and_nonce().
        $id = isset($_POST['id']) ? sanitize_text_field(wp_unslash($_POST['id'])) : '';
        $all = get_option(self::OPTION_KEY, []);
        $all = array_values(array_filter($all, fn($item) => ($item['id'] ?? '') !== $id));
        update_option(self::OPTION_KEY, $all, false);
        wp_send_json_success();
    }
    /**
     * Returns the appropriate capability for this feature.
     */
    private function cap(): string
    {
        return current_user_can('gravityforms_edit_forms') ? 'gravityforms_edit_forms' : 'manage_options';
    }
    /**
     * Verifies nonce and capability; sends JSON error and exits on failure.
     */
    private function check_cap_and_nonce(): void
    {
        check_ajax_referer(self::NONCE, 'nonce');
        if (!current_user_can($this->cap())) {
            wp_send_json_error('forbidden', 403);
        }
    }
    /**
     * Returns true when the field type concatenates multiple sub-inputs.
     *
     * @param array $field Gravity Forms field array.
     */
    private function is_multi_input_field(array $field): bool
    {
        $type = $field['type'] ?? '';
        $inp_type = $field['inputType'] ?? '';
        return 'name' === $type || 'address' === $type || 'checkbox' === $type || ('image_choice' === $type || 'multi_choice' === $type) && 'checkbox' === $inp_type;
    }
}