<?php
if (!defined('ABSPATH')) exit;

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

    private function __construct() {
        add_action('wp_ajax_formforge_save_form', [$this, 'ajax_save']);
        add_action('wp_ajax_formforge_preview_form', [$this, 'ajax_preview']);
        add_action('wp_ajax_formforge_delete_form', [$this, 'ajax_delete']);
    }

    public function create($data) {
        global $wpdb;
        $data = $this->prepare_payload_for_save($data, true);
        $fields = $data['fields'];
        $settings = $data['settings'];
        $conditional_logic = $data['conditional_logic'];
        $wpdb->insert($wpdb->prefix . 'formforge_forms', [ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
            'title' => sanitize_text_field($data['title'] ?? __('New Form', 'formforge')),
            'fields' => wp_json_encode($fields),
            'settings' => wp_json_encode($settings),
            'conditional_logic' => wp_json_encode($conditional_logic),
            'status' => 'publish',
        ]);
        $new_id = $wpdb->insert_id;
        // Cross-plugin integration: announce strings for translation. LangForge
        // (or any future plugin) listens on this action to register form
        // labels/placeholders/options in its String Translation table.
        $strings = $this->collect_translatable_strings($fields, $settings);
        do_action('formforge/strings_registered', $new_id, $strings);
        return $new_id;
    }

    public function update($id, $data) {
        global $wpdb;
        $data = $this->prepare_payload_for_save($data, false);
        $update = [];
        if (isset($data['title'])) $update['title'] = sanitize_text_field($data['title']);
        if (isset($data['fields'])) {
            $data['fields'] = $this->sanitize_fields_payload($data['fields']);
            $update['fields'] = wp_json_encode($data['fields']);
        }
        if (isset($data['settings'])) {
            $data['settings'] = $this->sanitize_settings_payload($data['settings']);
            $update['settings'] = wp_json_encode($data['settings']);
        }
        if (isset($data['conditional_logic'])) $update['conditional_logic'] = wp_json_encode($data['conditional_logic']);
        if (isset($data['status'])) $update['status'] = sanitize_text_field($data['status']);
        $result = $wpdb->update($wpdb->prefix . 'formforge_forms', $update, ['id' => (int) $id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.

        // Re-announce strings on every save so renamed labels / new options
        // make it into the translation queue without an explicit re-import.
        // LangForge's listener uses INSERT IGNORE on a (domain,name,language)
        // unique key, so duplicate registrations are no-ops.
        if (isset($data['fields']) || isset($data['settings'])) {
            $form = $this->get($id);
            $strings = $this->collect_translatable_strings(
                isset($data['fields']) ? $data['fields'] : ($form ? $form->fields : []),
                isset($data['settings']) ? $data['settings'] : ($form ? $form->settings : [])
            );
            do_action('formforge/strings_registered', (int) $id, $strings);
        }
        return $result;
    }

    public function delete($id) {
        global $wpdb;
        $wpdb->delete($wpdb->prefix . 'formforge_forms', ['id' => (int) $id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
        $wpdb->delete($wpdb->prefix . 'formforge_submissions', ['form_id' => (int) $id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
        $wpdb->delete($wpdb->prefix . 'formforge_analytics', ['form_id' => (int) $id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
        // Cross-plugin integration: invite listeners to clean up. LangForge
        // deletes the per-form string_domain so the String Translation page
        // doesn't accumulate stale rows for forms that no longer exist.
        do_action('formforge/strings_unregistered', (int) $id);
    }

    
    public function collect_translatable_strings($fields, $settings) {
        $strings = [];
        if (is_array($fields)) {
            foreach ($fields as $field) {
                $fid = (string) ($field['id'] ?? '');
                if ($fid === '') continue;
                if (!empty($field['label'])) {
                    $strings[] = ['key' => 'label', 'field_id' => $fid, 'value' => $field['label']];
                }
                if (!empty($field['placeholder'])) {
                    $strings[] = ['key' => 'placeholder', 'field_id' => $fid, 'value' => $field['placeholder']];
                }
                if (!empty($field['description'])) {
                    $strings[] = ['key' => 'description', 'field_id' => $fid, 'value' => $field['description']];
                }
                if (!empty($field['options']) && is_array($field['options'])) {
                    foreach ($field['options'] as $opt) {
                        if (is_array($opt)) {
                            $val = (string) ($opt['value'] ?? '');
                            $lbl = (string) ($opt['label'] ?? '');
                        } else {
                            // Primitive string option ("Business" with no slug
                            // distinction). Use the same string for both
                            // identity (value) and translation surface (label).
                            $val = (string) $opt;
                            $lbl = $val;
                        }
                        if ($lbl === '') continue;
                        // (retest 2026-04-29): emit the label even when
                        // it equals the slug. The AI Form Generator and many
                        
                        
                        // prior `$lbl !== $val` gate silently dropped every
                        // such option from String Translation.
                        $strings[] = [
                            'key' => 'option_label:' . ($val !== '' ? $val : $lbl),
                            'field_id' => $fid,
                            'value' => $lbl,
                        ];
                    }
                }
            }
        }
        if (is_array($settings)) {
            if (!empty($settings['submit_text'])) {
                $strings[] = ['key' => 'submit_text', 'field_id' => '', 'value' => $settings['submit_text']];
            }
            if (!empty($settings['success_message'])) {
                $strings[] = ['key' => 'success_message', 'field_id' => '', 'value' => $settings['success_message']];
            }
            if (!empty($settings['schedule_message'])) {
                $strings[] = ['key' => 'schedule_message', 'field_id' => '', 'value' => $settings['schedule_message']];
            }
        }
        return $strings;
    }

    public function get($id) {
        global $wpdb;
        $form = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}formforge_forms WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
        if ($form) {
            $form->fields = json_decode($form->fields, true) ?: [];
            $form->settings = json_decode($form->settings, true) ?: [];
            $form->conditional_logic = json_decode($form->conditional_logic, true) ?: [];
        }

        // Soft-limit on Free plan: hide forms past the limit from
        // direct-by-id lookups so the [formforge id="X"] shortcode,
        // REST endpoints, and submission handler cannot render them.
        // Admin edit page calls get_unfiltered to bypass this and
        // let the admin manage their own data.
        if ($form && class_exists('FORMFORGE_License') && !FORMFORGE_License::instance()->can('unlimited_forms')) {
            $effective = $this->get_all();  // already soft-limited
            $allowed   = array_column($effective, 'id');
            if (!in_array((int) $form->id, array_map('intval', $allowed), true)) {
                return null;
            }
        }
        return $form;
    }

    /**
 * Direct-by-id lookup bypassing Free soft-limit. Use only in admin
 * UI that needs to load any saved form for edit / delete (e.g. the
 * form editor screen handling a click on a locked row). Runtime
 * code that renders forms publicly must keep using get.
 */
    public function get_unfiltered($id) {
        global $wpdb;
        $form = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}formforge_forms WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
        if ($form) {
            $form->fields = json_decode($form->fields, true) ?: [];
            $form->settings = json_decode($form->settings, true) ?: [];
            $form->conditional_logic = json_decode($form->conditional_logic, true) ?: [];
        }
        return $form;
    }

    public function get_all($status = '') {
        $forms = $this->get_all_unfiltered($status);

        // Soft-limit on Free plan: runtime callers (REST list, shortcode
        // discovery, etc.) only see the first N forms by creation date.
        // Excess forms stay in the DB so a Pro upgrade restores them.
        // Wp.org compliant: never destroys user data on downgrade.
        // Admin list page calls get_all_unfiltered directly to render
        // the full set with a visual lock state on excess rows.
        if (class_exists('FORMFORGE_License') && !FORMFORGE_License::instance()->can('unlimited_forms')) {
            $limit = FORMFORGE_License::instance()->get_limit('forms');
            if (is_numeric($limit) && count($forms) > (int) $limit) {
                $forms = array_slice($forms, 0, (int) $limit);
            }
        }
        return $forms;
    }

    /**
 * Full set from DB, ignoring Free-plan soft-limit. Use this in
 * admin UI that needs to enumerate every saved form (e.g. the
 * Forms list page that renders excess forms with a "Pro only"
 * lock indicator). Runtime code should keep using get_all.
 */
    public function get_all_unfiltered($status = '') {
        global $wpdb;
        if ($status) {
            $forms = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
                "SELECT * FROM {$wpdb->prefix}formforge_forms WHERE status = %s ORDER BY created_at DESC, id DESC",
                sanitize_text_field($status)
            ));
        } else {
            $forms = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}formforge_forms ORDER BY created_at DESC, id DESC"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
        }
        foreach ($forms as &$f) {
            $f->fields = json_decode($f->fields, true) ?: [];
            $f->settings = json_decode($f->settings, true) ?: [];
        }
        return $forms;
    }

    public function default_settings() {
        return [
            'submit_text' => __('Submit', 'formforge'),
            'success_message' => __('Thank you! Your submission has been received.', 'formforge'),
            'submit_action' => 'message',
            'redirect_url' => '',
            'custom_js' => '',
            'notify_admin' => true,
            'notify_email' => get_option('admin_email'),
            'auto_reply' => false,
            'auto_reply_field' => '',
            'auto_reply_subject' => '',
            'auto_reply_message' => '',
            'webhook_url' => '',
            'slack_webhook_url' => '',
            'discord_webhook_url' => '',
            'telegram_enabled' => false,
            'telegram_bot_token' => '',
            'telegram_chat_id' => '',
            'post_submission_enabled' => false,
            'post_submission_type' => 'post',
            'post_submission_status' => 'draft',
            'post_title_field' => '',
            'post_content_field' => '',
            'mailchimp_enabled' => false,
            'mailchimp_list_id' => '',
            'mailchimp_email_field' => '',
            'mailchimp_field_mapping' => [],
            'gsheets_enabled' => false,
            'gsheets_spreadsheet_id' => '',
            'gsheets_sheet_name' => 'Sheet1',
            'hubspot_enabled' => false,
            'hubspot_create_deal' => false,
            'hubspot_pipeline' => '',
            'hubspot_stage' => '',
            'hubspot_field_mapping' => [],
            'create_user' => false,
            'user_role' => 'subscriber',
            'user_email_field' => '',
            'user_name_field' => '',
            'css_class' => '',
            'multi_step' => false,
            'conversational' => false,
            'track_abandonment' => true,
        ];
    }

    private function sanitize_fields_payload($fields) {
        if (!is_array($fields)) return [];
        foreach ($fields as &$field) {
            if (!is_array($field)) {
                $field = [];
                continue;
            }
            if (isset($field['styles'])) {
                $field['styles'] = FORMFORGE_Field_Types::sanitize_styles_config($field['styles']);
            }
            $type = $field['type'] ?? '';
            if ($type === 'number' || $type === 'range') {
                foreach (['min', 'max', 'step'] as $key) {
                    if (!array_key_exists($key, $field)) continue;
                    $raw = trim((string) $field[$key]);
                    $field[$key] = ($raw !== '' && is_numeric($raw)) ? $raw + 0 : '';
                }
            }
            if ($type === 'range' && array_key_exists('default_value', $field)) {
                $raw = trim((string) $field['default_value']);
                $field['default_value'] = ($raw !== '' && is_numeric($raw)) ? (string) ($raw + 0) : '';
            }
            if ($type === 'color' && array_key_exists('default_value', $field)) {
                $raw = trim((string) $field['default_value']);
                $field['default_value'] = preg_match('/^#[0-9a-f]{6}$/i', $raw) ? $raw : '';
            }
        }
        unset($field);
        return $fields;
    }

    private function sanitize_settings_payload($settings) {
        if (!is_array($settings)) $settings = [];

        $action = sanitize_key($settings['submit_action'] ?? '');
        if (!in_array($action, ['message', 'redirect', 'custom_js'], true)) {
            $action = !empty($settings['redirect_url']) ? 'redirect' : 'message';
        }
        $settings['submit_action'] = $action;

        if (isset($settings['redirect_url'])) {
            $settings['redirect_url'] = esc_url_raw(trim((string) $settings['redirect_url']));
        }
        if (isset($settings['custom_js'])) {
            $settings['custom_js'] = sanitize_textarea_field((string) $settings['custom_js']);
        }
	        if (isset($settings['styles'])) {
	            $settings['styles'] = FORMFORGE_Field_Types::sanitize_styles_config($settings['styles']);
	        }
	        if (isset($settings['create_user'])) {
	            $settings['create_user'] = (bool) $settings['create_user'];
	        }
	        if (isset($settings['user_email_field'])) {
	            $settings['user_email_field'] = sanitize_key($settings['user_email_field']);
	        }
	        if (isset($settings['user_name_field'])) {
	            $settings['user_name_field'] = sanitize_key($settings['user_name_field']);
	        }
	        if (isset($settings['user_role'])) {
	            $role = sanitize_key($settings['user_role']);
	            $settings['user_role'] = in_array($role, ['subscriber', 'contributor', 'author'], true) ? $role : 'subscriber';
	        }

	        return $settings;
	    }

    private function prepare_payload_for_save($data, $include_defaults = false) {
        if (!is_array($data)) {
            $data = [];
        }

        if ($include_defaults || array_key_exists('fields', $data)) {
            $data['fields'] = $this->sanitize_fields_payload($data['fields'] ?? []);
        }

        if ($include_defaults || array_key_exists('settings', $data)) {
            $settings = $data['settings'] ?? ($include_defaults ? $this->default_settings() : []);
            $data['settings'] = $this->sanitize_settings_payload($settings);
        }

        if ($include_defaults || array_key_exists('conditional_logic', $data)) {
            $data['conditional_logic'] = isset($data['conditional_logic']) && is_array($data['conditional_logic'])
                ? $data['conditional_logic']
                : [];
        }

        return $this->apply_license_gates($data);
    }

    private function apply_license_gates($data) {
        $license = class_exists('FORMFORGE_License') ? FORMFORGE_License::instance() : null;

        if (array_key_exists('conditional_logic', $data)
            && !empty($data['conditional_logic'])
            && (!$license || !$license->can('conditional_logic'))) {
            $data['conditional_logic'] = [];
        }

        if (isset($data['settings']) && is_array($data['settings'])) {
            $setting_features = [
                'multi_step'      => 'multi_step',
                'conversational'  => 'conversational',
                'create_user'     => 'user_registration',
            ];
            foreach ($setting_features as $setting_key => $feature) {
                if (!empty($data['settings'][$setting_key]) && (!$license || !$license->can($feature))) {
                    $data['settings'][$setting_key] = false;
                }
            }
        }

        if (isset($data['fields']) && is_array($data['fields'])) {
            $types = FORMFORGE_Field_Types::get_types();
            foreach ($data['fields'] as &$field) {
                if (!is_array($field)) {
                    continue;
                }
                $type = sanitize_key((string) ($field['type'] ?? ''));
                if ($type === '' || FORMFORGE_Field_Types::field_type_available($type)) {
                    continue;
                }

                $fallback_label = isset($types[$type]['label']) ? $types[$type]['label'] : __('Field', 'formforge');
                $label = trim((string) ($field['label'] ?? ''));
                $field['type'] = 'text';
                $field['label'] = ($label !== '' ? $label : $fallback_label) . ' (' . __('Requires Pro', 'formforge') . ')';
            }
            unset($field);
        }

        return $data;
    }

    public function ajax_save() {
        check_ajax_referer('formforge_nonce', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();

        $id = intval($_POST['form_id'] ?? 0);
        $data = [
            'title' => sanitize_text_field(wp_unslash($_POST['title'] ?? '')),
            'fields' => json_decode(wp_unslash($_POST['fields'] ?? '[]'), true), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON unslashed then decoded + sanitized per-field in prepare_payload_for_save.
            'settings' => json_decode(wp_unslash($_POST['settings'] ?? '{}'), true), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON unslashed then decoded + sanitized per-field in prepare_payload_for_save.
            'conditional_logic' => json_decode(wp_unslash($_POST['conditional_logic'] ?? '[]'), true), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON unslashed then decoded + sanitized per-field in prepare_payload_for_save.
        ];

        $data = $this->prepare_payload_for_save($data, true);

        if ($id) {
            $this->update($id, $data);
        } else {
            // Check form limit for free users
            if (!FORMFORGE_License::instance()->can('unlimited_forms')) {
                $count = count($this->get_all());
                if (!FORMFORGE_License::instance()->check_limit('forms', $count)) {
                    wp_send_json_error(['message' => __('Form creation is unavailable for the current plan.', 'formforge')]);
                    return;
                }
            }
            $id = $this->create($data);
        }

        wp_send_json_success(['id' => $id, 'message' => __('Form saved', 'formforge')]);
    }

    public function ajax_preview() {
        check_ajax_referer('formforge_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Permission denied.', 'formforge')], 403);
        }

        $fields = json_decode(wp_unslash($_POST['fields'] ?? '[]'), true); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON unslashed then decoded + sanitized per-field in sanitize_fields_payload.
        $settings = json_decode(wp_unslash($_POST['settings'] ?? '{}'), true); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON unslashed then decoded + sanitized per-field in sanitize_settings_payload.
        $conditional_logic = json_decode(wp_unslash($_POST['conditional_logic'] ?? '[]'), true); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON unslashed then decoded + sanitized per-field in apply_license_gates.

        $payload = [
            'title' => sanitize_text_field(wp_unslash($_POST['title'] ?? __('Preview', 'formforge'))),
            'fields' => $this->sanitize_fields_payload(is_array($fields) ? $fields : []),
            'settings' => $this->sanitize_settings_payload(is_array($settings) ? $settings : []),
            'conditional_logic' => is_array($conditional_logic) ? $conditional_logic : [],
        ];
        $payload = $this->apply_license_gates($payload);

        $html = FORMFORGE_Form_Renderer::instance()->render_preview($payload);
        wp_send_json_success(['html' => $html]);
    }

    public function ajax_delete() {
        check_ajax_referer('formforge_nonce', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();
        $this->delete(intval($_POST['form_id'] ?? 0));
        wp_send_json_success();
    }
}
