<?php
/**
 * FormForge Migration — import forms from Contact Form 7, Gravity Forms, WPForms, Ninja Forms.
 * Works directly with the database — no competitor plugin code is loaded or required.
 *
 * What is migrated:
 * - Form title
 * - Form fields (type, label, required, placeholder, options)
 * - Form submissions/entries (where the source stores them)
 *
 * What is NOT migrated:
 * - Conditional logic (structure differs too much)
 * - Payment settings
 * - Third-party add-on integration settings
 */

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

class FORMFORGE_Migration {

    private static $instance = null;

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

    public function __construct() {
        if (!is_admin()) return;
        add_action('admin_menu', [$this, 'add_menu'], 20);
        add_action('wp_ajax_formforge_migration_run', [$this, 'ajax_run']);
    }

    public function add_menu(): void {
        add_submenu_page(
            'formforge',
            __('Migration', 'formforge'),
            __('Migration', 'formforge'),
            'manage_options',
            'formforge-migration',
            [$this, 'render_page']
        );
    }

    
    // Detection
    

    private function detect_sources(): array {
        global $wpdb;
        $sources = [];

        // Contact Form 7
        $count = (int) $wpdb->get_var( // 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 COUNT(*) FROM {$wpdb->posts}
             WHERE post_type = 'wpcf7_contact_form' AND post_status = 'publish'"
        );
        if ($count > 0) {
            $sources['cf7'] = [
                'name'    => 'Contact Form 7',
                'count'   => $count,
                'note'    => __('Migrates: form fields. Submissions not stored by CF7 by default.', 'formforge'),
                'entries' => 0,
            ];
        }

        // Gravity Forms
        $gf_table = $wpdb->prefix . 'gf_form';
        if ($wpdb->get_var("SHOW TABLES LIKE '{$gf_table}'") === $gf_table) { // 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
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from $wpdb->prefix, no user values; one-time/admin read
            $count = (int) $wpdb->get_var(
                "SELECT COUNT(*) FROM {$gf_table} WHERE is_active = 1 AND is_trash = 0"
            );
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $gf_entry_table = $wpdb->prefix . 'gf_entry';
            $entries = 0;
            if ($wpdb->get_var("SHOW TABLES LIKE '{$gf_entry_table}'") === $gf_entry_table) { // 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
                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from $wpdb->prefix, no user values; one-time/admin read
                $entries = (int) $wpdb->get_var(
                    "SELECT COUNT(*) FROM {$gf_entry_table} WHERE status = 'active'"
                );
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            }
            if ($count > 0) {
                $sources['gravityforms'] = [
                    'name'    => 'Gravity Forms',
                    'count'   => $count,
                    'note'    => sprintf(
                        /* translators: %d: number of entries */
                        __('Migrates: form fields + %d entries.', 'formforge'),
                        $entries
                    ),
                    'entries' => $entries,
                ];
            }
        }

        // WPForms
        $count = (int) $wpdb->get_var( // 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 COUNT(*) FROM {$wpdb->posts}
             WHERE post_type = 'wpforms' AND post_status = 'publish'"
        );
        if ($count > 0) {
            // WPForms PRO stores entries
            $wpf_entry_table = $wpdb->prefix . 'wpforms_entries';
            $entries = 0;
            if ($wpdb->get_var("SHOW TABLES LIKE '{$wpf_entry_table}'") === $wpf_entry_table) { // 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
                $entries = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpf_entry_table}"); // 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
            }
            $sources['wpforms'] = [
                'name'    => 'WPForms',
                'count'   => $count,
                'note'    => $entries > 0
                    /* translators: %d: number of entries */
                    ? sprintf(__('Migrates: form fields + %d entries.', 'formforge'), $entries)
                    : __('Migrates: form fields. Entries require WPForms Pro.', 'formforge'),
                'entries' => $entries,
            ];
        }

        // Ninja Forms v3
        $nf_table = $wpdb->prefix . 'nf3_forms';
        if ($wpdb->get_var("SHOW TABLES LIKE '{$nf_table}'") === $nf_table) { // 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
            $count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$nf_table}"); // 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 ($count > 0) {
                $nf_sub_table = $wpdb->prefix . 'nf3_subs';
                $entries = 0;
                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from $wpdb->prefix, no user values; one-time/admin read
                $entries += (int) $wpdb->get_var(
                    "SELECT COUNT(*)
                     FROM {$wpdb->posts} p
                     INNER JOIN {$wpdb->postmeta} m ON m.post_id = p.ID
                     WHERE p.post_type = 'nf_sub'
                       AND p.post_status = 'publish'
                       AND m.meta_key = '_form_id'
                       AND m.meta_value IN (SELECT CAST(id AS CHAR) FROM {$nf_table})"
                );
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
                if ($wpdb->get_var("SHOW TABLES LIKE '{$nf_sub_table}'") === $nf_sub_table) { // 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
                    $entries += (int) $wpdb->get_var("SELECT COUNT(*) FROM {$nf_sub_table}"); // 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
                }
                $sources['ninjaforms'] = [
                    'name'    => 'Ninja Forms',
                    'count'   => $count,
                    'note'    => sprintf(
                        /* translators: %d: number of submissions */
                        __('Migrates: form fields + %d submissions.', 'formforge'),
                        $entries
                    ),
                    'entries' => $entries,
                ];
            }
        }

        return $sources;
    }

    
    // Migration dispatcher
    

    private function migrate(string $source): array {
        switch ($source) {
            case 'cf7':          return $this->migrate_cf7();
            case 'gravityforms': return $this->migrate_gravityforms();
            case 'wpforms':      return $this->migrate_wpforms();
            case 'ninjaforms':   return $this->migrate_ninjaforms();
            default:             return ['error' => 'Unknown source'];
        }
    }

    
    // Contact Form 7
    

    private function migrate_cf7(): array {
        global $wpdb;
        $migrated = $skipped = $errors = 0;
        $log = [];

        $forms = $wpdb->get_results( // 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 ID, post_title, post_content FROM {$wpdb->posts}
             WHERE post_type = 'wpcf7_contact_form' AND post_status = 'publish'
             ORDER BY ID ASC"
        );

        foreach ($forms as $cf7) {
            // Parse CF7 form markup into fields
            $fields = $this->parse_cf7_form($cf7->post_content);

            if (empty($fields)) {
                $skipped++;
                $log[] = sprintf('⊘ #%d "%s" — no parseable fields', $cf7->ID, esc_html($cf7->post_title));
                continue;
            }

            $form_id = $this->insert_form($cf7->post_title, $fields, [
                'submit_text'      => __('Send', 'formforge'),
                'success_message'  => __('Thank you! Your submission has been received.', 'formforge'),
            ]);

            if ($form_id) {
                $migrated++;
                $log[] = sprintf('✓ #%d "%s" → Form ID %d (%d fields)',
                    $cf7->ID, esc_html($cf7->post_title), $form_id, count($fields));
            } else {
                $errors++;
                $log[] = sprintf('✗ #%d "%s" — DB insert failed', $cf7->ID, esc_html($cf7->post_title));
            }
        }

        return compact('migrated', 'skipped', 'errors', 'log');
    }

    /**
 * Parse CF7 shortcode markup into FormForge field array.
 * Handles: text, email, tel, number, date, textarea, select, checkbox, radio, file, submit.
 */
    private function parse_cf7_form(string $markup): array {
        $fields = [];
        $cf7_to_ff = [
            'text'       => 'text',
            'email'      => 'email',
            'tel'        => 'phone',
            'number'     => 'number',
            'date'       => 'date',
            'time'       => 'time',
            'textarea'   => 'textarea',
            'select'     => 'select',
            'checkbox'   => 'checkbox',
            'radio'      => 'radio',
            'file'       => 'file_upload',
            'url'        => 'text',
            'range'      => 'number',
            'acceptance' => 'checkbox',
        ];

        // Match all CF7 shortcodes: [type* name attr1 attr2 ...]
        preg_match_all('/\[([a-z]+)(\*)?\s+([a-zA-Z0-9_-]+)((?:[^"[\]]*(?:"[^"]*")?)*)\]/i', $markup, $matches, PREG_SET_ORDER);

        $index = 0;
        foreach ($matches as $m) {
            $cf7_type = strtolower($m[1]);
            $required = ($m[2] === '*');
            $name     = $m[3];
            $attrs    = $m[4];

            // Skip submit button and hidden fields
            if (in_array($cf7_type, ['submit', 'hidden', 'captchac', 'captchar', 'quiz', 'response'], true)) {
                continue;
            }

            $ff_type = $cf7_to_ff[$cf7_type] ?? 'text';

            // Extract placeholder
            $placeholder = '';
            if (preg_match('/placeholder\s+"([^"]+)"/i', $attrs, $pm)) {
                $placeholder = $pm[1];
            }

            // Human-readable label from field name
            $label = ucwords(str_replace(['-', '_'], ' ', $name));

            // Extract options for select/checkbox/radio
            $options = [];
            preg_match_all('/"([^"]+)"/', $attrs, $opts);
            foreach ($opts[1] as $opt) {
                // Skip placeholder
                if ($opt === $placeholder) continue;
                // Skip CF7 attribute keywords
                if (in_array(strtolower($opt), ['first_as_label', 'use_label_element', 'free_text', 'exclusive', 'label_first'], true)) continue;
                $options[] = $opt;
            }

            $field = [
                'id'          => 'field_' . $index,
                'type'        => $ff_type,
                'label'       => $label,
                'required'    => $required,
                'placeholder' => $placeholder,
            ];

            if ($options && in_array($ff_type, ['select', 'checkbox', 'radio'], true)) {
                $field['options'] = $options;
            }

            if ($ff_type === 'file_upload') {
                $accept = $this->normalize_accept_extensions($this->extract_cf7_filetypes($attrs));
                if ($accept !== '') {
                    $field['accept'] = $accept;
                }
            }

            $fields[] = $field;
            $index++;
        }

        return $fields;
    }

    
    // Gravity Forms
    

    private function migrate_gravityforms(): array {
        global $wpdb;
        $migrated = $skipped = $errors = 0;
        $log = [];

        $gf_form_table = $wpdb->prefix . 'gf_form';
        $gf_meta_table = $wpdb->prefix . 'gf_form_meta';

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table names from $wpdb->prefix; one-time/admin read, no user values
        $forms = $wpdb->get_results(
            "SELECT f.id, f.title, m.display_meta, m.confirmations, m.notifications
             FROM {$gf_form_table} f
             LEFT JOIN {$gf_meta_table} m ON m.form_id = f.id
             WHERE f.is_active = 1 AND f.is_trash = 0
             ORDER BY f.id ASC"
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

        foreach ($forms as $gf) {
            $meta = json_decode($gf->display_meta, true);
            if (!$meta || empty($meta['fields'])) {
                $skipped++;
                $log[] = sprintf('⊘ GF #%d "%s" — no field data', $gf->id, esc_html($gf->title));
                continue;
            }

            $fields = $this->parse_gf_fields($meta['fields']);
            $settings = $this->build_gf_form_settings(
                $meta,
                $gf->confirmations ?? '',
                $gf->notifications ?? '',
                $meta['fields'],
                $fields
            );

            $form_id = $this->insert_form($gf->title, $fields, $settings);

            if (!$form_id) {
                $errors++;
                $log[] = sprintf('✗ GF #%d "%s" — DB insert failed', $gf->id, esc_html($gf->title));
                continue;
            }

            $migrated++;
            $entry_count = $this->migrate_gf_entries((int)$gf->id, $form_id, $meta['fields']);
            $log[] = sprintf('✓ GF #%d "%s" → Form ID %d (%d fields, %d entries)',
                $gf->id, esc_html($gf->title), $form_id, count($fields), $entry_count);
        }

        return compact('migrated', 'skipped', 'errors', 'log');
    }

    private function parse_gf_fields(array $gf_fields): array {
        $gf_to_ff = [
            'text'         => 'text',
            'textarea'     => 'textarea',
            'email'        => 'email',
            'phone'        => 'phone',
            'number'       => 'number',
            'date'         => 'date',
            'time'         => 'time',
            'select'       => 'select',
            'multiselect'  => 'select',
            'checkbox'     => 'checkbox',
            'radio'        => 'radio',
            'fileupload'   => 'file_upload',
            'name'         => 'text',
            'address'      => 'text',
            'website'      => 'text',
            'list'         => 'textarea',
            'hidden'       => null,        // skip
            'html'         => null,        // skip
            'section'      => 'heading',
            'page'         => null,        // skip
            'captcha'      => null,        // skip
        ];

        $fields = [];
        $index  = 0;

        foreach ($gf_fields as $gf_field) {
            $type = strtolower($gf_field['type'] ?? 'text');

            if (!array_key_exists($type, $gf_to_ff) || $gf_to_ff[$type] === null) {
                continue;
            }

            $ff_type = $gf_to_ff[$type];
            $label   = $gf_field['label'] ?? ucfirst($type);

            // For name fields, just use first/last as a single text field
            if ($type === 'name') {
                $label = $gf_field['label'] ?? __('Name', 'formforge');
            }

            $field = [
                'id'          => 'field_' . $index,
                'type'        => $ff_type,
                'label'       => $label,
                'required'    => !empty($gf_field['isRequired']),
                'placeholder' => $gf_field['placeholder'] ?? '',
            ];

            if (!empty($gf_field['description'])) {
                $field['description'] = wp_strip_all_tags((string) $gf_field['description']);
            }

            if (!empty($gf_field['cssClass'])) {
                $field['css_class'] = preg_replace('/[^A-Za-z0-9\-_ ]/', '', (string) $gf_field['cssClass']);
            }

            if (!empty($gf_field['adminLabel'])) {
                $field['admin_label'] = sanitize_text_field((string) $gf_field['adminLabel']);
            }

            // Options for select/checkbox/radio
            if (!empty($gf_field['choices']) && in_array($ff_type, ['select', 'checkbox', 'radio'], true)) {
                $field['options'] = array_column($gf_field['choices'], 'text');
            }

            if ($ff_type === 'file_upload') {
                $accept = $this->normalize_accept_extensions($gf_field['allowedExtensions'] ?? '');
                if ($accept !== '') {
                    $field['accept'] = $accept;
                }
            }

            $fields[] = $field;
            $index++;
        }

        return $fields;
    }

    private function migrate_gf_entries(int $gf_form_id, int $ff_form_id, array $gf_fields): int {
        global $wpdb;

        $gf_entry_table = $wpdb->prefix . 'gf_entry';
        $gf_meta_table  = $wpdb->prefix . 'gf_entry_meta';

        if ($wpdb->get_var("SHOW TABLES LIKE '{$gf_entry_table}'") !== $gf_entry_table) { // 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
            return 0;
        }

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from $wpdb->prefix, values are %-prepared; one-time/admin read
        $entries = $wpdb->get_results($wpdb->prepare(
            "SELECT id, ip, user_agent, created_by, date_created
             FROM {$gf_entry_table}
             WHERE form_id = %d AND status = 'active'
             ORDER BY id ASC",
            $gf_form_id
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

        // Build Gravity Forms field ID → migrated FormForge key map.
        // Compound GF inputs are stored as sub-input meta keys like `1.3`.
        $field_map = [];
        $index = 0;
        foreach ($gf_fields as $gf_field) {
            $type = strtolower($gf_field['type'] ?? 'text');
            if (!$this->is_supported_gf_field_type($type)) {
                continue;
            }
            $parent_id = (string) ($gf_field['id'] ?? '');
            if ($parent_id === '') {
                continue;
            }
            $field_map[$parent_id] = [
                'ff_id'  => 'field_' . $index,
                'label'  => $gf_field['label'] ?? '',
                'type'   => $type,
                'inputs' => $gf_field['inputs'] ?? [],
            ];
            $index++;
        }

        $count = 0;
        foreach ($entries as $entry) {
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from $wpdb->prefix, values are %-prepared; one-time/admin read
            $meta_rows = $wpdb->get_results($wpdb->prepare(
                "SELECT meta_key, meta_value FROM {$gf_meta_table}
                 WHERE entry_id = %d ORDER BY id ASC",
                $entry->id
            ));
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

            $data = [];
            $compound_values = [];
            foreach ($meta_rows as $row) {
                $key   = $row->meta_key;
                $value = trim((string)$row->meta_value);
                if ($value === '') continue;

                $parent_key = explode('.', (string) $key)[0];
                $mapped = $field_map[$parent_key] ?? null;
                if (!$mapped) {
                    continue;
                }

                if (in_array($mapped['type'], ['name', 'address'], true) && strpos((string) $key, '.') !== false) {
                    if (!isset($compound_values[$parent_key])) {
                        $compound_values[$parent_key] = [
                            'mapped' => $mapped,
                            'parts'  => [],
                        ];
                    }
                    $compound_values[$parent_key]['parts'][(string) $key] = $value;
                    continue;
                }

                if ($mapped['type'] === 'checkbox' && strpos((string) $key, '.') !== false) {
                    if (!isset($compound_values[$parent_key])) {
                        $compound_values[$parent_key] = [
                            'mapped' => $mapped,
                            'parts'  => [],
                        ];
                    }
                    $compound_values[$parent_key]['parts'][(string) $key] = $value;
                    continue;
                }

                $data[$mapped['ff_id']] = [
                    'label' => $mapped['label'],
                    'value' => $value,
                    'type'  => 'text',
                ];
            }

            foreach ($compound_values as $compound) {
                $mapped = $compound['mapped'];
                $value = $this->format_gf_compound_value($mapped, $compound['parts']);
                if ($value === '') {
                    continue;
                }
                $data[$mapped['ff_id']] = [
                    'label' => $mapped['label'],
                    'value' => $value,
                    'type'  => 'text',
                ];
            }

            $this->insert_submission($ff_form_id, $data, [
                'ip_address' => $entry->ip ?? '',
                'user_agent' => $entry->user_agent ?? '',
                'user_id'    => (int)($entry->created_by ?? 0),
                'created_at' => $entry->date_created ?? current_time('mysql'),
            ]);
            $count++;
        }

        return $count;
    }

    private function build_gf_form_settings(array $meta, $confirmations_raw, $notifications_raw, array $gf_fields, array $ff_fields): array {
        $settings = [
            'submit_text'     => $meta['button']['text'] ?? __('Submit', 'formforge'),
            'success_message' => __('Thank you! Your submission has been received.', 'formforge'),
        ];

        $confirmation = $this->pick_gf_default_confirmation($this->decode_gf_meta_blob($confirmations_raw));
        if ($confirmation) {
            $type = $confirmation['type'] ?? 'message';
            if ($type === 'message' && !empty($confirmation['message'])) {
                $settings['success_message'] = wp_strip_all_tags((string) $confirmation['message']);
            } elseif ($type === 'redirect' && !empty($confirmation['url'])) {
                $settings['submit_action'] = 'redirect';
                $settings['redirect_url'] = esc_url_raw((string) $confirmation['url']);
            } elseif ($type === 'page' && !empty($confirmation['pageId'])) {
                $url = get_permalink((int) $confirmation['pageId']);
                if ($url) {
                    $settings['submit_action'] = 'redirect';
                    $settings['redirect_url'] = esc_url_raw($url);
                }
            }
        }

        $gf_to_ff = $this->build_gf_to_ff_field_id_map($gf_fields, $ff_fields);
        $notifications = $this->decode_gf_meta_blob($notifications_raw);
        foreach ($notifications as $notification) {
            if (empty($notification['isActive']) || ($notification['event'] ?? 'form_submission') !== 'form_submission') {
                continue;
            }

            $to_type = $notification['toType'] ?? '';
            if ($to_type === 'email') {
                $email = $this->extract_first_email($notification['to'] ?? '');
                if ($email) {
                    $settings['notify_admin'] = true;
                    $settings['notify_email'] = $email;
                }
                continue;
            }

            if ($to_type === 'field') {
                $to_field = (string) ($notification['toField'] ?? '');
                if ($to_field !== '' && isset($gf_to_ff[$to_field])) {
                    $settings['auto_reply'] = true;
                    $settings['auto_reply_field'] = $gf_to_ff[$to_field];

                    if (!empty($notification['subject']) && strpos((string) $notification['subject'], '{') === false) {
                        $settings['auto_reply_subject'] = sanitize_text_field((string) $notification['subject']);
                    }
                    if (!empty($notification['message']) && strpos((string) $notification['message'], '{') === false) {
                        $settings['auto_reply_message'] = wp_strip_all_tags((string) $notification['message']);
                    }
                }
            }
        }

        return $settings;
    }

    private function decode_gf_meta_blob($raw): array {
        if (is_array($raw)) {
            return $raw;
        }

        $raw = trim((string) $raw);
        if ($raw === '') {
            return [];
        }

        $decoded = json_decode($raw, true);
        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
            return $decoded;
        }

        $unserialized = forge_safe_unserialize($raw);
        return is_array($unserialized) ? $unserialized : [];
    }

    private function pick_gf_default_confirmation(array $confirmations): array {
        foreach ($confirmations as $confirmation) {
            if (is_array($confirmation) && !empty($confirmation['isDefault'])) {
                return $confirmation;
            }
        }

        foreach ($confirmations as $confirmation) {
            if (is_array($confirmation)) {
                return $confirmation;
            }
        }

        return [];
    }

    private function build_gf_to_ff_field_id_map(array $gf_fields, array $ff_fields): array {
        $map = [];
        $index = 0;
        foreach ($gf_fields as $gf_field) {
            $type = strtolower($gf_field['type'] ?? 'text');
            if (!$this->is_supported_gf_field_type($type)) {
                continue;
            }
            $gf_id = (string) ($gf_field['id'] ?? '');
            if ($gf_id !== '' && isset($ff_fields[$index]['id'])) {
                $map[$gf_id] = $ff_fields[$index]['id'];
            }
            $index++;
        }
        return $map;
    }

    private function extract_first_email($raw): string {
        $raw = (string) $raw;
        if (strpos($raw, '{admin_email}') !== false || strpos($raw, '{wp:admin_email}') !== false || strpos($raw, '{system:admin_email}') !== false) {
            return sanitize_email(get_option('admin_email'));
        }

        $parts = preg_split('/[,;\s]+/', $raw);
        foreach ($parts as $part) {
            $email = sanitize_email(trim((string) $part));
            if ($email && is_email($email)) {
                return $email;
            }
        }

        return '';
    }

    
    // WPForms
    

    private function migrate_wpforms(): array {
        global $wpdb;
        $migrated = $skipped = $errors = 0;
        $log = [];

        $forms = $wpdb->get_results( // 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 ID, post_title, post_content FROM {$wpdb->posts}
             WHERE post_type = 'wpforms' AND post_status = 'publish'
             ORDER BY ID ASC"
        );

        foreach ($forms as $wpf) {
            $data = json_decode($wpf->post_content, true);
            if (!$data || empty($data['fields'])) {
                $skipped++;
                $log[] = sprintf('⊘ WPF #%d "%s" — no field data', $wpf->ID, esc_html($wpf->post_title));
                continue;
            }

            $fields      = $this->parse_wpforms_fields($data['fields']);
            $submit_text = $data['settings']['submit_text'] ?? __('Submit', 'formforge');

            $form_id = $this->insert_form($wpf->post_title, $fields, [
                'submit_text'     => $submit_text,
                'success_message' => $data['settings']['confirmation_message']
                    ?? __('Thank you! Your submission has been received.', 'formforge'),
            ]);

            if (!$form_id) {
                $errors++;
                $log[] = sprintf('✗ WPF #%d "%s" — DB insert failed', $wpf->ID, esc_html($wpf->post_title));
                continue;
            }

            $migrated++;
            $entry_count = $this->migrate_wpforms_entries((int)$wpf->ID, $form_id, $data['fields']);
            $log[] = sprintf('✓ WPF #%d "%s" → Form ID %d (%d fields, %d entries)',
                $wpf->ID, esc_html($wpf->post_title), $form_id, count($fields), $entry_count);
        }

        return compact('migrated', 'skipped', 'errors', 'log');
    }

    private function parse_wpforms_fields(array $wpf_fields): array {
        $wpf_to_ff = [
            'text'        => 'text',
            'textarea'    => 'textarea',
            'number'      => 'number',
            'number-slider' => 'number',
            'email'       => 'email',
            'phone'       => 'phone',
            'url'         => 'text',
            'date-time'   => 'date',
            'select'      => 'select',
            'checkbox'    => 'checkbox',
            'radio'       => 'radio',
            'file-upload' => 'file_upload',
            'name'        => 'text',
            'address'     => 'text',
            'rating'      => 'radio',
            'likert_scale' => 'radio',
            'pagebreak'   => null,
            'divider'     => null,
            'html'        => null,
            'content'     => 'heading',
            'hidden'      => null,
            'captcha'     => null,
        ];

        $fields = [];
        $index  = 0;

        foreach ($wpf_fields as $wpf_field) {
            $type = strtolower($wpf_field['type'] ?? 'text');
            if (!array_key_exists($type, $wpf_to_ff) || $wpf_to_ff[$type] === null) continue;

            $ff_type = $wpf_to_ff[$type];
            $label   = $wpf_field['label'] ?? ucfirst($type);

            $field = [
                'id'          => 'field_' . $index,
                'type'        => $ff_type,
                'label'       => $label,
                'required'    => !empty($wpf_field['required']),
                'placeholder' => $wpf_field['placeholder'] ?? '',
            ];

            // Options
            if (!empty($wpf_field['choices']) && in_array($ff_type, ['select', 'checkbox', 'radio'], true)) {
                $field['options'] = array_column($wpf_field['choices'], 'label');
            }

            if ($ff_type === 'file_upload') {
                $accept = $this->normalize_accept_extensions($wpf_field['extensions'] ?? '');
                if ($accept !== '') {
                    $field['accept'] = $accept;
                }
            }

            $fields[] = $field;
            $index++;
        }

        return $fields;
    }

    private function migrate_wpforms_entries(int $wpf_post_id, int $ff_form_id, array $wpf_fields): int {
        global $wpdb;

        $table = $wpdb->prefix . 'wpforms_entries';
        if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") !== $table) return 0; // 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

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from $wpdb->prefix, values are %-prepared; one-time/admin read
        $entries = $wpdb->get_results($wpdb->prepare(
            "SELECT entry_id, fields, ip_address, user_agent, user_id, date
             FROM {$table} WHERE form_id = %d ORDER BY entry_id ASC",
            $wpf_post_id
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

        // field ID → label map
        $field_map = [];
        foreach ($wpf_fields as $f) {
            $field_map[(string)$f['id']] = $f['label'] ?? '';
        }

        $count = 0;
        foreach ($entries as $entry) {
            $raw  = json_decode($entry->fields, true);
            if (!$raw) continue;

            $data = [];
            foreach ($raw as $fid => $val) {
                $label = $field_map[(string)$fid] ?? $fid;
                $value = is_array($val) ? implode(', ', $val) : (string)$val;
                if (empty(trim($value))) continue;
                $data[$fid] = ['label' => $label, 'value' => $value, 'type' => 'text'];
            }

            $this->insert_submission($ff_form_id, $data, [
                'ip_address' => $entry->ip_address ?? '',
                'user_agent' => $entry->user_agent ?? '',
                'user_id'    => (int)($entry->user_id ?? 0),
                'created_at' => $entry->date ?? current_time('mysql'),
            ]);
            $count++;
        }

        return $count;
    }

    
    // Ninja Forms v3
    

    private function migrate_ninjaforms(): array {
        global $wpdb;
        $migrated = $skipped = $errors = 0;
        $log = [];

        $nf_forms  = $wpdb->prefix . 'nf3_forms';
        $nf_fields = $wpdb->prefix . 'nf3_fields';
        $nf_fmeta  = $wpdb->prefix . 'nf3_field_meta';

        $forms = $wpdb->get_results("SELECT id, title FROM {$nf_forms} ORDER BY id ASC"); // 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

        foreach ($forms as $nf) {
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from $wpdb->prefix, values are %-prepared; one-time/admin read
            $nf_field_rows = $wpdb->get_results($wpdb->prepare(
                "SELECT id, label, `key`, type, parent_id, required FROM {$nf_fields}
                 WHERE parent_id = %d ORDER BY `order` ASC",
                $nf->id
            ));
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

            $fields = $this->parse_nf_fields($nf_field_rows, $this->get_nf_field_meta($nf_field_rows));

            if (empty($fields)) {
                $skipped++;
                $log[] = sprintf('⊘ NF #%d "%s" — no fields', $nf->id, esc_html($nf->title));
                continue;
            }

            $form_id = $this->insert_form($nf->title, $fields, [
                'submit_text'     => __('Submit', 'formforge'),
                'success_message' => __('Thank you! Your submission has been received.', 'formforge'),
            ]);

            if (!$form_id) {
                $errors++;
                $log[] = sprintf('✗ NF #%d "%s" — DB insert failed', $nf->id, esc_html($nf->title));
                continue;
            }

            $migrated++;
            $entry_count = $this->migrate_nf_entries((int)$nf->id, $form_id, $nf_field_rows);
            $log[] = sprintf('✓ NF #%d "%s" → Form ID %d (%d fields, %d entries)',
                $nf->id, esc_html($nf->title), $form_id, count($fields), $entry_count);
        }

        return compact('migrated', 'skipped', 'errors', 'log');
    }

    private function parse_nf_fields(array $nf_field_rows, array $meta_by_field = []): array {
        $nf_to_ff = [
            'textbox'         => 'text',
            'firstname'       => 'text',
            'lastname'        => 'text',
            'email'           => 'email',
            'phone'           => 'phone',
            'number'          => 'number',
            'date'            => 'date',
            'textarea'        => 'textarea',
            'listselect'      => 'select',
            'listmultiselect' => 'select',
            'listcheckbox'    => 'checkbox',
            'listradio'       => 'radio',
            'listimage'       => 'radio',
            'file'            => 'file_upload',
            'hr'              => null,
            'html'            => null,
            'note'            => 'heading',
            'submit'          => null,
            'recaptcha'       => null,
            'honeypot'        => null,
            'hidden'          => null,
        ];

        $fields = [];
        $index  = 0;

        foreach ($nf_field_rows as $nf_field) {
            $type = strtolower($nf_field->type ?? 'textbox');
            if (!array_key_exists($type, $nf_to_ff) || $nf_to_ff[$type] === null) continue;

            $ff_type = $nf_to_ff[$type];
            $meta = $meta_by_field[(int) $nf_field->id] ?? [];
            $field   = [
                'id'          => 'field_' . $index,
                'type'        => $ff_type,
                'label'       => $nf_field->label ?? ucfirst($type),
                'required'    => $this->decode_bool($meta['required'] ?? $nf_field->required ?? false),
                'placeholder' => (string) ($meta['placeholder'] ?? ''),
            ];

            if (in_array($ff_type, ['select', 'checkbox', 'radio'], true)) {
                $options = $this->parse_nf_options($meta['options'] ?? null);
                if ($options) {
                    $field['options'] = $options;
                }
            }

            if ($ff_type === 'file_upload') {
                $accept = $this->normalize_accept_extensions(
                    $meta['file_upload_types']
                    ?? $meta['upload_types']
                    ?? $meta['allowed_file_types']
                    ?? ''
                );
                if ($accept !== '') {
                    $field['accept'] = $accept;
                }
            }

            $fields[] = $field;
            $index++;
        }

        return $fields;
    }

    private function migrate_nf_entries(int $nf_form_id, int $ff_form_id, array $nf_field_rows): int {
        global $wpdb;

        $count = $this->migrate_nf_current_entries($nf_form_id, $ff_form_id, $nf_field_rows);

        $subs_table     = $wpdb->prefix . 'nf3_subs';
        $sub_meta_table = $wpdb->prefix . 'nf3_sub_meta';

        if ($wpdb->get_var("SHOW TABLES LIKE '{$subs_table}'") !== $subs_table) return $count; // 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

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from $wpdb->prefix, values are %-prepared; one-time/admin read
        $subs = $wpdb->get_results($wpdb->prepare(
            "SELECT id, date_updated FROM {$subs_table} WHERE form_id = %d ORDER BY id ASC",
            $nf_form_id
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

        // Build field row map: id → label
        $field_map = [];
        foreach ($nf_field_rows as $f) {
            $field_map[(int)$f->id] = $f->label ?? '';
        }

        foreach ($subs as $sub) {
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from $wpdb->prefix, values are %-prepared; one-time/admin read
            $meta_rows = $wpdb->get_results($wpdb->prepare(
                "SELECT `key`, `value` FROM {$sub_meta_table} WHERE parent_id = %d",
                $sub->id
            ));
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

            $data = [];
            foreach ($meta_rows as $row) {
                if (empty(trim((string)$row->value))) continue;
                // key is typically the field ID
                $fid   = is_numeric($row->key) ? (int)$row->key : $row->key;
                $label = $field_map[$fid] ?? $row->key;
                $data[$row->key] = ['label' => $label, 'value' => $row->value, 'type' => 'text'];
            }

            $this->insert_submission($ff_form_id, $data, [
                'ip_address' => '',
                'user_agent' => '',
                'user_id'    => 0,
                'created_at' => $sub->date_updated ?? current_time('mysql'),
            ]);
            $count++;
        }

        return $count;
    }

    private function migrate_nf_current_entries(int $nf_form_id, int $ff_form_id, array $nf_field_rows): int {
        global $wpdb;

        $subs = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table; one-off migration query, not cacheable
            "SELECT p.ID, p.post_author, p.post_date, p.post_modified
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} m ON m.post_id = p.ID
             WHERE p.post_type = 'nf_sub'
               AND p.post_status = 'publish'
               AND m.meta_key = '_form_id'
               AND m.meta_value = %s
             ORDER BY p.ID ASC",
            (string) $nf_form_id
        ));

        if (!$subs) {
            return 0;
        }

        $field_map = [];
        foreach ($nf_field_rows as $f) {
            $field_map[(int) $f->id] = $f->label ?? '';
        }

        $count = 0;
        foreach ($subs as $sub) {
            $meta_rows = $wpdb->get_results($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 meta_key, meta_value
                 FROM {$wpdb->postmeta}
                 WHERE post_id = %d AND meta_key LIKE %s
                 ORDER BY meta_id ASC",
                $sub->ID,
                $wpdb->esc_like('_field_') . '%'
            ));

            $data = [];
            foreach ($meta_rows as $row) {
                $field_id = (int) substr((string) $row->meta_key, 7);
                if ($field_id <= 0 || !isset($field_map[$field_id])) {
                    continue;
                }

                $value = forge_safe_unserialize($row->meta_value);
                if (is_array($value)) {
                    $value = implode(', ', array_filter(array_map('trim', array_map('strval', $value))));
                } else {
                    $value = trim((string) $value);
                }
                if ($value === '') {
                    continue;
                }

                $data[(string) $field_id] = [
                    'label' => $field_map[$field_id],
                    'value' => $value,
                    'type'  => 'text',
                ];
            }

            if (!$data) {
                continue;
            }

            $this->insert_submission($ff_form_id, $data, [
                'ip_address' => '',
                'user_agent' => '',
                'user_id'    => (int) ($sub->post_author ?? 0),
                'created_at' => $sub->post_date ?? $sub->post_modified ?? current_time('mysql'),
            ]);
            $count++;
        }

        return $count;
    }

    
    // Normalizers
    

    private function normalize_accept_extensions($raw): string {
        if (is_array($raw)) {
            $raw = implode(',', $raw);
        }
        $raw = trim((string) $raw);
        if ($raw === '') {
            return '';
        }

        $tokens = preg_split('/[\s,|]+/', strtolower($raw));
        $accept = [];
        foreach ($tokens as $token) {
            $token = trim((string) $token);
            if ($token === '') {
                continue;
            }
            if (strpos($token, '/') !== false) {
                $accept[] = preg_replace('/[^a-z0-9.+\-\/]/', '', $token);
                continue;
            }
            $token = ltrim($token, '.');
            $token = preg_replace('/[^a-z0-9.+\-]/', '', $token);
            if ($token !== '') {
                $accept[] = '.' . $token;
            }
        }

        return implode(',', array_values(array_unique(array_filter($accept))));
    }

    private function extract_cf7_filetypes(string $attrs): string {
        if (preg_match('/(?:filetypes|accept):([^\s"\]]+)/i', $attrs, $m)) {
            return $m[1];
        }
        return '';
    }

    private function decode_bool($value): bool {
        if (is_bool($value)) return $value;
        if (is_int($value) || is_float($value)) return ((int) $value) === 1;
        if ($value === null) return false;

        $raw = (string) $value;
        if ($raw === "\0") return false;
        if ($raw === "\1") return true;

        $normalized = strtolower(trim($raw));
        if ($normalized === '') return false;
        return !in_array($normalized, ['0', 'false', 'no', 'off'], true);
    }

    private function get_nf_field_meta(array $nf_field_rows): array {
        global $wpdb;

        $ids = [];
        foreach ($nf_field_rows as $row) {
            $id = (int) ($row->id ?? 0);
            if ($id > 0) {
                $ids[] = $id;
            }
        }
        if (!$ids) {
            return [];
        }

        $table = $wpdb->prefix . 'nf3_field_meta';
        if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") !== $table) { // 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
            return [];
        }

        $placeholders = implode(',', array_fill(0, count($ids), '%d'));
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from $wpdb->prefix; %d placeholders built from count($ids), int IDs %-prepared; one-time/admin read
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT parent_id, `key`, `value`, meta_key, meta_value
             FROM {$table}
             WHERE parent_id IN ($placeholders)",
            $ids
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

        $meta = [];
        foreach ($rows as $row) {
            $field_id = (int) $row->parent_id;
            $key = (string) ($row->key !== null && $row->key !== '' ? $row->key : $row->meta_key);
            if ($key === '') {
                continue;
            }
            $value = $row->value !== null ? $row->value : $row->meta_value;
            $meta[$field_id][$key] = $value;
        }

        return $meta;
    }

    private function parse_nf_options($raw): array {
        if ($raw === null || $raw === '') {
            return [];
        }

        $value = forge_safe_unserialize($raw);
        if (is_string($value)) {
            $decoded = json_decode($value, true);
            if (json_last_error() === JSON_ERROR_NONE) {
                $value = $decoded;
            }
        }

        if (!is_array($value)) {
            return [];
        }

        $options = [];
        foreach ($value as $item) {
            if (is_array($item)) {
                $label = $item['label'] ?? $item['text'] ?? $item['value'] ?? '';
            } else {
                $label = $item;
            }
            $label = trim((string) $label);
            if ($label !== '') {
                $options[] = $label;
            }
        }

        return $options;
    }

    private function is_supported_gf_field_type(string $type): bool {
        $supported = [
            'text', 'textarea', 'email', 'phone', 'number', 'date', 'time',
            'select', 'multiselect', 'checkbox', 'radio', 'fileupload',
            'name', 'address', 'website', 'list', 'section',
        ];
        return in_array($type, $supported, true);
    }

    private function format_gf_compound_value(array $mapped, array $parts): string {
        if (!$parts) {
            return '';
        }

        $ordered = [];
        foreach (($mapped['inputs'] ?? []) as $input) {
            $id = isset($input['id']) ? (string) $input['id'] : '';
            if ($id !== '' && isset($parts[$id])) {
                $ordered[] = $parts[$id];
            }
        }
        if (!$ordered) {
            ksort($parts, SORT_NATURAL);
            $ordered = array_values($parts);
        }

        $ordered = array_values(array_filter(array_map('trim', $ordered), function ($value) {
            return $value !== '';
        }));

        if (($mapped['type'] ?? '') === 'name') {
            return implode(' ', $ordered);
        }

        return implode(', ', $ordered);
    }

    
    // DB helpers
    

    private function insert_form(string $title, array $fields, array $settings): int {
        global $wpdb;

        $table = $wpdb->prefix . 'formforge_forms';
        $defaults = [
            'submit_text'      => __('Submit', 'formforge'),
            'success_message'  => __('Thank you! Your submission has been received.', 'formforge'),
            'redirect_url'     => '',
        ];

        $result = $wpdb->insert($table, [ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
            'title'            => sanitize_text_field($title),
            'fields'           => wp_json_encode($fields, JSON_UNESCAPED_UNICODE),
            'settings'         => wp_json_encode(array_merge($defaults, $settings), JSON_UNESCAPED_UNICODE),
            'conditional_logic' => wp_json_encode([]),
            'status'           => 'publish',
            'created_at'       => current_time('mysql'),
            'updated_at'       => current_time('mysql'),
        ]);

        return $result ? (int) $wpdb->insert_id : 0;
    }

    private function insert_submission(int $form_id, array $data, array $meta): void {
        global $wpdb;

        $table = $wpdb->prefix . 'formforge_submissions';
        $wpdb->insert($table, [ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
            'form_id'    => $form_id,
            'data'       => wp_json_encode($data, JSON_UNESCAPED_UNICODE),
            'ip_address' => sanitize_text_field($meta['ip_address'] ?? ''),
            'user_agent' => sanitize_text_field(substr($meta['user_agent'] ?? '', 0, 255)),
            'user_id'    => (int)($meta['user_id'] ?? 0),
            'status'     => 'read',
            'created_at' => $meta['created_at'] ?? current_time('mysql'),
        ]);
    }

    
    // AJAX
    

    public function ajax_run(): void {
        check_ajax_referer('formforge_migration', 'nonce');
        if (!current_user_can('manage_options')) wp_die('Forbidden', 403);

        $source = sanitize_key($_POST['source'] ?? '');
        if (!$source) wp_send_json_error('Missing source');

        $result = $this->migrate($source);
        wp_send_json_success($result);
    }

    
    // Admin page
    

    public function render_page(): void {
        $sources = $this->detect_sources();
        $nonce   = wp_create_nonce('formforge_migration');
        ?>
        <div class="wrap ff-admin">
        <h1><?php esc_html_e('Migration', 'formforge'); ?></h1>
        <p style="color:#6b7280;margin-top:4px;">
            <?php esc_html_e('Import forms and submissions from other plugins. Original data is never modified or deleted.', 'formforge'); ?>
        </p>

        <style>
        .ffm-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(340px,1fr)); gap:20px; margin-top:24px; }
        .ffm-card { background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:24px; }
        .ffm-card h3 { margin:0 0 6px; font-size:15px; display:flex; align-items:center; gap:8px; }
        .ffm-card .ffm-count { font-size:14px; color:#6b7280; margin-bottom:4px; }
        .ffm-card .ffm-note  { font-size:14px; color:#9ca3af; margin-bottom:16px; }
        .ffm-btn { background:#3B82F6; color:#fff; border:none; border-radius:6px; padding:9px 20px;
                   font-size:14px; font-weight:600; cursor:pointer; transition:opacity .15s; }
        .ffm-btn:hover { opacity:.88; }
        .ffm-btn:disabled { opacity:.5; cursor:not-allowed; }
        .ffm-result { margin-top:14px; font-size:14px; display:none; }
        .ffm-result.success { color:#059669; }
        .ffm-result.error   { color:#dc2626; }
        .ffm-log { margin-top:10px; max-height:180px; overflow-y:auto; background:#f9fafb;
                   border:1px solid #e5e7eb; border-radius:6px; padding:10px 12px; font-size:14px;
                   color:#374151; line-height:1.7; }
        .ffm-empty { background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:40px;
                     text-align:center; color:#9ca3af; font-size:14px; margin-top:24px; }
        </style>

        <?php if (empty($sources)) : ?>
            <div class="ffm-empty">
                <span class="dashicons dashicons-search" style="font-size:32px;margin-bottom:12px;display:block;"></span>
                <?php esc_html_e('No supported form plugins detected. Supported: Contact Form 7, Gravity Forms, WPForms, Ninja Forms.', 'formforge'); ?>
            </div>
        <?php else : ?>
            <div class="ffm-grid">
            <?php foreach ($sources as $key => $src) : ?>
                <div class="ffm-card" id="ffm-card-<?php echo esc_attr($key); ?>">
                    <h3>
                        <span class="dashicons dashicons-migrate" style="color:#3B82F6;"></span>
                        <?php echo esc_html($src['name']); ?>
                    </h3>
                    <p class="ffm-count">
                        <?php
                        printf(
                            /* translators: %s: number of forms */
                            esc_html(_n('%s form found', '%s forms found', $src['count'], 'formforge')),
                            '<strong>' . esc_html(number_format_i18n($src['count'])) . '</strong>' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- <strong> is a hardcoded literal; the number is esc_html'd.
                        ); ?>
                        <?php if ($src['entries'] > 0) : ?>
                            &nbsp;·&nbsp;
                            <?php
                            printf(
                                /* translators: %s: number of entries */
                                esc_html(_n('%s entry', '%s entries', $src['entries'], 'formforge')),
                                '<strong>' . esc_html(number_format_i18n($src['entries'])) . '</strong>' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- <strong> is a hardcoded literal; the number is esc_html'd.
                            ); ?>
                        <?php endif; ?>
                    </p>
                    <p class="ffm-note"><?php echo esc_html($src['note']); ?></p>
                    <button class="ffm-btn" data-source="<?php echo esc_attr($key); ?>">
                        <?php esc_html_e('Start Migration', 'formforge'); ?>
                    </button>
                    <div class="ffm-result" id="ffm-result-<?php echo esc_attr($key); ?>"></div>
                </div>
            <?php endforeach; ?>
            </div>
        <?php endif; ?>

        </div>

        <script>
        (function($) {
            $('.ffm-btn').on('click', function() {
                var $btn    = $(this);
                var source  = $btn.data('source');
                var $result = $('#ffm-result-' + source);

                $btn.prop('disabled', true).text('<?php echo esc_js(__('Migrating…', 'formforge')); ?>');
                $result.hide().removeClass('success error');

                $.post(ajaxurl, {
                    action: 'formforge_migration_run',
                    nonce:  '<?php echo esc_js($nonce); ?>',
                    source: source
                }, function(res) {
                    $btn.prop('disabled', false).text('<?php echo esc_js(__('Start Migration', 'formforge')); ?>');

                    if (res.success) {
                        var d = res.data;
                        var html = '<strong>✓ <?php echo esc_js(__('Done', 'formforge')); ?></strong> — '
                            + '<?php echo esc_js(__('Migrated', 'formforge')); ?>: ' + d.migrated
                            + ', <?php echo esc_js(__('Skipped', 'formforge')); ?>: ' + d.skipped
                            + (d.errors ? ', <?php echo esc_js(__('Errors', 'formforge')); ?>: ' + d.errors : '');
                        if (d.log && d.log.length) {
                            html += '<div class="ffm-log">' + d.log.join('<br>') + '</div>';
                        }
                        $result.addClass('success').html(html).show();
                    } else {
                        $result.addClass('error').text('<?php echo esc_js(__('Error', 'formforge')); ?>: ' + (res.data || 'unknown')).show();
                    }
                }).fail(function() {
                    $btn.prop('disabled', false).text('<?php echo esc_js(__('Start Migration', 'formforge')); ?>');
                    $result.addClass('error').text('<?php echo esc_js(__('Request failed. Try again.', 'formforge')); ?>').show();
                });
            });
        })(jQuery);
        </script>
        <?php
    }
}
