<?php

namespace GriffinForms\Includes\Checks;

use GriffinForms\Includes\Integrations\FormSettingsManager;
use GriffinForms\Includes\Integrations\Categories\Captcha as CaptchaRegistry;

class Form
{
    /**
     * Run checks and return issue list.
     *
     * @param int $form_id
     * @return array<int,array<string,mixed>>
     */
    public function run(int $form_id): array
    {
        if ($form_id <= 0) {
            return [];
        }

        $issues = [];
        $issues = array_merge($issues, $this->getComplianceIssues($form_id));
        $issues = array_merge($issues, $this->getBuilderIssues($form_id));
        $issues = array_merge($issues, $this->getExtensionInactiveIssues($form_id));

        if (function_exists('apply_filters')) {
            $form = new \GriffinForms\Items\Form();
            $form->loadItem($form_id);
            $issues = apply_filters('griffinforms_form_checks', $issues, $form_id, $form);
        }

        return is_array($issues) ? $issues : [];
    }

    /**
     * Compliance warnings (non-blocking).
     *
     * @param int $form_id
     * @return array<int,array<string,mixed>>
     */
    private function getComplianceIssues(int $form_id): array
    {
        $form = new \GriffinForms\Items\Form();
        $form->loadItem($form_id);

        if (!$this->isComplianceEnabled($form)) {
            return [];
        }

        $issues = [];
        $save_entries = (string) $form->getProp('save_entries');
        $profile = (string) $form->getProp('compliance_profile');
        $flags = $form->getProp('compliance_flags');
        if (!is_array($flags)) {
            $flags = [];
        }
        $flags = \GriffinForms\Includes\Helpers\Compliance::resolveFlags($flags);

        $retention_days = isset($flags['retention_days']) ? absint($flags['retention_days']) : 0;
        $settings = \GriffinForms\Settings::getInstance();
        $logging_enabled = (int) $settings->getOption('enable_native_logging', 0);

        if ($save_entries === 'no_entries') {
            $issues[] = $this->issue(
                'compliance_no_entries',
                'warning',
                __('Compliance is on, but this form is set to store no entries. Audit history may be incomplete.', 'griffinforms-form-builder')
            );
        }

        if ($logging_enabled !== 1) {
            $issues[] = $this->issue(
                'compliance_logging_disabled',
                'warning',
                __('Compliance is on, but native logging is off. Audit events won’t be captured.', 'griffinforms-form-builder')
            );
        }

        if ($retention_days > 0 && $save_entries !== '' && $save_entries !== 'all_entries') {
            $issues[] = $this->issue(
                'compliance_retention_mismatch',
                'warning',
                __('Retention is set, and this form also limits saved entries. Please confirm both settings align.', 'griffinforms-form-builder')
            );
        }

        if ($this->hasPublicFileUploads($form_id, $form)) {
            $issues[] = $this->issue(
                'compliance_public_file_uploads',
                'warning',
                __('Some file upload fields are set to store files publicly. Consider using private storage while compliance is on.', 'griffinforms-form-builder')
            );
        }

        return $issues;
    }

    private function isComplianceEnabled(\GriffinForms\Items\Form $form): bool
    {
        $compliance_mode = (int) $form->getProp('compliance_mode');
        if ($compliance_mode === 1) {
            return true;
        }

        $flag_mode = '';
        $flags = $form->getProp('compliance_flags');
        if (is_array($flags) && isset($flags['mode'])) {
            $flag_mode = (string) $flags['mode'];
        }

        $flag_mode_prop = $form->getProp('compliance_flag_mode');
        if (is_string($flag_mode_prop) && $flag_mode_prop !== '') {
            $flag_mode = $flag_mode_prop;
        }

        $flag_mode = strtolower(trim($flag_mode));
        if ($flag_mode === '' || $flag_mode === 'inherit') {
            $settings = \GriffinForms\Settings::getInstance();
            return (int) $settings->getOption('compliance_defaults_enabled', 0) === 1;
        }

        return false;
    }

    /**
     * Builder-focused warnings (non-compliance).
     *
     * @param int $form_id
     * @return array<int,array<string,mixed>>
     */
    private function getBuilderIssues(int $form_id): array
    {
        $form = new \GriffinForms\Items\Form();
        $form->loadItem($form_id);

        $issues = [];
        $formpages = $form->getProp('formpages');
        if (!is_array($formpages) || empty($formpages)) {
            $issues[] = $this->issue(
                'builder_no_pages',
                'error',
                __('No pages yet. Add a page to start your layout.', 'griffinforms-form-builder')
            );
            return $issues;
        }

        $email_field_ids = $this->getEmailFieldIds($form_id, $form);

        if (!$this->formHasAnyFields($form_id, $form)) {
            $issues[] = $this->issue(
                'builder_no_fields',
                'warning',
                __('No fields yet. Drag fields into the layout to start collecting responses.', 'griffinforms-form-builder')
            );
        }

        if (empty($email_field_ids)) {
            $issues[] = $this->issue(
                'builder_no_email_fields',
                'warning',
                __('No email field yet. Add one to enable confirmations and notifications.', 'griffinforms-form-builder')
            );
        }

        $notification_issues = $this->getNotificationIssues($form_id, $form, $email_field_ids);
        if (!empty($notification_issues)) {
            $issues = array_merge($issues, $notification_issues);
        }

        if ($this->hasRecentFormErrors($form_id, 10)) {
            $issues[] = $this->issue(
                'builder_recent_errors',
                'error',
                __('We’ve logged errors for this form in the last 10 days. Please review the logs.', 'griffinforms-form-builder')
            );
        }

        if ($this->isLastSubmissionStale($form_id, $form)) {
            $issues[] = $this->issue(
                'builder_submission_stale',
                'warning',
                __('No submissions in the last 7 days. You might want to check publish status or share the form.', 'griffinforms-form-builder')
            );
        }

        if ($this->isFormThemeMissing($form)) {
            $issues[] = $this->issue(
                'builder_form_theme_missing',
                'error',
                __('The selected form theme no longer exists. Please choose another theme.', 'griffinforms-form-builder')
            );
        }

        if ($this->isAntispamMissing()) {
            $issues[] = $this->issue(
                'builder_antispam_missing',
                'warning',
                __('Honeypot and default captcha are both off. Turning one on can help reduce spam.', 'griffinforms-form-builder')
            );
        }

        $structure = $this->getLayoutStructureStats($form);
        if (($structure['pages_without_rows'] ?? 0) > 0) {
            $issues[] = $this->issue(
                'builder_pages_without_rows',
                'warning',
                __('Some pages have no rows yet. Add a row to each page.', 'griffinforms-form-builder')
            );
        }
        if (($structure['rows_without_columns'] ?? 0) > 0) {
            $issues[] = $this->issue(
                'builder_rows_without_columns',
                'warning',
                __('Some rows have no columns yet. Add a column to each row.', 'griffinforms-form-builder')
            );
        }
        if (($structure['columns_without_fields'] ?? 0) > 0) {
            $issues[] = $this->issue(
                'builder_columns_without_fields',
                'warning',
                __('Some columns have no fields yet.', 'griffinforms-form-builder')
            );
        }

        $progress_bar_issues = $this->getProgressBarIssues($form);
        if (!empty($progress_bar_issues)) {
            $issues = array_merge($issues, $progress_bar_issues);
        }

        $progress_bar_disabled_issues = $this->getProgressBarDisabledIssues($form);
        if (!empty($progress_bar_disabled_issues)) {
            $issues = array_merge($issues, $progress_bar_disabled_issues);
        }

        $logic_issues = $this->getConditionalLogicIssues($form_id, $form);
        if (!empty($logic_issues)) {
            $issues = array_merge($issues, $logic_issues);
        }

        $account_issues = $this->getAccountRegistrationIssues($form_id, $form);
        if (!empty($account_issues)) {
            $issues = array_merge($issues, $account_issues);
        }

        $edit_email_issues = $this->getSubmissionEditEmailFieldIssues($form_id, $form);
        if (!empty($edit_email_issues)) {
            $issues = array_merge($issues, $edit_email_issues);
        }

        $payment_gateway_issues = $this->getPaymentGatewaySelectionIssues($form_id, $form);
        if (!empty($payment_gateway_issues)) {
            $issues = array_merge($issues, $payment_gateway_issues);
        }

        $mailchimp_issues = $this->getMailchimpIssues($form_id, $form);
        if (!empty($mailchimp_issues)) {
            $issues = array_merge($issues, $mailchimp_issues);
        }

        $submit_label = (string) $form->getProp('submit_label');
        if ($submit_label === '') {
            $issues[] = $this->issue(
                'builder_submit_label_empty',
                'info',
                __('Submit button label is empty. Adding a label can help users.', 'griffinforms-form-builder')
            );
        }

        $user_action = $form->getProp('user_action');
        $action_type = '';
        $action_message = '';
        $action_url = '';
        if (is_array($user_action)) {
            $action_type = isset($user_action['type']) ? (string) $user_action['type'] : '';
            $action_message = isset($user_action['message']) ? (string) $user_action['message'] : '';
            $action_url = isset($user_action['url']) ? (string) $user_action['url'] : '';
        }

        if ($action_type === '' || $action_type === 'do_nothing') {
            $issues[] = $this->issue(
                'builder_no_success_action',
                'info',
                __('No success message or redirect is set yet.', 'griffinforms-form-builder')
            );
        } elseif ($action_type === 'show_success_message' && $action_message === '') {
            $issues[] = $this->issue(
                'builder_success_message_empty',
                'info',
                __('Success message is enabled but empty. Add a short confirmation message.', 'griffinforms-form-builder')
            );
        } elseif ($action_type === 'redirect_user' && $action_url === '') {
            $issues[] = $this->issue(
                'builder_redirect_url_empty',
                'info',
                __('Redirect is enabled but no URL is set yet. Add a URL or disable redirect.', 'griffinforms-form-builder')
            );
        }

        return $issues;
    }

    /**
     * Progress bar enabled on a single-page form.
     *
     * @param \GriffinForms\Items\Form $form
     * @return array<int,array<string,mixed>>
     */
    private function getProgressBarIssues(\GriffinForms\Items\Form $form): array
    {
        $progress_bar = $form->getProp('progress_bar');
        if (!is_array($progress_bar)) {
            return [];
        }

        if (empty($progress_bar['toggle'])) {
            return [];
        }

        $formpages = $form->getProp('formpages');
        $page_count = is_array($formpages) ? count($formpages) : 0;
        if ($page_count <= 1) {
            return [
                $this->issue(
                    'builder_progress_bar_single_page',
                    'info',
                    __('Progress bar is enabled, but this form only has one page. Add more pages or disable the progress bar.', 'griffinforms-form-builder')
                ),
            ];
        }

        return [];
    }

    /**
     * Progress bar disabled on a multi-page form.
     *
     * @param \GriffinForms\Items\Form $form
     * @return array<int,array<string,mixed>>
     */
    private function getProgressBarDisabledIssues(\GriffinForms\Items\Form $form): array
    {
        $progress_bar = $form->getProp('progress_bar');
        if (!is_array($progress_bar) || !empty($progress_bar['toggle'])) {
            return [];
        }

        $formpages = $form->getProp('formpages');
        $page_count = is_array($formpages) ? count($formpages) : 0;
        if ($page_count > 1) {
            return [
                $this->issue(
                    'builder_progress_bar_disabled_multi_page',
                    'info',
                    __('This form has multiple pages, but the progress bar is disabled.', 'griffinforms-form-builder')
                ),
            ];
        }

        return [];
    }

    private function getPaymentGatewaySelectionIssues(int $form_id, \GriffinForms\Items\Form $form): array
    {
        if ($form_id <= 0) {
            return [];
        }

        if (!$this->formHasFieldType($form_id, $form, ['payment'])) {
            return [];
        }

        $registry = \GriffinForms\Includes\Integrations\Categories\Payment::getInstance();
        if (!$registry) {
            return [];
        }

        $configured = $registry->getConfiguredProviders();
        if (empty($configured['paypal'])) {
            return [];
        }

        if (in_array('paypal', $registry->getAllowedProviderIds(), true)) {
            return [];
        }

        return [
            $this->issue(
                'builder_payment_paypal_configured_not_selected',
                'info',
                __('This form uses a Payment field, but PayPal is not enabled in Settings -> Payments. Buyers will not see PayPal at checkout until you allow it there.', 'griffinforms-form-builder')
            ),
        ];
    }

    private function getMailchimpIssues(int $form_id, \GriffinForms\Items\Form $form): array
    {
        if ($form_id <= 0) {
            return [];
        }

        $providers = \GriffinForms\Includes\Integrations\Providers::getInstance();
        $provider = $providers->get('mailchimp', true);
        $integration = $providers->getIntegration('mailchimp');
        if (!$provider || !$integration) {
            return [];
        }

        $status = method_exists($integration, 'getStatus') ? (array) $integration->getStatus() : [];
        $configured_globally = !empty($status['configured']) || !empty($provider['configured']);
        $provider_active = array_key_exists('active', $status)
            ? !empty($status['active'])
            : $configured_globally;
        if (!$configured_globally || !$provider_active) {
            return [];
        }

        $manager = FormSettingsManager::getInstance();
        $stored = $manager->getIntegrationSettings($form_id, 'mailchimp');
        $stored_mode = is_array($stored) ? sanitize_key((string) ($stored['mode'] ?? 'inherit')) : 'inherit';
        $stored_settings = $this->normalizeMailchimpStoredSettings($stored);

        $field_ids = $this->getFormFieldIds($form_id, $form);
        $field_map = $this->getFieldMap($field_ids);
        $issues = [];

        $has_stored_configuration = $this->hasMeaningfulMailchimpStoredSettings($stored_mode, $stored_settings);

        if ($stored_mode === 'disabled') {
            $issues[] = $this->issue(
                'mailchimp_disabled_for_form',
                'info',
                __('Mailchimp is configured globally but disabled for this form.', 'griffinforms-form-builder')
            );
            return $issues;
        }

        if (!$has_stored_configuration) {
            $issues[] = $this->issue(
                'mailchimp_no_form_settings',
                'info',
                __('Mailchimp is configured globally, but this form has no Mailchimp mappings saved yet.', 'griffinforms-form-builder')
            );
            return $issues;
        }

        $integration->setFormContextId($form_id);
        $effective = method_exists($integration, 'getEffectiveFormSettings')
            ? $integration->getEffectiveFormSettings($form_id)
            : [];

        $raw_email_field_id = absint($stored_settings['subscriber_email_field'] ?? 0);
        if ($raw_email_field_id <= 0) {
            $issues[] = $this->issue(
                'mailchimp_email_field_missing',
                'error',
                __('No Mailchimp Subscriber Email field is selected for this form. Choose an email field before using Mailchimp sync.', 'griffinforms-form-builder')
            );
        } elseif (!isset($field_map[$raw_email_field_id])) {
            $issues[] = $this->issue(
                'mailchimp_missing_mapped_field',
                'error',
                __('A Mailchimp-mapped field no longer exists. Review your Mailchimp field mappings.', 'griffinforms-form-builder')
            );
        } elseif (!$this->fieldMatchesType($field_map, $raw_email_field_id, ['inputemail'])) {
            $issues[] = $this->issue(
                'mailchimp_email_field_invalid_type',
                'error',
                __('Mailchimp Subscriber Email must map to an email field. Choose a valid email field.', 'griffinforms-form-builder')
            );
        }

        foreach ([
            'first_name_field' => __('Mailchimp first name mapping references a field that no longer exists.', 'griffinforms-form-builder'),
            'last_name_field' => __('Mailchimp last name mapping references a field that no longer exists.', 'griffinforms-form-builder'),
        ] as $key => $message) {
            $field_id = absint($stored_settings[$key] ?? 0);
            if ($field_id > 0 && !isset($field_map[$field_id])) {
                $issues[] = $this->issue(
                    'mailchimp_' . $key . '_missing',
                    'warning',
                    $message
                );
            }
        }

        $raw_consent_field_id = absint($stored_settings['consent_field_id'] ?? 0);
        if ($raw_consent_field_id > 0 && !isset($field_map[$raw_consent_field_id])) {
            $issues[] = $this->issue(
                'mailchimp_consent_field_missing',
                'warning',
                __('Mailchimp consent source references a field that no longer exists.', 'griffinforms-form-builder')
            );
        } elseif ($raw_consent_field_id > 0 && !$this->fieldMatchesType($field_map, $raw_consent_field_id, ['inputcheckbox', 'termsconditions'])) {
            $issues[] = $this->issue(
                'mailchimp_consent_field_invalid_type',
                'warning',
                __('Mailchimp consent source must be a checkbox-style or Terms & Conditions field.', 'griffinforms-form-builder')
            );
        }

        $effective_mode = sanitize_key((string) ($effective['mode'] ?? 'custom'));
        if ($effective_mode !== 'disabled') {
            $effective_audience_id = sanitize_text_field((string) ($effective['audience_id'] ?? ''));
            if ($effective_audience_id === '') {
                $issues[] = $this->issue(
                    'mailchimp_audience_missing',
                    'error',
                    __('No Mailchimp audience is selected for this form. Choose an audience override or set a default audience in Mailchimp settings.', 'griffinforms-form-builder')
                );
            } elseif (empty($stored_settings['audience_id'])) {
                $issues[] = $this->issue(
                    'mailchimp_inheriting_global_audience',
                    'info',
                    __('This form is using the global Mailchimp default audience.', 'griffinforms-form-builder')
                );
            }
        }

        if ($this->hasRecentMailchimpErrors($form_id, 10)) {
            $issues[] = $this->issue(
                'mailchimp_recent_errors',
                'error',
                __('Recent Mailchimp sync errors were logged for this form. Review Mailchimp logs before publishing further changes.', 'griffinforms-form-builder')
            );
        }

        return $issues;
    }

    /**
     * Emit an error issue for any Pro-only integration that has form settings
     * saved but whose Pro extension is currently inactive.
     *
     * @param int $form_id
     * @return array<int,array<string,mixed>>
     */
    private function getExtensionInactiveIssues(int $form_id): array
    {
        $issues    = [];
        $providers = \GriffinForms\Includes\Integrations\Providers::getInstance();
        $manager   = FormSettingsManager::getInstance();

        foreach ($providers->getAll() as $provider_id => $provider) {
            if (empty($provider['is_paid'])) {
                continue;
            }

            $integration = $providers->getIntegration($provider_id);
            if (!$integration) {
                continue;
            }

            $status = method_exists($integration, 'getStatus') ? (array) $integration->getStatus() : [];
            if (empty($status['requires_extension'])) {
                // Pro is active — real class is in control; skip.
                continue;
            }

            // Extension is inactive. Warn only when this form has non-trivial saved settings.
            $stored = $manager->getIntegrationSettings($form_id, $provider_id);
            if (empty($stored) || $stored === ['mode' => 'inherit']) {
                continue;
            }

            $name     = esc_html($provider['name'] ?? $provider_id);
            $issues[] = [
                'code'    => $provider_id . '_extension_inactive',
                'level'   => 'error',
                'message' => sprintf(
                    /* translators: %s: integration name */
                    __('This form has %s sync configured, but the Pro extension is not active. Submissions will not be synced until Pro is reactivated.', 'griffinforms-form-builder'),
                    $name
                ),
            ];
        }

        return $issues;
    }

    private function getSubmissionEditEmailFieldIssues(int $form_id, \GriffinForms\Items\Form $form): array
    {
        $issues = [];

        $edit_policy = $form->getProp('submission_edit');
        if (!is_array($edit_policy)) {
            return $issues;
        }

        if (empty($edit_policy['toggle']) || empty($edit_policy['email_owners'])) {
            return $issues;
        }

        $mapped_field_ids = isset($edit_policy['email_fields']) && is_array($edit_policy['email_fields'])
            ? array_values(array_filter(array_map('absint', $edit_policy['email_fields'])))
            : [];

        if (empty($mapped_field_ids)) {
            return $issues;
        }

        foreach ($mapped_field_ids as $field_id) {
            $field = new \GriffinForms\Items\Field();
            if (!$field->loadItem($field_id)) {
                continue;
            }

            if ((int) $field->getProp('is_required') !== 1) {
                $issues[] = $this->issue(
                    'builder_edit_email_field_not_required',
                    'warning',
                    __('An email field used for submission edit access is not required. Make it required so submitters don\'t lose their edit link.', 'griffinforms-form-builder')
                );
            }
        }

        return $issues;
    }

    private function getAccountRegistrationIssues(int $form_id, \GriffinForms\Items\Form $form): array
    {
        if ($form_id <= 0) {
            return [];
        }

        $manager = FormSettingsManager::getInstance();
        $stored = $manager->getIntegrationSettings($form_id, 'wordpress_accounts');
        $rules = $this->normalizeAccountRules($stored);

        if (empty($rules)) {
            return [];
        }

        $field_ids = $this->getFormFieldIds($form_id, $form);
        $field_map = $this->getFieldMap($field_ids);
        $role_map = $this->getRoleMap();
        $settings = \GriffinForms\Settings::getInstance();
        $logging_enabled = (int) $settings->getOption('enable_native_logging', 0) === 1;
        $issues = [];
        $email_field_rule_map = [];
        $password_field_rule_map = [];

        if (!$logging_enabled) {
            $issues[] = $this->issue(
                'accounts_native_logging_disabled',
                'warning',
                __('User Registration is configured on this form, but native logging is disabled. Successful and failed registration actions will be harder to review.', 'griffinforms-form-builder')
            );
        }

        if (!$this->hasActiveCaptchaProtectionForForm($form_id)) {
            $issues[] = $this->issue(
                'accounts_registration_without_captcha',
                'warning',
                __('User Registration is configured on this form, but no CAPTCHA protection is active for it. Sign-up forms without CAPTCHA can attract spam registrations.', 'griffinforms-form-builder')
            );
        }

        foreach ($rules as $index => $rule) {
            if (!is_array($rule)) {
                continue;
            }

            $rule_number = $index + 1;
            $rule_code_suffix = 'rule_' . $rule_number;
            $rule_prefix = sprintf(__('User Registration %d', 'griffinforms-form-builder'), $rule_number);

            $email_field_id = absint($rule['email_field_id'] ?? 0);
            if ($email_field_id <= 0 || !$this->fieldMatchesType($field_map, $email_field_id, ['inputemail'])) {
                $issues[] = $this->issue(
                    'accounts_missing_email_field_' . $rule_code_suffix,
                    'error',
                    sprintf(__('%s maps an email field that no longer exists. Open User Registration and choose another email field.', 'griffinforms-form-builder'), $rule_prefix)
                );
            } else {
                if (!isset($email_field_rule_map[$email_field_id])) {
                    $email_field_rule_map[$email_field_id] = [];
                }
                $email_field_rule_map[$email_field_id][] = $rule_number;
                if ($this->fieldIsIterable($field_map, $email_field_id)) {
                    $issues[] = $this->issue(
                        'accounts_iterable_email_fanout_info_' . $rule_code_suffix,
                        'info',
                        sprintf(__('%s uses an iterable email field. One submission may create multiple account actions from this setup.', 'griffinforms-form-builder'), $rule_prefix)
                    );
                }
            }

            $password_mode = (string) ($rule['password_mode'] ?? '');
            $password_field_id = absint($rule['password_field_id'] ?? 0);
            $email_field_is_iterable = $this->fieldIsIterable($field_map, $email_field_id);
            if ($password_mode === 'mapped_password' && $email_field_is_iterable) {
                $issues[] = $this->issue(
                    'accounts_iterable_email_with_mapped_password_' . $rule_code_suffix,
                    'error',
                    sprintf(__('%s uses an iterable email field with a mapped password field. Switch the password strategy to Email Password Setup Link.', 'griffinforms-form-builder'), $rule_prefix)
                );
            }

            if ($password_mode === 'mapped_password') {
                if ($password_field_id <= 0 || !$this->fieldMatchesType($field_map, $password_field_id, ['inputpassword'])) {
                    $issues[] = $this->issue(
                        'accounts_missing_password_field_' . $rule_code_suffix,
                        'error',
                        sprintf(__('%s uses a mapped password field that no longer exists. Review the password strategy.', 'griffinforms-form-builder'), $rule_prefix)
                    );
                } else {
                    $password_field = new \GriffinForms\Items\Field();
                    if ($password_field->loadItem($password_field_id)) {
                        if ((int) $password_field->getProp('is_required') !== 1) {
                            $issues[] = $this->issue(
                                'accounts_password_field_not_required_' . $rule_code_suffix,
                                'warning',
                                sprintf(__('%s uses a password field that is not required. Submissions may arrive without a password.', 'griffinforms-form-builder'), $rule_prefix)
                            );
                        }

                        $field_settings = $password_field->getProp('field_settings');
                        $field_settings = is_array($field_settings) ? $field_settings : maybe_unserialize($field_settings);
                        $field_settings = is_array($field_settings) ? $field_settings : [];
                        if (!$email_field_is_iterable && empty($field_settings['require_confirmation'])) {
                            $issues[] = $this->issue(
                                'accounts_password_field_confirmation_disabled_' . $rule_code_suffix,
                                'warning',
                                sprintf(__('%s uses a mapped password field without password confirmation enabled. Consider turning on confirmation to reduce mistyped registrations.', 'griffinforms-form-builder'), $rule_prefix)
                            );
                        }

                        $validations = $password_field->getProp('validations');
                        $validations = is_array($validations) ? $validations : [];
                        $min_strength = isset($validations['min_strength']) ? strtolower(trim((string) $validations['min_strength'])) : 'weak';
                        if ($min_strength === '' || in_array($min_strength, ['weak', 'medium'], true)) {
                            $issues[] = $this->issue(
                                'accounts_password_field_medium_or_weak_policy_' . $rule_code_suffix,
                                'warning',
                                sprintf(__('%s uses a password field that accepts medium or weak passwords. Consider requiring strong or very strong passwords.', 'griffinforms-form-builder'), $rule_prefix)
                            );
                        }
                    }
                }
            } elseif (!$email_field_is_iterable) {
                $issues[] = $this->issue(
                    'accounts_password_setup_link_info_' . $rule_code_suffix,
                    'info',
                    sprintf(__('%s sends a password setup email instead of asking the user to enter a password in the form.', 'griffinforms-form-builder'), $rule_prefix)
                );
            }

            if ($password_mode === 'mapped_password' && $password_field_id > 0) {
                if (!isset($password_field_rule_map[$password_field_id])) {
                    $password_field_rule_map[$password_field_id] = [];
                }
                $password_field_rule_map[$password_field_id][] = $rule_number;
            }

            $username_mode = (string) ($rule['username_mode'] ?? '');
            $username_field_id = absint($rule['username_field_id'] ?? 0);
            if ($username_mode === 'mapped_or_generate') {
                if ($username_field_id <= 0 || !$this->fieldMatchesType($field_map, $username_field_id, ['inputtext', 'inputemail', 'inputurl', 'textarea'])) {
                    $issues[] = $this->issue(
                        'accounts_missing_username_field_' . $rule_code_suffix,
                        'error',
                        sprintf(__('%s is set to map a username field, but that source field is missing.', 'griffinforms-form-builder'), $rule_prefix)
                    );
                } elseif ($this->fieldIsIterable($field_map, $email_field_id) && !$this->fieldIsIterable($field_map, $username_field_id)) {
                    $issues[] = $this->issue(
                        'accounts_iterable_email_requires_iterable_username_field_' . $rule_code_suffix,
                        'error',
                        sprintf(__('%s uses an iterable email field, but the mapped username field is not iterable. Choose an iterable username field or generate usernames automatically.', 'griffinforms-form-builder'), $rule_prefix)
                    );
                } else {
                    $username_field = new \GriffinForms\Items\Field();
                    if ($username_field->loadItem($username_field_id) && (int) $username_field->getProp('is_required') !== 1) {
                        $issues[] = $this->issue(
                            'accounts_username_field_not_required_' . $rule_code_suffix,
                            'info',
                            sprintf(__('%s maps username from a field that is not required. Submissions may arrive without a username value.', 'griffinforms-form-builder'), $rule_prefix)
                        );
                    }
                }
            } else {
                $issues[] = $this->issue(
                    'accounts_generate_username_info_' . $rule_code_suffix,
                    'info',
                    sprintf(__('%s generates usernames automatically instead of mapping a username field.', 'griffinforms-form-builder'), $rule_prefix)
                );
            }

            $issues = array_merge($issues, $this->getAccountMappingFieldIssues($rule, $rule_prefix, $rule_code_suffix, $field_map, $email_field_id));

            $creation_mode = (string) ($rule['creation_mode'] ?? '');
            $confirmation_message_id = absint($rule['confirmation_message_id'] ?? 0);
            if (!empty($rule['send_confirmation_email'])) {
                if ($confirmation_message_id <= 0) {
                    $issues[] = $this->issue(
                        'accounts_confirmation_message_not_selected_' . $rule_code_suffix,
                        'error',
                        sprintf(__('%s is set to send a confirmation email, but no confirmation message is selected.', 'griffinforms-form-builder'), $rule_prefix)
                    );
                } else {
                    $missing_messages = $this->findMissingMessages([$confirmation_message_id]);
                    if (!empty($missing_messages)) {
                        $issues[] = $this->issue(
                            'accounts_missing_confirmation_message_' . $rule_code_suffix,
                            'error',
                            sprintf(__('%s references a confirmation message that no longer exists.', 'griffinforms-form-builder'), $rule_prefix)
                        );
                    }
                }

                if (in_array($creation_mode, ['pending_admin_activation', 'manual_review_only'], true)) {
                    $issues[] = $this->issue(
                        'accounts_deferred_confirmation_followup_' . $rule_code_suffix,
                        'warning',
                        sprintf(__('%s defers account creation, so confirmation email sending depends on later admin follow-through.', 'griffinforms-form-builder'), $rule_prefix)
                    );
                }
            }

            $role_key = sanitize_key((string) ($rule['role'] ?? ''));
            if ($role_key === 'administrator') {
                $issues[] = $this->issue(
                    'accounts_administrator_role_not_allowed_' . $rule_code_suffix,
                    'error',
                    sprintf(__('%s assigns the Administrator role, which is not allowed for account registration setups in V1. Choose another role.', 'griffinforms-form-builder'), $rule_prefix)
                );
            } elseif ($role_key !== '' && !isset($role_map[$role_key])) {
                $issues[] = $this->issue(
                    'accounts_invalid_role_' . $rule_code_suffix,
                    'warning',
                    sprintf(__('%s references a user role that is not currently available on this site.', 'griffinforms-form-builder'), $rule_prefix)
                );
            }

            $existing_account_behavior = (string) ($rule['existing_account_behavior'] ?? '');
            if ($existing_account_behavior === 'accept_noop') {
                $issues[] = $this->issue(
                    'accounts_accept_noop_behavior_info_' . $rule_code_suffix,
                    'info',
                    sprintf(__('%s is set to Accept Form / Do Nothing when the email already belongs to an existing user.', 'griffinforms-form-builder'), $rule_prefix)
                );
            }
        }

        foreach ($email_field_rule_map as $field_id => $rule_numbers) {
            $rule_numbers = array_values(array_unique(array_map('absint', (array) $rule_numbers)));
            if ($field_id <= 0 || count($rule_numbers) <= 1) {
                continue;
            }

            $issues[] = $this->issue(
                'accounts_duplicate_email_field_mapping_' . $field_id,
                'error',
                sprintf(
                    __('Email field #%1$d is mapped by multiple user registration workflows (%2$s). Each email field should belong to only one user registration.', 'griffinforms-form-builder'),
                    $field_id,
                    implode(', ', $rule_numbers)
                )
            );
        }

        foreach ($password_field_rule_map as $field_id => $rule_numbers) {
            $rule_numbers = array_values(array_unique(array_map('absint', (array) $rule_numbers)));
            if ($field_id <= 0 || count($rule_numbers) <= 1) {
                continue;
            }

            $issues[] = $this->issue(
                'accounts_shared_password_field_' . $field_id,
                'warning',
                sprintf(
                    __('Password field #%1$d is reused by multiple user registration workflows (%2$s). Review whether those workflows should share one submitted password source.', 'griffinforms-form-builder'),
                    $field_id,
                    implode(', ', $rule_numbers)
                )
            );
        }

        return $issues;
    }

    private function formHasAnyFields(int $form_id, \GriffinForms\Items\Form $form): bool
    {
        if ($form_id <= 0) {
            return false;
        }

        $field_ids = $this->getFormFieldIds($form_id, $form);
        return !empty($field_ids);
    }

    private function getEmailFieldIds(int $form_id, \GriffinForms\Items\Form $form): array
    {
        $field_ids = $this->getFormFieldIds($form_id, $form);
        if (empty($field_ids)) {
            return [];
        }

        global $wpdb;
        $config = \GriffinForms\Config::getInstance();
        $field_table = $config->getTable('field');
        $placeholders = implode(',', array_fill(0, count($field_ids), '%d'));

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, field_type FROM %i WHERE id IN ({$placeholders})",
                $field_table,
                ...$field_ids
            ),
            ARRAY_A
        );

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

        $email_ids = [];
        foreach ($rows as $row) {
            if (isset($row['field_type']) && $row['field_type'] === 'inputemail') {
                $email_ids[] = (int) $row['id'];
            }
        }

        return $email_ids;
    }

    private function formHasFieldType(int $form_id, \GriffinForms\Items\Form $form, array $field_types): bool
    {
        $field_ids = $this->getFormFieldIds($form_id, $form);
        if (empty($field_ids)) {
            return false;
        }

        $field_map = $this->getFieldMap($field_ids);
        if (empty($field_map)) {
            return false;
        }

        foreach ($field_map as $field_meta) {
            $field_type = (string) ($field_meta['field_type'] ?? '');
            if (in_array($field_type, $field_types, true)) {
                return true;
            }
        }

        return false;
    }

    private function getFormFieldIds(int $form_id, \GriffinForms\Items\Form $form): array
    {
        if ($form_id <= 0) {
            return [];
        }

        $config = \GriffinForms\Config::getInstance();
        $column_table = $config->getTable('rowcolumn');

        // Only use the form_id index shortcut when every column row for this
        // form has been backfilled. A partial backfill (mix of form_id=0 and
        // form_id=N rows) would silently omit unbackfilled columns. The layout
        // tree traversal is always correct and is the safe fallback.
        if ($this->tableHasColumn($column_table, 'form_id')) {
            global $wpdb;
            $layout_col_count = $this->getFormColumnCount($config, $form);
            $indexed_col_count = (int) $wpdb->get_var(
                $wpdb->prepare('SELECT COUNT(*) FROM %i WHERE form_id = %d', $column_table, $form_id)
            );
            if ($layout_col_count > 0 && $indexed_col_count === $layout_col_count) {
                return $this->getFieldsFromColumnsByFormId($column_table, $form_id);
            }
        }

        return $this->getFieldsFromLayoutTree($config, $form);
    }

    private function getFormColumnCount(\GriffinForms\Config $config, \GriffinForms\Items\Form $form): int
    {
        $page_ids = $form->getProp('formpages');
        if (!is_array($page_ids) || empty($page_ids)) {
            return 0;
        }
        $page_ids = array_values(array_filter(array_map('absint', $page_ids)));
        if (empty($page_ids)) {
            return 0;
        }

        global $wpdb;
        $page_table  = $config->getTable('formpage');
        $row_table   = $config->getTable('pagerow');
        $column_table = $config->getTable('rowcolumn');

        $page_ph = implode(',', array_fill(0, count($page_ids), '%d'));
        $pages = $wpdb->get_results(
            $wpdb->prepare("SELECT pagerows FROM %i WHERE id IN ({$page_ph})", $page_table, ...$page_ids),
            ARRAY_A
        );

        $row_ids = [];
        if (is_array($pages)) {
            foreach ($pages as $page) {
                $pr = isset($page['pagerows']) ? maybe_unserialize($page['pagerows']) : [];
                if (is_array($pr)) {
                    foreach ($pr as $rid) { $rid = absint($rid); if ($rid > 0) $row_ids[$rid] = true; }
                }
            }
        }
        if (empty($row_ids)) {
            return 0;
        }

        $row_ids = array_keys($row_ids);
        $row_ph  = implode(',', array_fill(0, count($row_ids), '%d'));
        $rows = $wpdb->get_results(
            $wpdb->prepare("SELECT rowcolumns FROM %i WHERE id IN ({$row_ph})", $row_table, ...$row_ids),
            ARRAY_A
        );

        $col_count = 0;
        if (is_array($rows)) {
            foreach ($rows as $row) {
                $rc = isset($row['rowcolumns']) ? maybe_unserialize($row['rowcolumns']) : [];
                if (is_array($rc)) {
                    foreach ($rc as $cid) { if (absint($cid) > 0) $col_count++; }
                }
            }
        }
        return $col_count;
    }

    private function hasPublicFileUploads(int $form_id, \GriffinForms\Items\Form $form): bool
    {
        if ($form_id <= 0) {
            return false;
        }

        $field_ids = $this->getFormFieldIds($form_id, $form);
        if (empty($field_ids)) {
            return false;
        }

        $settings = \GriffinForms\Settings::getInstance();
        $default_mode = strtolower(trim((string) $settings->getOption('files_default_storage_mode', 'private')));
        if (!in_array($default_mode, ['private', 'media_library', 'media'], true)) {
            $default_mode = 'private';
        }

        global $wpdb;
        $config = \GriffinForms\Config::getInstance();
        $field_table = $config->getTable('field');
        $placeholders = implode(',', array_fill(0, count($field_ids), '%d'));
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, field_type, field_settings FROM %i WHERE id IN ({$placeholders})",
                $field_table,
                ...$field_ids
            ),
            ARRAY_A
        );

        if (!is_array($rows)) {
            return false;
        }

        foreach ($rows as $row) {
            if (!isset($row['field_type'])) {
                continue;
            }
            $field_type = (string) $row['field_type'];
            if (!in_array($field_type, ['fileupload', 'file_upload'], true)) {
                continue;
            }

            $raw_settings = $row['field_settings'] ?? '';
            $field_settings = [];
            if (is_string($raw_settings) && $raw_settings !== '') {
                $decoded = json_decode($raw_settings, true);
                if (is_array($decoded)) {
                    $field_settings = $decoded;
                } else {
                    $field_settings = maybe_unserialize($raw_settings);
                }
            } elseif (is_array($raw_settings)) {
                $field_settings = $raw_settings;
            }

            if (!is_array($field_settings)) {
                $field_settings = [];
            }

            $mode = $field_settings['storage_mode'] ?? '';
            $mode = is_string($mode) ? strtolower(trim($mode)) : '';
            if ($mode === '') {
                $mode = $default_mode;
            }

            if (in_array($mode, ['media_library', 'media'], true)) {
                return true;
            }
        }

        return false;
    }

    private function getFieldsFromColumnsByFormId(string $column_table, int $form_id): array
    {
        if (!$this->tableHasColumn($column_table, 'form_id')) {
            return [];
        }

        global $wpdb;
        $columns = $wpdb->get_results(
            $wpdb->prepare(
                'SELECT fields FROM %i WHERE form_id = %d',
                $column_table,
                $form_id
            ),
            ARRAY_A
        );

        return $this->extractFieldIdsFromColumnRows($columns);
    }

    private function getFieldsFromLayoutTree(\GriffinForms\Config $config, \GriffinForms\Items\Form $form): array
    {
        $page_ids = $form->getProp('formpages');
        if (!is_array($page_ids) || empty($page_ids)) {
            return [];
        }

        $page_ids = array_values(array_filter(array_map('absint', $page_ids)));
        if (empty($page_ids)) {
            return [];
        }

        global $wpdb;
        $page_table = $config->getTable('formpage');
        $row_table = $config->getTable('pagerow');
        $column_table = $config->getTable('rowcolumn');

        $page_placeholders = implode(',', array_fill(0, count($page_ids), '%d'));
        $pages = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT pagerows FROM %i WHERE id IN ({$page_placeholders})",
                $page_table,
                ...$page_ids
            ),
            ARRAY_A
        );

        $row_ids = [];
        if (is_array($pages)) {
            foreach ($pages as $page) {
                $pagerows = isset($page['pagerows']) ? maybe_unserialize($page['pagerows']) : [];
                if (!is_array($pagerows)) {
                    continue;
                }
                foreach ($pagerows as $row_id) {
                    $row_id = absint($row_id);
                    if ($row_id > 0) {
                        $row_ids[$row_id] = true;
                    }
                }
            }
        }

        if (empty($row_ids)) {
            return [];
        }

        $row_ids = array_keys($row_ids);
        $row_placeholders = implode(',', array_fill(0, count($row_ids), '%d'));
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT rowcolumns FROM %i WHERE id IN ({$row_placeholders})",
                $row_table,
                ...$row_ids
            ),
            ARRAY_A
        );

        $column_ids = [];
        if (is_array($rows)) {
            foreach ($rows as $row) {
                $rowcolumns = isset($row['rowcolumns']) ? maybe_unserialize($row['rowcolumns']) : [];
                if (!is_array($rowcolumns)) {
                    continue;
                }
                foreach ($rowcolumns as $column_id) {
                    $column_id = absint($column_id);
                    if ($column_id > 0) {
                        $column_ids[$column_id] = true;
                    }
                }
            }
        }

        if (empty($column_ids)) {
            return [];
        }

        $column_ids = array_keys($column_ids);
        $column_placeholders = implode(',', array_fill(0, count($column_ids), '%d'));
        $columns = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT fields FROM %i WHERE id IN ({$column_placeholders})",
                $column_table,
                ...$column_ids
            ),
            ARRAY_A
        );

        return $this->extractFieldIdsFromColumnRows($columns);
    }

    private function extractFieldIdsFromColumnRows($columns): array
    {
        if (!is_array($columns)) {
            return [];
        }

        $field_ids = [];
        foreach ($columns as $column) {
            $fields = isset($column['fields']) ? maybe_unserialize($column['fields']) : [];
            if (!is_array($fields)) {
                continue;
            }
            foreach ($fields as $field_id) {
                $field_id = absint($field_id);
                if ($field_id > 0) {
                    $field_ids[$field_id] = true;
                }
            }
        }

        return array_keys($field_ids);
    }

    private function parseAutoresponderMappings($value): array
    {
        if (is_string($value) && $value !== '') {
            $decoded = json_decode($value, true);
            if (is_array($decoded)) {
                return $decoded;
            }
        }
        if (is_array($value)) {
            return $value;
        }
        return [];
    }

    private function getNotificationIssues(int $form_id, \GriffinForms\Items\Form $form, array $email_field_ids): array
    {
        $issues = [];
        $is_v2  = !empty($form->getProp('notifications'));

        if ($is_v2) {
            $map          = \GriffinForms\Includes\Notifications\NotificationChannelRegistry::resolveFormNotifications($form);
            $initial_subs = $map['form_submission']['initial_submission'] ?? [];
            if (!is_array($initial_subs)) {
                $initial_subs = [];
            }

            $all_form_field_ids        = $this->getFormFieldIds($form_id, $form);
            $autoresponder_field_ids   = [];
            $autoresponder_message_ids = [];
            $orphaned_field_ids        = [];
            $has_autoresponder_card    = false;
            $has_any_notification      = false;

            foreach ($initial_subs as $card) {
                if (empty($card['enabled'])) {
                    continue;
                }
                $channel    = isset($card['channel']) ? (string) $card['channel'] : '';
                $recipients = isset($card['recipients']) && is_array($card['recipients']) ? $card['recipients'] : [];

                $has_field_recipient = false;
                $has_valid_recipient = false;
                foreach ($recipients as $r) {
                    $r_type  = isset($r['type'])  ? (string) $r['type']  : '';
                    $r_value = isset($r['value']) ? (string) $r['value'] : '';
                    if ($r_type === 'field' && $r_value !== '' && absint($r_value) > 0) {
                        $field_id = absint($r_value);
                        if (in_array($field_id, $all_form_field_ids, true)) {
                            $has_field_recipient = true;
                            $has_valid_recipient = true;
                            if ($channel === 'email') {
                                $autoresponder_field_ids[] = $field_id;
                            }
                        } else {
                            $orphaned_field_ids[] = $field_id;
                        }
                    } elseif ($r_type === 'static' && $r_value !== '') {
                        $has_valid_recipient = true;
                    }
                }

                if ($has_valid_recipient) {
                    $has_any_notification = true;
                }

                if ($channel === 'email' && $has_valid_recipient) {
                    $has_autoresponder_card = true;
                    $msg_id = isset($card['message_id']) ? absint($card['message_id']) : 0;
                    if ($msg_id > 0) {
                        $autoresponder_message_ids[] = $msg_id;
                    }
                }
            }

            if (!empty($orphaned_field_ids)) {
                $issues[] = $this->issue(
                    'builder_notification_field_missing',
                    'error',
                    __('A notification is mapped to a field that no longer exists. Review your notification recipients.', 'griffinforms-form-builder')
                );
            }

            if (!empty($email_field_ids)) {
                if (!$has_autoresponder_card) {
                    $issues[] = $this->issue(
                        'builder_autoresponder_missing',
                        'warning',
                        __('Email fields are in place, but an autoresponder isn\'t set yet.', 'griffinforms-form-builder')
                    );
                } else {
                    $mapped = array_unique($autoresponder_field_ids);
                    $missing_mappings = array_diff($email_field_ids, $mapped);
                    if (!empty($missing_mappings)) {
                        $issues[] = $this->issue(
                            'builder_autoresponder_partial_mapping',
                            'info',
                            __('Some email fields aren\'t mapped to an autoresponder yet.', 'griffinforms-form-builder')
                        );
                    }
                    $missing_messages = $this->findMissingMessages(array_unique($autoresponder_message_ids));
                    if (!empty($missing_messages)) {
                        $issues[] = $this->issue(
                            'builder_autoresponder_message_missing',
                            'error',
                            __('An autoresponder message is missing. Please update your mappings.', 'griffinforms-form-builder')
                        );
                    }
                }
            }

            if (!$has_any_notification) {
                $issues[] = $this->issue(
                    'builder_no_admin_alert',
                    'warning',
                    __('No admin alert email yet. Add one to get submission notifications.', 'griffinforms-form-builder')
                );
            }

            return $issues;
        }

        // Legacy path: forms not yet saved through v2 modal.
        $autoresponder = $this->parseAutoresponderMappings($form->getProp('autoresponder'));
        if (!empty($email_field_ids) && empty($autoresponder)) {
            $issues[] = $this->issue(
                'builder_autoresponder_missing',
                'warning',
                __('Email fields are in place, but an autoresponder isn\'t set yet.', 'griffinforms-form-builder')
            );
        }

        if (!empty($email_field_ids) && !empty($autoresponder)) {
            $mapped_email_ids = [];
            $message_ids      = [];
            foreach ($autoresponder as $mapping) {
                $email_id = isset($mapping['email_field_id']) ? absint($mapping['email_field_id']) : 0;
                if ($email_id > 0) {
                    $mapped_email_ids[$email_id] = true;
                }
                $msg_id = isset($mapping['message_id']) ? absint($mapping['message_id']) : 0;
                if ($msg_id > 0) {
                    $message_ids[$msg_id] = true;
                }
            }
            $missing_mappings = array_diff($email_field_ids, array_keys($mapped_email_ids));
            if (!empty($missing_mappings)) {
                $issues[] = $this->issue(
                    'builder_autoresponder_partial_mapping',
                    'info',
                    __('Some email fields aren\'t mapped to an autoresponder yet.', 'griffinforms-form-builder')
                );
            }
            $missing_messages = $this->findMissingMessages(array_keys($message_ids));
            if (!empty($missing_messages)) {
                $issues[] = $this->issue(
                    'builder_autoresponder_message_missing',
                    'error',
                    __('An autoresponder message is missing. Please update your mappings.', 'griffinforms-form-builder')
                );
            }
        }

        $alert_emails    = $form->getProp('alert_emails');
        $has_alert_email = false;
        if (is_array($alert_emails)) {
            foreach ($alert_emails as $email => $message_id) {
                if (is_string($email) && $email !== '') {
                    $has_alert_email = true;
                    break;
                }
            }
        }
        if (!$has_alert_email) {
            $issues[] = $this->issue(
                'builder_no_admin_alert',
                'warning',
                __('No admin alert email yet. Add one to get submission notifications.', 'griffinforms-form-builder')
            );
        }

        return $issues;
    }

        private function normalizeMailchimpStoredSettings($stored): array
    {
        if (!is_array($stored)) {
            return [];
        }

        $settings = isset($stored['settings']) && is_array($stored['settings'])
            ? $stored['settings']
            : [];

        return [
            'disable_mailchimp_sync' => !empty($settings['disable_mailchimp_sync']),
            'audience_id' => sanitize_text_field((string) ($settings['audience_id'] ?? '')),
            'subscription_status' => sanitize_key((string) ($settings['subscription_status'] ?? '')),
            'subscriber_email_field' => absint($settings['subscriber_email_field'] ?? 0),
            'first_name_field' => absint($settings['first_name_field'] ?? 0),
            'last_name_field' => absint($settings['last_name_field'] ?? 0),
            'consent_field_id' => absint($settings['consent_field_id'] ?? 0),
            'static_tags' => isset($settings['static_tags']) ? (string) $settings['static_tags'] : '',
        ];
    }

    private function hasMeaningfulMailchimpStoredSettings(string $stored_mode, array $stored_settings): bool
    {
        if ($stored_mode === 'disabled') {
            return true;
        }

        if (!empty($stored_settings['disable_mailchimp_sync'])) {
            return true;
        }

        if (absint($stored_settings['subscriber_email_field'] ?? 0) > 0) {
            return true;
        }

        if (absint($stored_settings['first_name_field'] ?? 0) > 0) {
            return true;
        }

        if (absint($stored_settings['last_name_field'] ?? 0) > 0) {
            return true;
        }

        if (absint($stored_settings['consent_field_id'] ?? 0) > 0) {
            return true;
        }

        if (sanitize_text_field((string) ($stored_settings['audience_id'] ?? '')) !== '') {
            return true;
        }

        if (sanitize_key((string) ($stored_settings['subscription_status'] ?? '')) !== '') {
            return true;
        }

        return trim((string) ($stored_settings['static_tags'] ?? '')) !== '';
    }

    private function normalizeAccountRules($stored): array
    {
        if (!is_array($stored)) {
            return [];
        }

        $settings = isset($stored['settings']) && is_array($stored['settings']) ? $stored['settings'] : [];
        $rules = isset($settings['rules']) && is_array($settings['rules']) ? $settings['rules'] : [];
        $normalized_rules = [];

        foreach ($rules as $index => $rule) {
            if (!is_array($rule)) {
                continue;
            }

            $normalized_rules[] = [
                'id' => isset($rule['id']) ? sanitize_key((string) $rule['id']) : 'wa_rule_' . ($index + 1),
                'email_field_id' => absint($rule['email_field_id'] ?? 0),
                'password_mode' => sanitize_text_field((string) ($rule['password_mode'] ?? 'generate_password')),
                'password_field_id' => absint($rule['password_field_id'] ?? 0),
                'first_name_mode' => sanitize_text_field((string) ($rule['first_name_mode'] ?? ((absint($rule['first_name_field_id'] ?? 0) > 0) ? 'map_field' : 'none'))),
                'first_name_field_id' => absint($rule['first_name_field_id'] ?? 0),
                'last_name_mode' => sanitize_text_field((string) ($rule['last_name_mode'] ?? ((absint($rule['last_name_field_id'] ?? 0) > 0) ? 'map_field' : 'none'))),
                'last_name_field_id' => absint($rule['last_name_field_id'] ?? 0),
                'username_mode' => sanitize_text_field((string) ($rule['username_mode'] ?? ((absint($rule['username_field_id'] ?? 0) > 0) ? 'mapped_or_generate' : 'generate_only'))),
                'username_field_id' => absint($rule['username_field_id'] ?? 0),
                'profile_image_mode' => sanitize_text_field((string) ($rule['profile_image_mode'] ?? ((absint($rule['profile_image_field_id'] ?? 0) > 0) ? 'map_field' : 'none'))),
                'profile_image_field_id' => absint($rule['profile_image_field_id'] ?? 0),
                'send_confirmation_email' => !empty($rule['send_confirmation_email']),
                'confirmation_message_id' => absint($rule['confirmation_message_id'] ?? 0),
                'role' => sanitize_key((string) ($rule['role'] ?? 'subscriber')),
            ];
        }

        return $normalized_rules;
    }

    private function getFieldMap(array $field_ids): array
    {
        $field_ids = array_values(array_filter(array_map('absint', $field_ids)));
        if (empty($field_ids)) {
            return [];
        }

        global $wpdb;
        $config = \GriffinForms\Config::getInstance();
        $field_table = $config->getTable('field');
        $placeholders = implode(',', array_fill(0, count($field_ids), '%d'));
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, field_type, iterations FROM %i WHERE id IN ({$placeholders})",
                $field_table,
                ...$field_ids
            ),
            ARRAY_A
        );

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

        $field_map = [];
        foreach ($rows as $row) {
            $field_id = isset($row['id']) ? absint($row['id']) : 0;
            if ($field_id <= 0) {
                continue;
            }

            $iterations = isset($row['iterations']) ? maybe_unserialize($row['iterations']) : [];
            $is_iterable = is_array($iterations) && !empty($iterations['allow']);

            $field_map[$field_id] = [
                'field_type' => isset($row['field_type']) ? (string) $row['field_type'] : '',
                'is_iterable' => $is_iterable,
            ];
        }

        return $field_map;
    }

    private function fieldMatchesType(array $field_map, int $field_id, array $allowed_types): bool
    {
        if ($field_id <= 0 || !isset($field_map[$field_id])) {
            return false;
        }

        $field_type = (string) ($field_map[$field_id]['field_type'] ?? '');
        return in_array($field_type, $allowed_types, true);
    }

    private function fieldIsIterable(array $field_map, int $field_id): bool
    {
        if ($field_id <= 0 || !isset($field_map[$field_id])) {
            return false;
        }

        return !empty($field_map[$field_id]['is_iterable']);
    }

    private function getAccountMappingFieldIssues(array $rule, string $rule_prefix, string $rule_code_suffix, array $field_map, int $email_field_id): array
    {
        $issues = [];
        $email_field_is_iterable = $this->fieldIsIterable($field_map, $email_field_id);

        $mapping_specs = $this->getAccountMappingSpecs();

        foreach ($mapping_specs as $mapping_spec) {
            $mode = (string) ($rule[$mapping_spec['mode_key']] ?? '');
            $field_id = absint($rule[$mapping_spec['field_key']] ?? 0);
            if ($mode !== 'map_field') {
                continue;
            }

            if (!$this->fieldMatchesType($field_map, $field_id, $mapping_spec['allowed_types'])) {
                $issues[] = $this->issue(
                    $mapping_spec['code'] . $rule_code_suffix,
                    'error',
                    sprintf(__('%1$s maps %2$s from a field that no longer exists.', 'griffinforms-form-builder'), $rule_prefix, $mapping_spec['label'])
                );
                continue;
            }

            if ($email_field_is_iterable && !$this->fieldIsIterable($field_map, $field_id)) {
                if (!empty($mapping_spec['disallow_when_email_iterable'])) {
                    $issues[] = $this->issue(
                        $mapping_spec['iterable_code'] . $rule_code_suffix,
                        'error',
                        sprintf(__('%1$s uses an iterable email field, but profile image mapping is not available for iterable email registrations. Stop mapping profile image.', 'griffinforms-form-builder'), $rule_prefix)
                    );
                } else {
                    $issues[] = $this->issue(
                        $mapping_spec['iterable_code'] . $rule_code_suffix,
                        'error',
                        sprintf(__('%1$s uses an iterable email field, but the mapped %2$s field is not iterable. Choose an iterable source field or stop mapping %2$s.', 'griffinforms-form-builder'), $rule_prefix, strtolower($mapping_spec['label']))
                    );
                }
            }
        }

        return $issues;
    }

    private function getAccountMappingSpecs(): array
    {
        return [
            [
                'mode_key' => 'first_name_mode',
                'field_key' => 'first_name_field_id',
                'label' => __('First Name', 'griffinforms-form-builder'),
                'allowed_types' => ['inputtext', 'inputemail', 'inputurl', 'textarea'],
                'code' => 'accounts_missing_first_name_field_',
                'iterable_code' => 'accounts_iterable_email_requires_iterable_first_name_field_',
            ],
            [
                'mode_key' => 'last_name_mode',
                'field_key' => 'last_name_field_id',
                'label' => __('Last Name', 'griffinforms-form-builder'),
                'allowed_types' => ['inputtext', 'inputemail', 'inputurl', 'textarea'],
                'code' => 'accounts_missing_last_name_field_',
                'iterable_code' => 'accounts_iterable_email_requires_iterable_last_name_field_',
            ],
            [
                'mode_key' => 'profile_image_mode',
                'field_key' => 'profile_image_field_id',
                'label' => __('Profile Image', 'griffinforms-form-builder'),
                'allowed_types' => ['fileupload', 'file_upload'],
                'code' => 'accounts_missing_profile_image_field_',
                'iterable_code' => 'accounts_iterable_email_requires_iterable_profile_image_field_',
                'disallow_when_email_iterable' => true,
            ],
        ];
    }

    private function getRoleMap(): array
    {
        if (!function_exists('wp_roles')) {
            return [];
        }

        $roles = wp_roles();
        if (!is_object($roles) || empty($roles->roles) || !is_array($roles->roles)) {
            return [];
        }

        return $roles->roles;
    }

    private function isAntispamMissing(): bool
    {
        $settings = \GriffinForms\Settings::getInstance();
        $honeypot = (int) $settings->getOption('enable_honeypot', 0);
        $active_provider = sanitize_key((string) $settings->getOption('active_captcha_provider', ''));
        return $honeypot !== 1 && $active_provider === '';
    }

    private function hasActiveCaptchaProtectionForForm(int $form_id): bool
    {
        if ($form_id <= 0) {
            return false;
        }

        $registry = CaptchaRegistry::getInstance();
        if (!$registry) {
            return false;
        }

        $integration = $registry->getActiveIntegration($form_id);
        if (!$integration) {
            return false;
        }

        if (method_exists($integration, 'isEnabledForForm')) {
            return (bool) $integration->isEnabledForForm($form_id);
        }

        if (method_exists($integration, 'isConfigured')) {
            return (bool) $integration->isConfigured();
        }

        return false;
    }

    private function isLastSubmissionStale(int $form_id, \GriffinForms\Items\Form $form): bool
    {
        if ($form_id <= 0) {
            return false;
        }

        $lifetime_count = (int) $form->getProp('lifetime_submission_count');
        if ($lifetime_count <= 0) {
            return false;
        }

        global $wpdb;
        $config = \GriffinForms\Config::getInstance();
        $submission_table = $config->getTable('submission');
        $latest = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT MAX(created) FROM %i WHERE form_id = %d AND status = %d",
                $submission_table,
                $form_id,
                1
            )
        );

        if (empty($latest)) {
            return false;
        }

        $latest_ts = strtotime($latest . ' UTC');
        if ($latest_ts === false) {
            return false;
        }

        $cutoff = current_time('timestamp', true) - WEEK_IN_SECONDS;
        return $latest_ts < $cutoff;
    }

    private function isFormThemeMissing(\GriffinForms\Items\Form $form): bool
    {
        $theme_id = absint($form->getProp('form_theme'));
        if ($theme_id <= 0) {
            return false;
        }

        global $wpdb;
        $config = \GriffinForms\Config::getInstance();
        $theme_table = $config->getTable('formtheme');
        $exists = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM %i WHERE id = %d",
                $theme_table,
                $theme_id
            )
        );

        return empty($exists);
    }

    private function getConditionalLogicIssues(int $form_id, \GriffinForms\Items\Form $form): array
    {
        $issues = [];
        $rules = $form->getProp('conditional_logic');

        if (is_string($rules)) {
            $rules = maybe_unserialize($rules);
        }

        if (!is_array($rules) || empty($rules)) {
            return [];
        }

        $field_ids = $this->getFormFieldIds($form_id, $form);
        $field_id_map = array_fill_keys($field_ids, true);

        foreach ($rules as $index => $rule) {
            if (!is_array($rule)) {
                continue;
            }

            $actions = isset($rule['actions']) ? $rule['actions'] : [];
            if (!is_array($actions) || empty($actions)) {
                $issues[] = $this->issue(
                    'builder_logic_no_actions',
                    'error',
                    __('A conditional logic rule has no actions yet. Add at least one action.', 'griffinforms-form-builder')
                );
            }

            $conditions = [];
            if (isset($rule['all']) && is_array($rule['all'])) {
                $conditions = array_merge($conditions, $rule['all']);
            }
            if (isset($rule['any']) && is_array($rule['any'])) {
                $conditions = array_merge($conditions, $rule['any']);
            }
            $condition_actions = array_merge($conditions, is_array($actions) ? $actions : []);

            foreach ($condition_actions as $entry) {
                if (!is_array($entry)) {
                    continue;
                }
                if (!isset($entry['item'], $entry['item_id'])) {
                    continue;
                }
                if ((string) $entry['item'] !== 'field') {
                    continue;
                }
                $field_id = absint($entry['item_id']);
                if ($field_id > 0 && !isset($field_id_map[$field_id])) {
                    $issues[] = $this->issue(
                        'builder_logic_missing_field',
                        'error',
                        __('A conditional logic rule references a field that no longer exists. Please update the rule.', 'griffinforms-form-builder')
                    );
                    break;
                }
            }
        }

        return $issues;
    }

    private function hasRecentFormErrors(int $form_id, int $days): bool
    {
        if ($form_id <= 0 || $days <= 0) {
            return false;
        }

        global $wpdb;
        $config = \GriffinForms\Config::getInstance();
        $log_table = $config->getTable('log');
        $since = gmdate('Y-m-d H:i:s', current_time('timestamp', true) - DAY_IN_SECONDS * $days);

        $count = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM %i WHERE item_type = %s AND item_id = %d AND log_type = %s AND date_time >= %s",
                $log_table,
                'form',
                $form_id,
                'error',
                $since
            )
        );

        return (int) $count > 0;
    }

    private function hasRecentMailchimpErrors(int $form_id, int $days): bool
    {
        if ($form_id <= 0 || $days <= 0) {
            return false;
        }

        global $wpdb;
        $config = \GriffinForms\Config::getInstance();
        $log_table = $config->getTable('log');
        $submission_table = $config->getTable('submission');
        $since = gmdate('Y-m-d H:i:s', current_time('timestamp', true) - DAY_IN_SECONDS * $days);
        $category = \GriffinForms\Log::getInstance()->normalizeCategoryPublic('Integration (mailchimp)');

        $form_error_count = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$log_table} WHERE item_type = %s AND item_id = %d AND log_type = %s AND category = %s AND date_time >= %s",
                'form',
                $form_id,
                'error',
                $category,
                $since
            )
        );
        if ((int) $form_error_count > 0) {
            return true;
        }

        $submission_error_count = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$log_table} l INNER JOIN {$submission_table} s ON s.id = l.item_id WHERE l.item_type = %s AND l.log_type = %s AND l.category = %s AND l.date_time >= %s AND s.form_id = %d",
                'submission',
                'error',
                $category,
                $since,
                $form_id
            )
        );

        return (int) $submission_error_count > 0;
    }

    private function getLayoutStructureStats(\GriffinForms\Items\Form $form): array
    {
        $page_ids = $form->getProp('formpages');
        if (!is_array($page_ids) || empty($page_ids)) {
            return [
                'pages_without_rows' => 0,
                'rows_without_columns' => 0,
                'columns_without_fields' => 0,
            ];
        }

        $page_ids = array_values(array_filter(array_map('absint', $page_ids)));
        if (empty($page_ids)) {
            return [
                'pages_without_rows' => 0,
                'rows_without_columns' => 0,
                'columns_without_fields' => 0,
            ];
        }

        global $wpdb;
        $config = \GriffinForms\Config::getInstance();
        $page_table = $config->getTable('formpage');
        $row_table = $config->getTable('pagerow');
        $column_table = $config->getTable('rowcolumn');

        $page_placeholders = implode(',', array_fill(0, count($page_ids), '%d'));
        $pages = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, pagerows FROM %i WHERE id IN ({$page_placeholders})",
                $page_table,
                ...$page_ids
            ),
            ARRAY_A
        );

        $pages_without_rows = 0;
        $row_ids = [];
        if (is_array($pages)) {
            foreach ($pages as $page) {
                $pagerows = isset($page['pagerows']) ? maybe_unserialize($page['pagerows']) : [];
                if (!is_array($pagerows) || empty($pagerows)) {
                    $pages_without_rows++;
                    continue;
                }
                foreach ($pagerows as $row_id) {
                    $row_id = absint($row_id);
                    if ($row_id > 0) {
                        $row_ids[$row_id] = true;
                    }
                }
            }
        }

        $rows_without_columns = 0;
        $column_ids = [];
        if (!empty($row_ids)) {
            $row_ids = array_keys($row_ids);
            $row_placeholders = implode(',', array_fill(0, count($row_ids), '%d'));
            $rows = $wpdb->get_results(
                $wpdb->prepare(
                    "SELECT id, rowcolumns FROM %i WHERE id IN ({$row_placeholders})",
                    $row_table,
                    ...$row_ids
                ),
                ARRAY_A
            );

            if (is_array($rows)) {
                foreach ($rows as $row) {
                    $rowcolumns = isset($row['rowcolumns']) ? maybe_unserialize($row['rowcolumns']) : [];
                    if (!is_array($rowcolumns) || empty($rowcolumns)) {
                        $rows_without_columns++;
                        continue;
                    }
                    foreach ($rowcolumns as $column_id) {
                        $column_id = absint($column_id);
                        if ($column_id > 0) {
                            $column_ids[$column_id] = true;
                        }
                    }
                }
            }
        }

        $columns_without_fields = 0;
        if (!empty($column_ids)) {
            $column_ids = array_keys($column_ids);
            $column_placeholders = implode(',', array_fill(0, count($column_ids), '%d'));
            $columns = $wpdb->get_results(
                $wpdb->prepare(
                    "SELECT fields FROM %i WHERE id IN ({$column_placeholders})",
                    $column_table,
                    ...$column_ids
                ),
                ARRAY_A
            );

            if (is_array($columns)) {
                foreach ($columns as $column) {
                    $fields = isset($column['fields']) ? maybe_unserialize($column['fields']) : [];
                    if (!is_array($fields) || empty($fields)) {
                        $columns_without_fields++;
                    }
                }
            }
        }

        return [
            'pages_without_rows' => $pages_without_rows,
            'rows_without_columns' => $rows_without_columns,
            'columns_without_fields' => $columns_without_fields,
        ];
    }

    private function findMissingMessages(array $message_ids): array
    {
        if (empty($message_ids)) {
            return [];
        }

        $message_ids = array_values(array_filter(array_map('absint', $message_ids)));
        if (empty($message_ids)) {
            return [];
        }

        global $wpdb;
        $config = \GriffinForms\Config::getInstance();
        $message_table = $config->getTable('message');
        $placeholders = implode(',', array_fill(0, count($message_ids), '%d'));
        $rows = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT id FROM %i WHERE id IN ({$placeholders})",
                $message_table,
                ...$message_ids
            )
        );

        if (!is_array($rows)) {
            return $message_ids;
        }

        $existing = array_map('absint', $rows);
        return array_values(array_diff($message_ids, $existing));
    }

    private function tableHasColumn(string $table, string $column): bool
    {
        static $cache = array();
        $key = $table . ':' . $column;
        if (array_key_exists($key, $cache)) {
            return $cache[$key];
        }
        global $wpdb;
        $exists = $wpdb->get_var($wpdb->prepare(
            "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = %s AND COLUMN_NAME = %s",
            $table,
            $column
        ));
        $cache[$key] = !empty($exists);
        return $cache[$key];
    }

    private function issue(string $code, string $level, string $message): array
    {
        return [
            'code' => $code,
            'level' => $level,
            'message' => $message,
        ];
    }

    /**
     * Return a whitelist of HTML tags/attrs allowed in overlay rendering.
     *
     * @return array<string,array<int,string>>
     */
    public static function getAllowedHtml(): array
    {
        return [
            'a' => [
                'href',
                'target',
                'rel',
                'class',
            ],
            'strong' => [],
            'em' => [],
            'span' => ['class'],
            'code' => ['class'],
            'br' => [],
        ];
    }
}
