<?php
/**
 * The admin-specific functionality of the plugin.
 *
 * @link       https://github.com/sitestaffr/sitestaffr-plugin
 * @since      1.0.0
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/admin
 */

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

require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-billing.php';
require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-ban-ajax.php';
require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-call-ajax.php';
if ( file_exists( SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-diagnostics-ajax.php' ) ) {
	require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-diagnostics-ajax.php';
} elseif ( ! trait_exists( 'SiteStaffr_Admin_Diagnostics_Ajax_Trait' ) ) {
	// Stub trait when diagnostics file is excluded from distribution.
	trait SiteStaffr_Admin_Diagnostics_Ajax_Trait {}
}
require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-description-generation-ajax.php';
require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-business-settings-ajax.php';
require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-notification-ajax.php';
require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-usage-ajax.php';
require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-widget-settings-ajax.php';
require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-report-issue-ajax.php';
require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-knowledge-ajax.php';
require_once SITESTAFFR_PLUGIN_DIR . 'admin/traits/trait-sitestaffr-admin-agent-ajax.php';
require_once SITESTAFFR_PLUGIN_DIR . 'admin/class-sitestaffr-admin-view-query-service.php';


/**
 * The admin-specific functionality of the plugin.
 *
 * Defines the plugin name, version, and hooks for enqueuing
 * the admin-specific stylesheet and JavaScript.
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/admin
 * @author     SiteStaffr
 */
class SiteStaffr_Admin {


    use SiteStaffr_Admin_Billing_Trait;
    use SiteStaffr_Admin_Ban_Ajax_Trait;
    use SiteStaffr_Admin_Call_Ajax_Trait;
    use SiteStaffr_Admin_Diagnostics_Ajax_Trait;
    use SiteStaffr_Admin_Description_Generation_Ajax_Trait;
    use SiteStaffr_Admin_Business_Settings_Ajax_Trait;
    use SiteStaffr_Admin_Notification_Ajax_Trait;
    use SiteStaffr_Admin_Usage_Ajax_Trait;
    use SiteStaffr_Admin_Widget_Settings_Ajax_Trait;
    use SiteStaffr_Admin_Report_Issue_Ajax_Trait;
    use SiteStaffr_Admin_Knowledge_Ajax_Trait;
    use SiteStaffr_Admin_Agent_Ajax_Trait;

    /**
     * Maximum allowed length for business description.
     *
     * @since 0.14.151
     */
    const BUSINESS_DESCRIPTION_MAX_LENGTH = 5000;
    const CUSTOM_GREETING_MAX_LENGTH = 250;
    const WEBSITE_SCAN_MAX_REQUESTS = 5;
    const CUSTOM_GREETING_TONE_DEFAULT = 'warm_neutral';

    /**
     * The ID of this plugin.
     *
     * @since    1.0.0
     * @access   private
     * @var      string    $plugin_name    The ID of this plugin.
     */
    private $plugin_name;

    /**
     * The version of this plugin.
     *
     * @since    1.0.0
     * @access   private
     * @var      string    $version    The current version of this plugin.
     */
    private $version;

    /**
     * Guard to avoid repeated cache purges within the same request.
     *
     * @since 0.14.267
     * @var bool
     */
    private $frontend_cache_purged = false;

    /**
     * Initialize the class and set its properties.
     *
     * @since    1.0.0
     * @param    string    $plugin_name       The name of this plugin.
     * @param    string    $version    The version of this plugin.
     */
    public function __construct( $plugin_name, $version ) {
        $this->plugin_name = $plugin_name;
        $this->version = $version;

        // Register non-loader AJAX handlers directly in constructor.
        // Core overlapping hooks are intentionally registered by the loader in includes/class-sitestaffr.php.
        $this->register_ajax_handlers_direct();

        // AJAX: dismiss low balance alert (stores user meta tied to billing cycle).
        add_action( 'wp_ajax_sitestaffr_dismiss_low_balance_alert', array( $this, 'ajax_dismiss_low_balance_alert' ) );

        // Billing actions (Usage page): create checkout + portal sessions.
        add_action( 'admin_post_sitestaffr_billing_checkout', array( $this, 'handle_billing_checkout_action' ) );
        add_action( 'admin_post_sitestaffr_billing_portal', array( $this, 'handle_billing_portal_action' ) );
        add_action( 'admin_post_sitestaffr_cancel_downgrade', array( $this, 'handle_cancel_downgrade_action' ) );
        add_action( 'admin_post_sitestaffr_refresh_billing_cache', array( $this, 'handle_refresh_billing_cache_action' ) );

        // Fix admin menu highlighting for Conversations and Conversation Details pages.
        add_filter( 'parent_file', array( $this, 'fix_admin_menu_highlight' ) );

        // Keep frontend output in sync on cached sites after setup/widget changes.
        add_action( 'update_option_sitestaffr_settings', array( $this, 'maybe_purge_frontend_cache_after_settings_update' ), 10, 3 );
        add_action( 'update_option_sitestaffr_widget_settings', array( $this, 'maybe_purge_frontend_cache_after_widget_settings_update' ), 10, 3 );
        add_action( 'update_option_sitestaffr_setup_completed', array( $this, 'maybe_purge_frontend_cache_after_setup_flag_update' ), 10, 3 );
    }

    /**
     * Fix admin menu highlighting for pages routed through the Dashboard slug.
     *
     * When viewing Conversations (view=all) or Conversation Details (action=view),
     * the URL uses ?page=sitestaffr which causes WordPress to highlight "Dashboard"
     * instead of "Conversations" in the admin nav. This filter corrects that.
     *
     * @since 0.14.83
     *
     * @param string $parent_file The parent file slug.
     * @return string Corrected parent file slug.
     */
    public function fix_admin_menu_highlight( $parent_file ) {
        global $plugin_page, $submenu_file;

        if ( 'sitestaffr' === $plugin_page ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only menu highlighting state.
            $view   = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : '';
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only menu highlighting state.
            $action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : '';

            if ( 'all' === $view || 'view' === $action ) {
                $submenu_file = 'sitestaffr-view-calls';
                $parent_file  = 'sitestaffr';
            }
        }

        return $parent_file;
    }

    /**
     * Hide setup wizard submenu item while keeping the page registered.
     *
     * We remove the menu node late (admin_head) so WordPress capability checks
    /**
     * Add SiteStaffr quick links to the top WordPress admin bar.
     *
     * @since 0.14.286
     * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
     * @return void
     */
    public function add_admin_bar_menu( $wp_admin_bar ) {
        if ( ! is_admin_bar_showing() || ! $wp_admin_bar || ! method_exists( $wp_admin_bar, 'add_node' ) ) {
            return;
        }

        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        $new_calls_count = 0;
        if ( class_exists( 'SiteStaffr_Call_Manager' ) && method_exists( 'SiteStaffr_Call_Manager', 'get_new_calls_count' ) ) {
            $new_calls_count = (int) SiteStaffr_Call_Manager::get_new_calls_count();
        }

        $has_new_messages = $new_calls_count > 0;
        $logo_path = plugin_dir_path( __DIR__ ) . 'assets/images/menu-icon.svg';
        $logo_url  = esc_url( SITESTAFFR_PLUGIN_URL . 'assets/images/menu-icon.svg?v=' . SITESTAFFR_VERSION );

        $logo_inner = '<span style="display:inline-block;font-weight:700;color:#fff;font-size:13px;line-height:18px;">S</span>';
        if ( file_exists( $logo_path ) ) {
            $logo_inner = '<img src="' . $logo_url . '" alt="" style="width:18px;height:18px;display:block;" />';
        }

        $root_title = '<span style="display:inline-flex;align-items:center;justify-content:center;height:32px;line-height:32px;"><span style="position:relative;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;">' . $logo_inner;
        if ( $has_new_messages ) {
            $root_title .= '<span style="position:absolute;top:-2px;right:-3px;width:8px;height:8px;border-radius:50%;background:#d63638;border:2px solid #1d2327;"></span>';
        }
        $root_title .= '</span></span><span class="screen-reader-text">' . esc_html__( 'SiteStaffr', 'sitestaffr' ) . '</span>';

        $conversations_title = __( 'Conversations', 'sitestaffr' );
        if ( $has_new_messages ) {
            $conversations_title .= ' <span style="display:inline-grid;place-items:center;width:18px;height:18px;margin-left:6px;border-radius:50%;background:#d63638;color:#fff;font-size:11px;font-weight:700;line-height:1;">' . esc_html( number_format_i18n( $new_calls_count ) ) . '</span>';
        }

        $wp_admin_bar->add_node(
            array(
                'id'    => 'sitestaffr-toolbar',
                'title' => $root_title,
                'href'  => admin_url( 'admin.php?page=sitestaffr' ),
            )
        );

        $wp_admin_bar->add_node(
            array(
                'id'     => 'sitestaffr-toolbar-conversations',
                'parent' => 'sitestaffr-toolbar',
                'title'  => $conversations_title,
                'href'   => admin_url( 'admin.php?page=sitestaffr-view-calls' ),
            )
        );

        $wp_admin_bar->add_node(
            array(
                'id'     => 'sitestaffr-toolbar-usage',
                'parent' => 'sitestaffr-toolbar',
                'title'  => __( 'Usage & Billing', 'sitestaffr' ),
                'href'   => admin_url( 'admin.php?page=sitestaffr-usage' ),
            )
        );
    }

    /**
     * Get a setting value from SiteStaffr options.
     *
     * Helper method to reduce code duplication when retrieving settings.
     * Provides a clean interface for accessing settings with default values.
     *
     * @since 0.13.40
     * @access private
     *
     * @param string $key         Setting key to retrieve.
     * @param string $option_name Option name (default: 'sitestaffr_settings').
     * @param mixed  $default     Default value if setting not found.
     *
     * @return mixed Setting value or default.
     */
    private function get_setting( $key, $option_name = 'sitestaffr_settings', $default = '' ) {
        $settings = get_option( $option_name, array() );
        return isset( $settings[ $key ] ) ? $settings[ $key ] : $default;
    }

    /**
     * Purge known page/object caches after frontend-visible config changes.
     *
     * This prevents stale full-page cache from hiding the floating widget right
     * after setup completes or widget display settings are updated.
     *
     * @since 0.14.267
     * @param string $reason Context for logs.
     * @return void
     */
    private function purge_frontend_caches( $reason = '' ) {
        if ( $this->frontend_cache_purged ) {
            return;
        }

        $this->frontend_cache_purged = true;
        $providers = array();

        // LiteSpeed Cache (API + action hook).
        if ( class_exists( 'LiteSpeed_Cache_API' ) && method_exists( 'LiteSpeed_Cache_API', 'purge_all' ) ) {
            LiteSpeed_Cache_API::purge_all();
            $providers[] = 'litespeed_api';
        }
		if ( has_action( 'litespeed_purge_all' ) ) {
			do_action( 'litespeed_purge_all' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Third-party cache plugin hook.
			$providers[] = 'litespeed_action';
		}

        // WP Rocket.
        if ( function_exists( 'rocket_clean_domain' ) ) {
            rocket_clean_domain();
            $providers[] = 'wp_rocket';
        }

        // W3 Total Cache.
        if ( function_exists( 'w3tc_flush_all' ) ) {
            w3tc_flush_all();
            $providers[] = 'w3tc';
        }

        // WP Super Cache.
        if ( function_exists( 'wp_cache_clear_cache' ) ) {
            wp_cache_clear_cache();
            $providers[] = 'wp_super_cache';
        }

        // SiteGround Optimizer.
		if ( has_action( 'sg_cachepress_purge_cache' ) ) {
			do_action( 'sg_cachepress_purge_cache' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Third-party cache plugin hook.
			$providers[] = 'siteground';
		}

        // Generic object cache flush.
        if ( function_exists( 'wp_cache_flush' ) ) {
            wp_cache_flush();
            $providers[] = 'object_cache';
        }

        SiteStaffr_Logger::legacy(
            sprintf(
                'SiteStaffr: Frontend cache purge requested (%s) via: %s',
                $reason ? $reason : 'unspecified',
                empty( $providers ) ? 'none_detected' : implode( ',', $providers )
            )
        );
    }

    /**
     * Purge caches when setup transitions to complete.
     *
     * @since 0.14.267
     * @param mixed  $old_value Previous option value.
     * @param mixed  $new_value New option value.
     * @param string $option Option name.
     * @return void
     */
    public function maybe_purge_frontend_cache_after_settings_update( $old_value, $new_value, $option ) {
        if ( 'sitestaffr_settings' !== $option ) {
            return;
        }

        $old_setup_complete = is_array( $old_value ) && ! empty( $old_value['setup_complete'] );
        $new_setup_complete = is_array( $new_value ) && ! empty( $new_value['setup_complete'] );

        if ( ! $old_setup_complete && $new_setup_complete ) {
            $this->purge_frontend_caches( 'setup_complete_transition' );
        }
    }

    /**
     * Purge caches when fixed/widget settings change.
     *
     * @since 0.14.267
     * @param mixed  $old_value Previous option value.
     * @param mixed  $new_value New option value.
     * @param string $option Option name.
     * @return void
     */
    public function maybe_purge_frontend_cache_after_widget_settings_update( $old_value, $new_value, $option ) {
        if ( 'sitestaffr_widget_settings' !== $option ) {
            return;
        }

        if ( wp_json_encode( $old_value ) !== wp_json_encode( $new_value ) ) {
            $this->purge_frontend_caches( 'widget_settings_changed' );
        }
    }

    /**
     * Purge caches when standalone setup completion flag flips true.
     *
     * @since 0.14.267
     * @param mixed  $old_value Previous option value.
     * @param mixed  $new_value New option value.
     * @param string $option Option name.
     * @return void
     */
    public function maybe_purge_frontend_cache_after_setup_flag_update( $old_value, $new_value, $option ) {
        if ( 'sitestaffr_setup_completed' !== $option ) {
            return;
        }

        if ( ! $old_value && ! empty( $new_value ) ) {
            $this->purge_frontend_caches( 'setup_completed_option' );
        }
    }

    /**
     * Resolve current subscription plan from billing cache or legacy option.
     *
     * @since 0.14.210
     * @return string
     */
    public function get_current_plan_key() {
        $plan_key = '';

        if ( class_exists( 'SiteStaffr_Billing_State' ) ) {
            $billing_cache = SiteStaffr_Billing_State::get_cache();
            $plan_key = isset( $billing_cache['plan_name'] ) ? sanitize_key( $billing_cache['plan_name'] ) : '';
        }

        if ( empty( $plan_key ) ) {
            $subscription = get_option( 'sitestaffr_subscription', array() );
            if ( is_array( $subscription ) ) {
                $plan_key = isset( $subscription['plan_name'] ) ? sanitize_key( $subscription['plan_name'] ) : '';
            }
        }

        if ( empty( $plan_key ) ) {
            $plan_key = 'trial';
        }

        return $plan_key;
    }

    /**
     * Get maximum description generations allowed for a plan tier.
     *
     * @since 1.1.0
     * @param string $plan_name The plan key (trial, starter, business, pro).
     * @return int
     */
    public function get_max_generations_for_plan( $plan_name ) {
        return SITESTAFFR_DAILY_GEN_LIMIT;
    }

    /**
     * Voice IDs allowed per billing plan.
     *
     * Trial unlocks all voices as a teaser. Paid tiers are progressively gated.
     *
     * @since 0.14.267
     * @var array<string, string[]>
     */
    private static $voice_plan_map = array(
        'trial'    => array( 'marin', 'cedar' ),
        'starter'  => array( 'marin', 'cedar' ),
        'business' => array( 'marin', 'cedar', 'sage', 'coral', 'ash' ),
        'pro'      => array( 'marin', 'cedar', 'sage', 'coral', 'ash', 'alloy', 'echo', 'shimmer', 'verse', 'ballad' ),
    );

    /**
     * Maximum custom instructions per billing plan.
     *
     * @since 1.20.0
     * @var array<string, int>
     */
    private static $custom_instructions_plan_map = array(
        'trial'    => 0,
        'starter'  => 0,
        'business' => 2,
        'pro'      => 5,
    );

    /**
     * Get allowed voice IDs for a billing plan.
     *
     * @since 0.14.267
     * @param string $plan_key Billing plan key.
     * @return string[] Allowed voice IDs.
     */
    public static function get_voices_for_plan( $plan_key ) {
        $plan_key = sanitize_key( $plan_key );
        if ( isset( self::$voice_plan_map[ $plan_key ] ) ) {
            return self::$voice_plan_map[ $plan_key ];
        }
        // Unknown plan defaults to starter (most restrictive paid tier).
        return self::$voice_plan_map['starter'];
    }

    /**
     * Get the minimum plan required to use a voice.
     *
     * @since 0.14.267
     * @param string $voice_id Voice identifier.
     * @return string Plan key ('starter', 'business', or 'pro').
     */
    public static function get_required_plan_for_voice( $voice_id ) {
        // Check tiers from most restrictive to least.
        if ( in_array( $voice_id, self::$voice_plan_map['starter'], true ) ) {
            return 'starter';
        }
        if ( in_array( $voice_id, self::$voice_plan_map['business'], true ) ) {
            return 'business';
        }
        return 'pro';
    }

    /**
     * Get voice metadata for all voices.
     *
     * Shared between render_ai_voice_selector() and enqueue_scripts().
     *
     * @since 1.2.3
     * @return array Keyed by voice ID.
     */
    public static function get_voice_metadata() {
        return array(
            'marin' => array(
                'name'     => 'Marin',
                'category' => 'recommended',
                'tagline'  => 'Warm & Welcoming',
                'description' => 'Makes your visitors feel right at home with a friendly, inviting tone that puts people at ease. Perfect for service businesses that prioritize customer comfort.',
                'best_for' => 'Home Services|Hospitality|Customer Support',
                'avatar'   => 'receptionists/marin/marin_avatar.png',
            ),
            'cedar' => array(
                'name'     => 'Cedar',
                'category' => 'recommended',
                'tagline'  => 'Smooth & Natural',
                'description' => 'Brings a calm, professional presence to every interaction. Ideal for businesses that want to project reliability and trustworthiness.',
                'best_for' => 'Professional Services|Local Agencies|Legal Offices',
                'avatar'   => 'receptionists/cedar/cedar_avatar.png',
            ),
            'sage' => array(
                'name'     => 'Sage',
                'category' => 'standard',
                'tagline'  => 'Wise & Thoughtful',
                'description' => 'A measured, trustworthy tone that conveys expertise. Great for financial services and consulting.',
                'best_for' => 'Financial Advisors|Accounting Firms|Coaching Services',
                'avatar'   => 'receptionists/sage/sage_avatar.png',
            ),
            'coral' => array(
                'name'     => 'Coral',
                'category' => 'standard',
                'tagline'  => 'Bright & Cheerful',
                'description' => 'Brings energy and positivity to customer interactions. Perfect for retail, hospitality, and customer-focused businesses.',
                'best_for' => 'Retail|Salons|Restaurants',
                'avatar'   => 'receptionists/coral/coral_avatar.png',
            ),
            'ash' => array(
                'name'     => 'Ash',
                'category' => 'standard',
                'tagline'  => 'Clear & Confident',
                'description' => 'Projects authority and competence. Great for professional services, consulting, and B2B businesses.',
                'best_for' => 'B2B Services|Consulting Shops|Marketing Agencies',
                'avatar'   => 'receptionists/ash/ash_avatar.png',
            ),
            'alloy' => array(
                'name'     => 'Alloy',
                'category' => 'standard',
                'tagline'  => 'Neutral & Professional',
                'description' => 'A balanced, versatile voice suitable for any business type. Clear and easy to understand with broad appeal.',
                'best_for' => 'General SMB|Front Desk Coverage|Mixed Inquiries',
                'avatar'   => 'receptionists/alloy/alloy_avatar.png',
            ),
            'echo' => array(
                'name'     => 'Echo',
                'category' => 'standard',
                'tagline'  => 'Calm & Reassuring',
                'description' => 'A gentle, soothing presence that helps visitors feel heard and supported. Ideal for healthcare and support services.',
                'best_for' => 'Clinics|Wellness Practices|Care Teams',
                'avatar'   => 'receptionists/echo/echo_avatar.png',
            ),
            'shimmer' => array(
                'name'     => 'Shimmer',
                'category' => 'standard',
                'tagline'  => 'Light & Elegant',
                'description' => 'Refined and sophisticated presence. Perfect for luxury brands, spas, and premium services.',
                'best_for' => 'Spas|Boutique Brands|Premium Services',
                'avatar'   => 'receptionists/shimmer/shimmer_avatar.png',
            ),
            'verse' => array(
                'name'     => 'Verse',
                'category' => 'expressive',
                'tagline'  => 'Poetic & Refined',
                'description' => 'Cultured and articulate with artistic sensibility. Perfect for galleries, museums, and cultural institutions.',
                'best_for' => 'Art Galleries|Cultural Venues|Education Programs',
                'avatar'   => 'receptionists/verse/verse_avatar.png',
            ),
            'ballad' => array(
                'name'     => 'Ballad',
                'category' => 'expressive',
                'tagline'  => 'Expressive & Melodic',
                'description' => 'Brings artistry and emotional depth to conversations. Ideal for creative industries and entertainment.',
                'best_for' => 'Creative Studios|Event Services|Entertainment SMBs',
                'avatar'   => 'receptionists/ballad/ballad_avatar.png',
            ),
        );
    }

    /**
     * Determine if custom greeting is enabled for the current plan.
     *
     * @since 0.14.210
     * @param string $plan_key Billing plan key.
     * @return bool
     */
    public function is_custom_greeting_allowed_for_plan( $plan_key ) {
        return in_array( sanitize_key( $plan_key ), array( 'pro' ), true );
    }

    /**
     * Get the maximum number of custom instructions for a plan.
     *
     * @since 1.20.0
     * @param string $plan_key Billing plan key.
     * @return int
     */
    public static function get_custom_instructions_limit( $plan_key ) {
        $plan_key = sanitize_key( $plan_key );
        if ( isset( self::$custom_instructions_plan_map[ $plan_key ] ) ) {
            return self::$custom_instructions_plan_map[ $plan_key ];
        }
        return 0;
    }

    /**
     * Validate business hours format.
     *
     * Checks if business hours string is valid and not corrupt.
     * Prevents corrupt data like "Minutes seconds" from being saved to database.
     *
     * @since 0.8.4
     * @access private
     *
     * @param string $hours Business hours string to validate.
     *
     * @return array Validation result: array('valid' => bool, 'error' => string|null, 'sanitized' => string)
     */
    private function validate_business_hours( $hours ) {
        // Empty is allowed (not required field)
        if ( empty( $hours ) ) {
            return array( 'valid' => true, 'error' => null, 'sanitized' => '' );
        }

        $hours_trimmed = trim( $hours );
        $hours_lower = strtolower( $hours_trimmed );

        // Check for corruption patterns that should NEVER be saved
        $corrupt_patterns = array( 'minutes seconds', 'minutes', 'seconds' );
        foreach ( $corrupt_patterns as $pattern ) {
            if ( $hours_lower === $pattern ) {
                SiteStaffr_Logger::legacy( 'SiteStaffr: REJECTED corrupt business hours: "' . $hours . '"' );
                return array(
                    'valid' => false,
                    'error' => __( 'Invalid business hours format detected. Please select valid hours or use a preset option.', 'sitestaffr' ),
                    'sanitized' => '',
                );
            }
        }

        // Check minimum length (too short = likely corrupt)
        if ( strlen( $hours_trimmed ) < 3 ) {
            SiteStaffr_Logger::legacy( 'SiteStaffr: REJECTED suspiciously short business hours: "' . $hours . '"' );
            return array(
                'valid' => false,
                'error' => __( 'Business hours are too short. Please provide valid hours or leave blank.', 'sitestaffr' ),
                'sanitized' => '',
            );
        }

        // Passed all validation checks
        return array( 'valid' => true, 'error' => null, 'sanitized' => $hours_trimmed );
    }

    /**
     * Sanitize and enforce maximum length for business description fields.
     *
     * @since 0.14.151
     *
     * @param string $description Raw description text.
     * @return array{value:string,truncated:bool}
     */
    private function sanitize_business_description( $description ) {
        $sanitized = sanitize_textarea_field( $description );
        $truncated = false;

        if ( strlen( $sanitized ) > self::BUSINESS_DESCRIPTION_MAX_LENGTH ) {
            $sanitized = substr( $sanitized, 0, self::BUSINESS_DESCRIPTION_MAX_LENGTH );
            $truncated = true;
        }

        return array(
            'value'     => $sanitized,
            'truncated' => $truncated,
        );
    }

    /**
     * Sanitize custom greeting input (single-line plain text, emoji-free) with max length.
     *
     * @since 0.14.210
     * @param string $greeting Raw greeting text.
     * @return array{value:string,truncated:bool}
     */
    private function sanitize_custom_greeting( $greeting ) {
        $sanitized = sanitize_text_field( (string) $greeting );
        $sanitized = preg_replace( '/[\r\n\t]+/u', ' ', $sanitized );
        $sanitized = preg_replace( '/\s+/u', ' ', $sanitized );

        if ( is_string( $sanitized ) ) {
            $emoji_stripped = preg_replace( '/[\p{So}]+/u', '', $sanitized );
            if ( is_string( $emoji_stripped ) ) {
                $sanitized = $emoji_stripped;
            }
        }

        $sanitized = trim( is_string( $sanitized ) ? $sanitized : '' );
        $truncated = false;

        if ( $this->utf8_strlen( $sanitized ) > self::CUSTOM_GREETING_MAX_LENGTH ) {
            $sanitized = $this->utf8_substr( $sanitized, 0, self::CUSTOM_GREETING_MAX_LENGTH );
            $truncated = true;
        }

        return array(
            'value'     => $sanitized,
            'truncated' => $truncated,
        );
    }

    /**
     * UTF-8 safe string length with graceful fallbacks.
     *
     * @since 0.14.218
     * @param string $value String to measure.
     * @return int
     */
    private function utf8_strlen( $value ) {
        if ( function_exists( 'mb_strlen' ) ) {
            return (int) mb_strlen( $value, 'UTF-8' );
        }

        if ( function_exists( 'iconv_strlen' ) ) {
            $length = iconv_strlen( $value, 'UTF-8' );
            if ( false !== $length ) {
                return (int) $length;
            }
        }

        if ( preg_match_all( '/./u', $value, $matches ) ) {
            return count( $matches[0] );
        }

        return strlen( $value );
    }

    /**
     * UTF-8 safe substring with graceful fallbacks.
     *
     * @since 0.14.218
     * @param string $value  Input string.
     * @param int    $start  Start offset.
     * @param int    $length Max character length.
     * @return string
     */
    private function utf8_substr( $value, $start, $length ) {
        if ( function_exists( 'mb_substr' ) ) {
            return (string) mb_substr( $value, $start, $length, 'UTF-8' );
        }

        if ( function_exists( 'iconv_substr' ) ) {
            $slice = iconv_substr( $value, $start, $length, 'UTF-8' );
            if ( false !== $slice ) {
                return (string) $slice;
            }
        }

        $chars = preg_split( '//u', $value, -1, PREG_SPLIT_NO_EMPTY );
        if ( is_array( $chars ) ) {
            return implode( '', array_slice( $chars, $start, $length ) );
        }

        return substr( $value, $start, $length );
    }

    /**
     * Return allowed tone keys for custom greeting delivery.
     *
     * @since 0.14.211
     * @return array<string,string>
     */
    private function get_custom_greeting_tone_options() {
        return array(
            'warm_neutral'      => __( 'Warm & Neutral', 'sitestaffr' ),
            'upbeat_friendly'   => __( 'Upbeat & Friendly', 'sitestaffr' ),
            'calm_professional' => __( 'Serious & Professional', 'sitestaffr' ),
            'serious_respectful'=> __( 'Compassionate & Empathetic', 'sitestaffr' ),
        );
    }

    /**
     * Sanitize custom greeting tone to an allowed option.
     *
     * @since 0.14.211
     * @param string $tone Raw tone key.
     * @return string
     */
    private function sanitize_custom_greeting_tone( $tone ) {
        $tone_key = sanitize_key( (string) $tone );
        $options = $this->get_custom_greeting_tone_options();
        return isset( $options[ $tone_key ] ) ? $tone_key : self::CUSTOM_GREETING_TONE_DEFAULT;
    }

    /**
     * Return inline JS for the knowledge mode radio toggle.
     *
     * @since 1.19.28
     * @return string
     */
    private function get_knowledge_mode_inline_js() {
        return <<<'JS'
(function() {
    var radios = document.querySelectorAll('input[name="sitestaffr_knowledge_mode"]');
    var status = document.getElementById('knowledge-mode-status');
    if (!radios.length || !status) return;
    var previousValue = document.querySelector('input[name="sitestaffr_knowledge_mode"]:checked');
    previousValue = previousValue ? previousValue.value : 'search';

    radios.forEach(function(radio) {
        radio.addEventListener('change', function() {
            var selected = this;
            status.textContent = 'Saving...';
            status.style.color = '#666';

            var data = new FormData();
            data.append('action', 'sitestaffr_save_knowledge_mode');
            data.append('nonce', sitestaffr_knowledge.knowledge_mode_nonce);
            data.append('knowledge_mode', selected.value);
            fetch(sitestaffr_knowledge.ajax_url, { method: 'POST', body: data })
                .then(function(r) { return r.json(); })
                .then(function(d) {
                    if (d.success) {
                        status.textContent = 'Saved';
                        status.style.color = '#10b981';
                        previousValue = selected.value;
                        setTimeout(function() { status.textContent = ''; }, 2000);
                    } else {
                        status.textContent = 'Save failed';
                        status.style.color = '#991b1b';
                        var prev = document.querySelector('input[name="sitestaffr_knowledge_mode"][value="' + previousValue + '"]');
                        if (prev) prev.checked = true;
                    }
                })
                .catch(function() {
                    status.textContent = 'Save failed';
                    status.style.color = '#991b1b';
                    var prev = document.querySelector('input[name="sitestaffr_knowledge_mode"][value="' + previousValue + '"]');
                    if (prev) prev.checked = true;
                });
        });
    });
})();
JS;
    }

    /**
     * Register AJAX handlers directly (not through the loader).
     *
     * This method registers AJAX handlers immediately when called, bypassing
     * the loader pattern to avoid any timing issues with WordPress AJAX processing.
     *
     * @since    1.0.0
     * @access   private
     */
    private function register_ajax_handlers_direct() {


        // NOTE: wp_ajax_sitestaffr_save_wizard is registered via loader in includes/class-sitestaffr.php.
        // Keep this direct map for handlers that are intentionally owned by SiteStaffr_Admin.

        // Register the send test email handler
        add_action( 'wp_ajax_sitestaffr_send_test_email', array( $this, 'ajax_send_test_email' ) );

        // Register the update email setting handler
        add_action( 'wp_ajax_sitestaffr_update_email_setting', array( $this, 'ajax_update_email_setting' ) );

        // Register the notification email update handler
        add_action( 'wp_ajax_sitestaffr_update_notification_email', array( $this, 'ajax_update_notification_email' ) );

        // Register the combined notification settings save handler
        add_action( 'wp_ajax_sitestaffr_save_notification_settings', array( $this, 'ajax_save_notification_settings' ) );

        // Register AI voice autosave handler (Settings page)
        add_action( 'wp_ajax_sitestaffr_update_ai_voice', array( $this, 'ajax_update_ai_voice' ) );

        // Register the update business phone handler
        add_action( 'wp_ajax_sitestaffr_update_business_phone', array( $this, 'ajax_update_business_phone' ) );

        // Register the save enhancement handler
        add_action( 'wp_ajax_sitestaffr_save_enhancement', array( $this, 'ajax_save_enhancement' ) );

        // Register the reset training counter handler
        add_action( 'wp_ajax_sitestaffr_reset_training_counter', array( $this, 'ajax_reset_training_counter' ) );

        // Register AJAX handler for saving business description
        add_action( 'wp_ajax_sitestaffr_save_business_description', array( $this, 'ajax_save_business_description' ) );

        // Register AJAX handler for website-driven business description generation.
        add_action( 'wp_ajax_sitestaffr_generate_business_description', array( $this, 'ajax_generate_business_description_from_website' ) );

        // Lightweight check: did a description generation complete recently?
        add_action( 'wp_ajax_sitestaffr_check_fresh_description', array( $this, 'ajax_check_fresh_description' ) );

        // Register AJAX handlers for inline call actions (Task W - v0.10.29)
        add_action( 'wp_ajax_sitestaffr_mark_call_read', array( $this, 'ajax_mark_call_read' ) );

        add_action( 'wp_ajax_sitestaffr_mark_call_unread', array( $this, 'ajax_mark_call_unread' ) );

        add_action( 'wp_ajax_sitestaffr_delete_call', array( $this, 'ajax_delete_call' ) );
        add_action( 'wp_ajax_sitestaffr_restore_call', array( $this, 'ajax_restore_call' ) );
        add_action( 'wp_ajax_sitestaffr_permanent_delete_call', array( $this, 'ajax_permanent_delete_call' ) );

        // Register AJAX handlers for banned visitors management (v0.11.45)
        add_action( 'wp_ajax_sitestaffr_get_banned_visitors', array( $this, 'ajax_get_banned_visitors' ) );
        add_action( 'wp_ajax_sitestaffr_ban_visitor', array( $this, 'ajax_ban_visitor' ) );
        add_action( 'wp_ajax_sitestaffr_unban_visitor', array( $this, 'ajax_unban_visitor' ) );

        // Register diagnostics AJAX handlers (only when diagnostics files are present).
        if ( file_exists( SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-diagnostics.php' ) ) {
            add_action( 'wp_ajax_sitestaffr_analyze_tokens', array( $this, 'ajax_analyze_tokens' ) );
            add_action( 'wp_ajax_sitestaffr_drain_minutes_test_state', array( $this, 'ajax_drain_minutes_test_state' ) );
            add_action( 'wp_ajax_sitestaffr_debug_auto_sync', array( $this, 'ajax_debug_auto_sync_candidates' ) );
        }

        // Usage & Billing channel filters (AJAX fragment refresh).
        add_action( 'wp_ajax_sitestaffr_filter_usage_data', array( $this, 'ajax_filter_usage_data' ) );

        // Widget Settings per-section AJAX save.
        add_action( 'wp_ajax_sitestaffr_save_widget_section', array( $this, 'ajax_save_widget_section' ) );

        // Stripe hosted Checkout session creation (Setup Wizard).
        add_action( 'wp_ajax_sitestaffr_create_wizard_checkout', array( $this, 'ajax_create_wizard_checkout' ) );

        // Business Information AJAX save (Settings page).
        add_action( 'wp_ajax_sitestaffr_save_business_settings', array( $this, 'ajax_save_business_settings' ) );

        // My AI Agent page AJAX handlers.
        add_action( 'wp_ajax_sitestaffr_save_agent_instruction', array( $this, 'ajax_save_agent_instruction' ) );
        add_action( 'wp_ajax_sitestaffr_save_agent_voice', array( $this, 'ajax_save_agent_voice' ) );
        add_action( 'wp_ajax_sitestaffr_save_custom_instruction', array( $this, 'ajax_save_custom_instruction' ) );
        add_action( 'wp_ajax_sitestaffr_toggle_custom_instruction', array( $this, 'ajax_toggle_custom_instruction' ) );
        add_action( 'wp_ajax_sitestaffr_delete_custom_instruction', array( $this, 'ajax_delete_custom_instruction' ) );

        // Upgrade proration preview (Usage & Billing page).
        add_action( 'wp_ajax_sitestaffr_preview_upgrade', array( $this, 'ajax_preview_upgrade' ) );

        // Execute subscription upgrade via middleware (Usage & Billing page).
        add_action( 'wp_ajax_sitestaffr_execute_upgrade', array( $this, 'ajax_execute_upgrade' ) );

        // Execute subscription downgrade via middleware (Usage & Billing page).
        add_action( 'wp_ajax_sitestaffr_execute_downgrade', array( $this, 'ajax_execute_downgrade' ) );

        // Reset to fresh install (Diagnostics page, only when files present).
        if ( file_exists( SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-diagnostics.php' ) ) {
            add_action( 'wp_ajax_sitestaffr_reset_to_fresh_install', array( $this, 'ajax_reset_to_fresh_install' ) );
        }

        // Register AJAX handler for report-issue (Sprint 2).
        add_action( 'wp_ajax_sitestaffr_report_issue', array( $this, 'ajax_report_issue' ) );

        // Knowledge sync AJAX handlers (AI Knowledge page).
        add_action( 'wp_ajax_sitestaffr_knowledge_get_content_list', array( $this, 'ajax_knowledge_get_content_list' ) );
        add_action( 'wp_ajax_sitestaffr_knowledge_prepare_sync', array( $this, 'ajax_knowledge_prepare_sync' ) );
        add_action( 'wp_ajax_sitestaffr_knowledge_embed_chunk', array( $this, 'ajax_knowledge_embed_chunk' ) );
        add_action( 'wp_ajax_sitestaffr_knowledge_remove_post', array( $this, 'ajax_knowledge_remove_post' ) );
        add_action( 'wp_ajax_sitestaffr_knowledge_schema_check', array( $this, 'ajax_knowledge_schema_check' ) );
        add_action( 'wp_ajax_sitestaffr_knowledge_probe_embed', array( $this, 'ajax_knowledge_probe_embed' ) );
        add_action( 'wp_ajax_sitestaffr_save_knowledge_mode', array( $this, 'ajax_save_knowledge_mode' ) );
        add_action( 'wp_ajax_sitestaffr_add_manual_knowledge', array( $this, 'ajax_add_manual_knowledge' ) );
        add_action( 'wp_ajax_sitestaffr_delete_manual_knowledge', array( $this, 'ajax_delete_manual_knowledge' ) );

        // Wizard: register site before Step 3 so description gen has valid auth.
        add_action( 'wp_ajax_sitestaffr_wizard_register_site', array( $this, 'ajax_wizard_register_site' ) );

    }

    /**
     * Check if the given hook corresponds to the setup wizard page.
     *
     * WordPress may suffix the parent menu slug (e.g. sitestaffr-1_page_)
     * so we match on the trailing portion instead of exact strings.
     *
     * @since 0.14.283
     * @param string $hook The admin page hook suffix.
     * @return bool
     */
    private function is_setup_wizard_hook( $hook ) {
        return false !== strpos( $hook, '_page_sitestaffr-setup' );
    }

    /**
     * Register the stylesheets for the admin area.
     *
     * @since    1.0.0
     * @param    string    $hook    The current admin page hook.
     */
    public function enqueue_styles( $hook ) {
        // Setup wizard: load fonts + global CSS + wizard-specific CSS (v0.14.184)
        if ( $this->is_setup_wizard_hook( $hook ) ) {
            // Wizard typography (self-hosted fonts — bundled locally per WP.org guidelines)
            wp_enqueue_style(
                $this->plugin_name . '-wizard-fonts',
                SITESTAFFR_PLUGIN_URL . 'admin/css/sitestaffr-fonts.css',
                array(),
                SITESTAFFR_VERSION
            );
            // Load global CSS (provides background, header, base styles)
            wp_enqueue_style(
                $this->plugin_name . '-global',
                SITESTAFFR_PLUGIN_URL . 'admin/css/sitestaffr-global.css',
                array(),
                SITESTAFFR_VERSION,
                'all'
            );
            // Load wizard-specific CSS
            wp_enqueue_style(
                $this->plugin_name . '-wizard',
                SITESTAFFR_PLUGIN_URL . 'admin/css/sitestaffr-wizard.css',
                array( $this->plugin_name . '-global' ),
                SITESTAFFR_VERSION,
                'all'
            );
            return;
        }

        // ========================================================================
        // DIAGNOSTIC LOGGING - Verify file paths and URLs (v0.5.8)
        // ========================================================================
        $global_css_url = SITESTAFFR_PLUGIN_URL . 'admin/css/sitestaffr-global.css';

        // ========================================================================
        // GLOBAL STYLES - Load on ALL admin pages (v0.5.0)
        // ========================================================================
        wp_enqueue_style(
            $this->plugin_name . '-global',
            $global_css_url,
            array(),
            SITESTAFFR_VERSION,
            'all'
        );
        // Get current screen for conditional loading
        $screen = get_current_screen();
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only route parameters for admin asset loading.
        $request_page_slug = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only route parameters for admin asset loading.
        $request_call_id   = isset( $_GET['call_id'] ) ? absint( wp_unslash( $_GET['call_id'] ) ) : 0;

        // ========================================================================
        // PAGE-SPECIFIC STYLES - Load only on their respective pages (v0.5.0)
        // ========================================================================

        // Calls page styles
        // CRITICAL FIX v0.5.9: Add $_GET['page'] fallback for menu position conflicts
        // CRITICAL FIX v0.11.45: Added sitestaffr-view-calls for View Calls page
        // FIX: Load calls CSS for ALL conversation views (all/trash/banned), not just view=all
        if ( $hook === 'toplevel_page_sitestaffr' ||
             $hook === 'sitestaffr_page_sitestaffr-calls' ||
             $hook === 'sitestaffr_page_sitestaffr-view-calls' ||
             ( $screen && $screen->id === 'toplevel_page_sitestaffr' ) ||
             ( $screen && $screen->id === 'sitestaffr_page_sitestaffr-calls' ) ||
             ( $screen && $screen->id === 'sitestaffr_page_sitestaffr-view-calls' ) ||
             'sitestaffr-calls' === $request_page_slug ||
             'sitestaffr-view-calls' === $request_page_slug ||
             'sitestaffr' === $request_page_slug ) {
            wp_enqueue_style(
                $this->plugin_name . '-calls',
                SITESTAFFR_PLUGIN_URL . 'admin/css/sitestaffr-calls.css',
                array( $this->plugin_name . '-global' ),
                SITESTAFFR_VERSION,
                'all'
            );

            // Banned visitors JS (only on banned view).
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view parameter for conditional asset loading.
            $is_banned_view = isset( $_GET['view'] ) && 'banned' === sanitize_key( wp_unslash( $_GET['view'] ) );
            if ( $is_banned_view ) {
                wp_enqueue_script(
                    $this->plugin_name . '-calls-banned',
                    SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-calls-banned.js',
                    array( 'jquery' ),
                    SITESTAFFR_VERSION,
                    true
                );
                wp_localize_script( $this->plugin_name . '-calls-banned', 'sitestaffrBannedData', array(
                    'nonce' => wp_create_nonce( 'sitestaffr_banned_numbers_nonce' ),
                    'i18n'  => array(
                        'loadFailed'         => __( 'Failed to load banned users. Please refresh the page.', 'sitestaffr' ),
                        'unknown'            => __( 'Unknown', 'sitestaffr' ),
                        'unban'              => __( 'Unban', 'sitestaffr' ),
                        'confirmUnban'       => __( 'Are you sure you want to unban this user?', 'sitestaffr' ),
                        'unbanning'          => __( 'Unbanning...', 'sitestaffr' ),
                        'unbanSuccess'       => __( 'User unbanned successfully.', 'sitestaffr' ),
                        'unbanFailed'        => __( 'Failed to unban user. Please try again.', 'sitestaffr' ),
                        'identifierRequired' => __( 'Identifier is required.', 'sitestaffr' ),
                        'invalidIdentifier'  => __( 'Please enter a valid identifier.', 'sitestaffr' ),
                        'banning'            => __( 'Banning...', 'sitestaffr' ),
                        'banSuccess'         => __( 'User banned successfully.', 'sitestaffr' ),
                        'banFailed'          => __( 'Failed to ban user. Please try again.', 'sitestaffr' ),
                        'banUser'            => __( 'Ban User', 'sitestaffr' ),
                    ),
                ) );
            }
        }

        // Call Detail page - Enqueue BOTH Calls CSS + AI Knowledge CSS (v0.7.10)
        // CRITICAL FIX: Make call_id check independent of calls page condition
        // This ensures CSS loads even if page parameter detection fails
        if ( $request_call_id > 0 ) {
            // Enqueue Calls CSS (for call details page - includes two-column layout)
            wp_enqueue_style(
                $this->plugin_name . '-calls',
                SITESTAFFR_PLUGIN_URL . 'admin/css/sitestaffr-calls.css',
                array( $this->plugin_name . '-global' ),
                SITESTAFFR_VERSION,
                'all'
            );
        }

        // Settings/Dashboard page styles (main SiteStaffr page AND settings submenu)
        // CRITICAL FIX v0.5.9: Add $_GET['page'] fallback for menu position conflicts
        if ( $hook === 'toplevel_page_sitestaffr' ||
             $hook === 'sitestaffr_page_sitestaffr-settings' ||
             ( $screen && $screen->id === 'toplevel_page_sitestaffr' ) ||
             ( $screen && $screen->id === 'sitestaffr_page_sitestaffr-settings' ) ||
             'sitestaffr' === $request_page_slug ||
             'sitestaffr-settings' === $request_page_slug ||
             false !== strpos( $hook, 'sitestaffr-my-agent' ) ||
             'sitestaffr-my-agent' === $request_page_slug ) {
            $settings_css_url = SITESTAFFR_PLUGIN_URL . 'admin/css/sitestaffr-settings.css';
            wp_enqueue_style(
                $this->plugin_name . '-settings',
                $settings_css_url,
                array( $this->plugin_name . '-global' ),
                SITESTAFFR_VERSION,
                'all'
            );
        }

        // Usage Dashboard page styles
        if ( $hook === 'sitestaffr_page_sitestaffr-usage' ||
             ( $screen && $screen->id === 'sitestaffr_page_sitestaffr-usage' ) ||
             'sitestaffr-usage' === $request_page_slug ) {
            $usage_css_url = SITESTAFFR_PLUGIN_URL . 'admin/css/sitestaffr-usage-dashboard.css';
            wp_enqueue_style(
                $this->plugin_name . '-usage-dashboard',
                $usage_css_url,
                array( $this->plugin_name . '-global' ),
                SITESTAFFR_VERSION,
                'all'
            );
        }

        // Buy More Minutes modal assets on all SiteStaffr admin pages except setup wizard.
        $page_slug = $request_page_slug;
        $screen_id = ( $screen && isset( $screen->id ) ) ? (string) $screen->id : '';
        $is_sitestaffr_admin_page = (
            false !== strpos( (string) $hook, 'sitestaffr' ) ||
            false !== strpos( $screen_id, 'sitestaffr' ) ||
            0 === strpos( $page_slug, 'sitestaffr' )
        );
        $is_setup_wizard_page = (
            $this->is_setup_wizard_hook( $hook ) ||
            false !== strpos( $screen_id, '_page_sitestaffr-setup' ) ||
            $page_slug === 'sitestaffr-setup'
        );
        if ( $is_sitestaffr_admin_page && ! $is_setup_wizard_page ) {
            wp_enqueue_style(
                $this->plugin_name . '-modal',
                SITESTAFFR_PLUGIN_URL . 'admin/css/sitestaffr-modal.css',
                array( $this->plugin_name . '-global' ),
                SITESTAFFR_VERSION,
                'all'
            );
        }

        // Widget Settings page styles
        if ( $hook === 'sitestaffr_page_sitestaffr-widget-settings' ||
             ( $screen && $screen->id === 'sitestaffr_page_sitestaffr-widget-settings' ) ||
             'sitestaffr-widget-settings' === $request_page_slug ) {
            $widget_settings_css_url = SITESTAFFR_PLUGIN_URL . 'admin/css/admin-widget-settings.css';
            wp_enqueue_style(
                $this->plugin_name . '-widget-settings',
                $widget_settings_css_url,
                array( $this->plugin_name . '-global' ),
                SITESTAFFR_VERSION,
                'all'
            );
            // Enqueue WordPress color picker
            wp_enqueue_style( 'wp-color-picker' );
        }

        // AI Knowledge page styles
        if ( false !== strpos( $hook, 'sitestaffr-knowledge' ) ||
             'sitestaffr-knowledge' === $request_page_slug ) {
            wp_enqueue_style(
                $this->plugin_name . '-admin-knowledge',
                SITESTAFFR_PLUGIN_URL . 'admin/css/admin-knowledge.css',
                array( $this->plugin_name . '-global' ),
                SITESTAFFR_VERSION,
                'all'
            );
        }

        // My AI Agent page styles (includes knowledge CSS).
        if ( false !== strpos( $hook, 'sitestaffr-my-agent' ) ||
             'sitestaffr-my-agent' === $request_page_slug ) {
            wp_enqueue_style(
                $this->plugin_name . '-admin-knowledge',
                SITESTAFFR_PLUGIN_URL . 'admin/css/admin-knowledge.css',
                array( $this->plugin_name . '-global' ),
                SITESTAFFR_VERSION,
                'all'
            );
            wp_enqueue_style(
                $this->plugin_name . '-admin-my-agent',
                SITESTAFFR_PLUGIN_URL . 'admin/css/admin-my-agent.css',
                array( $this->plugin_name . '-global' ),
                SITESTAFFR_VERSION,
                'all'
            );
        }
    }

    /**
     * Register the JavaScript for the admin area.
     *
     * @since    1.0.0
     * @param    string    $hook    The current admin page hook.
     */
    public function enqueue_scripts( $hook ) {
        // Get current screen to conditionally load scripts
        $screen = get_current_screen();
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only route parameters for admin asset loading.
        $request_page_slug   = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only route parameters for admin asset loading.
        $request_cycle_offset = isset( $_GET['cycle_offset'] ) ? absint( wp_unslash( $_GET['cycle_offset'] ) ) : 0;
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only route parameters for admin asset loading.
        $request_call_id = isset( $_GET['call_id'] ) ? absint( wp_unslash( $_GET['call_id'] ) ) : 0;

        // Dequeue WordPress block-editor scripts on the setup wizard page.
        // These can cause ".register" errors when wp-data isn't loaded.
        // IMPORTANT: Only dequeue, never deregister — deregistering breaks
        // dependency chains for other plugins (e.g., Elementor, Crocoblock)
        // that load admin-wide scripts depending on wp-i18n, wp-hooks, etc.
        if ( $this->is_setup_wizard_hook( $hook ) ) {
            $scripts_to_remove = array(
                // PRIMARY CONFLICT SCRIPTS - These cause .register errors
                'wp-hooks',
                'wp-dom-ready',
                'wp-i18n',
                'wp-a11y',
                // Block editor scripts that depend on wp.data
                'wp-edit-post',
                'wp-editor',
                'wp-block-library',
                'wp-block-editor',
                'wp-blocks',
                'wp-edit-site',
                'wp-format-library',
                'wp-list-reusable-blocks',
                'wp-nux',
                'wp-plugins',
                'wp-edit-widgets',
                'wp-customize-widgets',
                'wp-widgets',
                'wp-data',
                'wp-redux-routine',
                'wp-core-data',
                'wp-block-directory',
                'wp-reusable-blocks',
                'wp-patterns',
                'wp-element',
                'wp-components',
            );

            foreach ( $scripts_to_remove as $script_handle ) {
                wp_dequeue_script( $script_handle );
            }

            // Enqueue setup wizard external JS (no jQuery dependency).
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only GET param for UI state.
            $checkout_complete = isset( $_GET['checkout_complete'] ) ? sanitize_key( wp_unslash( $_GET['checkout_complete'] ) ) : '';

            wp_enqueue_script(
                $this->plugin_name . '-setup-wizard',
                SITESTAFFR_PLUGIN_URL . 'admin/js/setup-wizard.js',
                array(),
                SITESTAFFR_VERSION,
                true
            );
            wp_localize_script( $this->plugin_name . '-setup-wizard', 'sitestaffrWizardData', array(
                'ajaxUrl'                  => admin_url( 'admin-ajax.php' ),
                'dashboardUrl'             => admin_url( 'admin.php?page=sitestaffr' ),
                'widgetSettingsUrl'         => admin_url( 'admin.php?page=sitestaffr-widget-settings' ),
                'generateDescriptionNonce' => wp_create_nonce( 'sitestaffr_generate_business_description' ),
                'restUrl'                  => esc_url_raw( rest_url( 'sitestaffr/v1/' ) ),
                'restNonce'                => wp_create_nonce( 'wp_rest' ),
                'checkoutComplete'         => $checkout_complete,
                'i18n'                     => array(
                    'next'                    => __( 'Next', 'sitestaffr' ),
                    'businessNameRequired'    => __( 'Business name is required.', 'sitestaffr' ),
                    'businessPhoneRequired'   => __( 'Business phone number is required.', 'sitestaffr' ),
                    'invalidPhone'            => __( 'Please enter a valid 10-digit phone number.', 'sitestaffr' ),
                    'saveError'               => __( 'Error saving settings. Please try again.', 'sitestaffr' ),
                    'connectionError'         => __( 'Connection error. Please try again.', 'sitestaffr' ),
                    'checkoutFailed'          => __( 'Unable to start checkout. Please try again.', 'sitestaffr' ),
                    'checkoutUnexpected'      => __( 'Unexpected checkout response. Please refresh and try again.', 'sitestaffr' ),
                    'checkoutSessionFailed'   => __( 'Failed to create checkout session.', 'sitestaffr' ),
                    'checkoutConnectionError' => __( 'Connection error while starting checkout. Please try again.', 'sitestaffr' ),
                    'allSet'                  => __( "You're All Set!", 'sitestaffr' ),
                    'allSetDesc'              => __( 'Your AI voice agent is configured and ready. Here\'s what to do next:', 'sitestaffr' ),
                    'nextStep1'               => __( 'Style your widget — choose colors, icons, and size to match your site', 'sitestaffr' ),
                    'nextStep2'               => __( 'Your widget is already live on all pages — toggle it off or use shortcodes for specific pages', 'sitestaffr' ),
                    'customizeWidget'         => __( 'Customize Your Widget', 'sitestaffr' ),
                    'goToDashboard'           => __( 'Go to Dashboard', 'sitestaffr' ),
                    'selectHours'             => __( 'Please select your business hours before continuing.', 'sitestaffr' ),
                    'addDescription'          => __( 'Please add a business description before continuing.', 'sitestaffr' ),
                    'generateDescription'     => __( 'Please generate a business description before continuing. Use the Scan My Website button above.', 'sitestaffr' ),
                    'saveEnhancementError'    => __( 'Error saving enhancement data. Please try again.', 'sitestaffr' ),
                    'open'                    => __( 'Open', 'sitestaffr' ),
                    'closed'                  => __( 'Closed', 'sitestaffr' ),
                    'scanParseError'          => __( 'Error processing response. Please try again.', 'sitestaffr' ),
                    'scanEndpointUnavailable' => __( 'Website scan endpoint is unavailable (AJAX returned 0). Please sync plugin files and refresh wp-admin.', 'sitestaffr' ),
                    'scanRequestFailed'       => __( 'Website scan request failed. Please refresh and try again.', 'sitestaffr' ),
                    'scanRateLimited'         => __( 'You\'ve reached the daily limit for description generations. Please try again tomorrow.', 'sitestaffr' ),
                    'scanFailed'              => __( 'Unable to scan website. You can write your description manually below.', 'sitestaffr' ),
                    'scanConnectionError'     => __( 'Connection error. You can write your description manually below.', 'sitestaffr' ),
                ),
            ) );

            return;
        }

        // ========================================================================
        // GLOBAL SCRIPTS - Load on ALL admin pages (v0.5.7)
        // ========================================================================

        // Tooltip functionality - used across all SiteStaffr admin pages
        wp_enqueue_script(
            $this->plugin_name . '-tooltips',
            SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-tooltips.js',
            array( 'jquery' ),
            SITESTAFFR_VERSION,
            false  // Load in header to ensure tooltips work immediately
        );

        // Low-balance alert dismiss handler (attached to tooltips, which loads on all pages).
        wp_localize_script( $this->plugin_name . '-tooltips', 'sitestaffrAlertDismiss', array(
            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
            'nonce'   => wp_create_nonce( 'sitestaffr_low_balance_dismiss' ),
        ) );
        wp_add_inline_script( $this->plugin_name . '-tooltips', '(function(){var a=document.getElementById("sitestaffr-global-low-balance-alert");if(!a)return;var d=a.querySelector(".sitestaffr-alert-dismiss");if(!d)return;d.addEventListener("click",function(){a.style.transition="opacity 0.3s ease, max-height 0.3s ease";a.style.opacity="0";a.style.maxHeight="0";a.style.overflow="hidden";a.style.marginBottom="0";a.style.padding="0";setTimeout(function(){a.remove()},350);var c=sitestaffrAlertDismiss||{};var x=new XMLHttpRequest();x.open("POST",c.ajaxUrl,true);x.setRequestHeader("Content-Type","application/x-www-form-urlencoded");var l=encodeURIComponent(a.getAttribute("data-alert-level")||"");x.send("action=sitestaffr_dismiss_low_balance_alert&nonce="+encodeURIComponent(c.nonce)+"&alert_level="+l)})})();' );

        // Diagnostics page script (only when diagnostics files are present).
        if ( file_exists( SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-diagnostics.php' ) &&
             ( $hook === 'sitestaffr_page_sitestaffr-diagnostics' ||
                ( $screen && $screen->id === 'sitestaffr_page_sitestaffr-diagnostics' ) ||
                'sitestaffr-diagnostics' === $request_page_slug ) ) {
            wp_enqueue_script(
                $this->plugin_name . '-diagnostics',
                SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-diagnostics.js',
                array( 'jquery' ),
                SITESTAFFR_VERSION,
                true // Load in footer
            );

            // Pass nonce to JavaScript
            wp_localize_script(
                $this->plugin_name . '-diagnostics',
                'sitestaffrDiagnostics',
                array(
                    'nonce' => wp_create_nonce( 'sitestaffr_diagnostics_nonce' ),
                )
            );

        }

        // Call Detail page scripts (copy session ID, ban IP, mark unread, delete, report issue).
        if ( $request_call_id > 0 ) {
            wp_enqueue_script(
                $this->plugin_name . '-call-detail',
                SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-call-detail.js',
                array( 'jquery' ),
                SITESTAFFR_VERSION,
                true
            );
            wp_localize_script( $this->plugin_name . '-call-detail', 'sitestaffrCallDetailData', array(
                'banNonce'     => wp_create_nonce( 'sitestaffr_banned_numbers_nonce' ),
                'ajaxNonce'    => wp_create_nonce( 'sitestaffr_ajax_nonce' ),
                'callsListUrl' => wp_nonce_url( admin_url( 'admin.php?page=sitestaffr&view=all' ), 'sitestaffr_view' ),
                'dashboardUrl' => admin_url( 'admin.php?page=sitestaffr' ),
                'i18n'         => array(
                    'copied'           => __( 'Copied!', 'sitestaffr' ),
                    'confirmBan'       => __( 'Ban this IP address?', 'sitestaffr' ),
                    'banning'          => __( 'Banning...', 'sitestaffr' ),
                    'banSuccess'       => __( 'IP address banned successfully.', 'sitestaffr' ),
                    'banFailed'        => __( 'Failed to ban IP address.', 'sitestaffr' ),
                    'banFailedRetry'   => __( 'Failed to ban IP address. Please try again.', 'sitestaffr' ),
                    'banned'           => __( 'Banned', 'sitestaffr' ),
                    'confirmDelete'    => __( 'Are you sure you want to delete this conversation? This cannot be undone.', 'sitestaffr' ),
                    'deleteFailed'     => __( 'Failed to delete conversation.', 'sitestaffr' ),
                    'deleteFailedRetry' => __( 'Failed to delete conversation. Please try again.', 'sitestaffr' ),
                    'submitReport'     => __( 'Submit Report', 'sitestaffr' ),
                    'selectIssueType'  => __( 'Please select an issue type.', 'sitestaffr' ),
                    'submitting'       => __( 'Submitting...', 'sitestaffr' ),
                    'reportFailed'     => __( 'Failed to submit report. Please try again.', 'sitestaffr' ),
                ),
            ) );
        }

        // Widget Settings page scripts
        if ( $hook === 'sitestaffr_page_sitestaffr-widget-settings' ||
             ( $screen && $screen->id === 'sitestaffr_page_sitestaffr-widget-settings' ) ||
             'sitestaffr-widget-settings' === $request_page_slug ) {
            // Enqueue WordPress color picker
            wp_enqueue_script( 'wp-color-picker' );

            // Enqueue widget settings live preview script
            wp_enqueue_script(
                $this->plugin_name . '-widget-settings',
                SITESTAFFR_PLUGIN_URL . 'admin/js/admin-widget-settings.js',
                array( 'jquery', 'wp-color-picker' ),
                SITESTAFFR_VERSION,
                true
            );

            // Localize script with plugin data
            wp_localize_script(
                $this->plugin_name . '-widget-settings',
                'sitestaffr_admin_data',
                array(
                    'plugin_url'          => SITESTAFFR_PLUGIN_URL,
                    'middleware_url'      => SITESTAFFR_MIDDLEWARE_URL,
                    'version'             => SITESTAFFR_VERSION,
                    'save_widget_nonce'   => wp_create_nonce( 'sitestaffr_save_widget_settings' ),
                    'sitestaffr_icon_svg' => file_get_contents( SITESTAFFR_PLUGIN_DIR . 'assets/images/sitestaffr-icon.svg' ), // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin asset
                    'plan_name'           => class_exists( 'SiteStaffr_Billing_State' ) ? sanitize_key( isset( SiteStaffr_Billing_State::get_cache()['plan_name'] ) ? SiteStaffr_Billing_State::get_cache()['plan_name'] : 'starter' ) : 'starter',
                )
            );

        }

        // Settings page scripts (v0.12.11 - AI Voice Selector)
        if ( $hook === 'sitestaffr_page_sitestaffr-settings' ||
             ( $screen && $screen->id === 'sitestaffr_page_sitestaffr-settings' ) ||
             'sitestaffr-settings' === $request_page_slug ) {
            // Enqueue settings page JS (copy, save, hours editor, generate description, etc.)
            wp_enqueue_script(
                $this->plugin_name . '-settings-page',
                SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-settings-page.js',
                array( 'jquery' ),
                SITESTAFFR_VERSION,
                true
            );
            wp_localize_script( $this->plugin_name . '-settings-page', 'sitestaffrSettingsPageData', array(
                'pluginUrl'      => SITESTAFFR_PLUGIN_URL,
                'pluginVersion'  => SITESTAFFR_VERSION,
                'siteUrl'        => get_site_url(),
                'nonces'         => array(
                    'admin'               => wp_create_nonce( 'sitestaffr_admin_nonce' ),
                    'generateDescription' => wp_create_nonce( 'sitestaffr_generate_business_description' ),
                ),
                'i18n'           => array(
                    'copied'                 => __( 'Copied!', 'sitestaffr' ),
                    'copyFailed'             => __( 'Failed to copy phone number. Please copy manually.', 'sitestaffr' ),
                    'businessNameRequired'   => __( 'Business name is required.', 'sitestaffr' ),
                    'saving'                 => __( 'Saving...', 'sitestaffr' ),
                    'saved'                  => __( 'Saved!', 'sitestaffr' ),
                    'settingsSaved'          => __( 'Settings saved.', 'sitestaffr' ),
                    'error'                  => __( 'Error', 'sitestaffr' ),
                    'saveFailed'             => __( 'Save failed.', 'sitestaffr' ),
                    'networkError'           => __( 'Network error. Please try again.', 'sitestaffr' ),
                    'stop'                   => __( 'Stop', 'sitestaffr' ),
                    'preview'                => __( 'Preview', 'sitestaffr' ),
                    'tonePreviewUnavailable' => __( 'Tone preview audio is not available yet for this option.', 'sitestaffr' ),
                    'sending'                => __( 'Sending...', 'sitestaffr' ),
                    'testEmailError'         => __( 'Error sending test email.', 'sitestaffr' ),
                    'connectionError'        => __( 'Connection error. Please try again.', 'sitestaffr' ),
                    'invalidEmail'           => __( 'Please enter a valid email address.', 'sitestaffr' ),
                    'invalidCcEmails'        => __( 'Please enter valid CC email addresses, separated by commas.', 'sitestaffr' ),
                    'tooManyCcEmails'        => __( 'You can add up to 5 CC email addresses.', 'sitestaffr' ),
                    'errorSavingSettings'    => __( 'Error saving settings.', 'sitestaffr' ),
                    'dailyLimitReached'      => __( 'You\'ve reached the daily limit for description generations. Please try again tomorrow.', 'sitestaffr' ),
                    'scanningWebsite'        => __( 'Scanning website...', 'sitestaffr' ),
                    'descriptionGenerated'   => __( 'Description generated!', 'sitestaffr' ),
                    'generationFailed'       => __( 'Generation failed.', 'sitestaffr' ),
                ),
            ) );

            // Enqueue AI Voice Selector script
            wp_enqueue_script(
                $this->plugin_name . '-settings-voice',
                SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-settings-voice.js',
                array( 'jquery' ),
                SITESTAFFR_VERSION,
                true
            );

            // Pass plugin URL for audio file paths.
            wp_add_inline_script(
                $this->plugin_name . '-settings-voice',
                'var sitestaffrPluginUrl = ' . wp_json_encode( SITESTAFFR_PLUGIN_URL ) . ';',
                'before'
            );

            $voice_plan_key     = $this->get_current_plan_key();
            $voice_allowed_list = self::get_voices_for_plan( $voice_plan_key );

            wp_localize_script(
                $this->plugin_name . '-settings-voice',
                'sitestaffrSettingsVoice',
                array(
                    'ajaxUrl'       => admin_url( 'admin-ajax.php' ),
                    'nonce'         => wp_create_nonce( 'sitestaffr_admin_nonce' ),
                    'version'       => SITESTAFFR_VERSION,
                    'allowedVoices' => $voice_allowed_list,
                )
            );

            // Voice data for JS (filtered to allowed voices only).
            $voice_data_for_js = self::get_voice_metadata();
            $voice_data_for_js = array_intersect_key( $voice_data_for_js, array_flip( $voice_allowed_list ) );
            wp_add_inline_script(
                $this->plugin_name . '-settings-voice',
                'var sitestaffrVoiceData = ' . wp_json_encode( $voice_data_for_js ) . ';',
                'before'
            );

            // Realtime toggle handler.
            $realtime_on  = __( 'Realtime Voice (Fast & Natural)', 'sitestaffr' );
            $realtime_off = __( 'Text-Based (Traditional)', 'sitestaffr' );
            wp_add_inline_script(
                $this->plugin_name . '-settings-voice',
                'jQuery(document).ready(function($) {
                    $("#sitestaffr_use_realtime").on("change", function() {
                        var statusText = $(this).is(":checked") ? ' . wp_json_encode( $realtime_on ) . ' : ' . wp_json_encode( $realtime_off ) . ';
                        $("#current-mode-status").text(statusText);
                    });
                });'
            );
        }

        // Usage page scripts (AJAX channel filters).
        if ( $hook === 'sitestaffr_page_sitestaffr-usage' ||
             ( $screen && $screen->id === 'sitestaffr_page_sitestaffr-usage' ) ||
             'sitestaffr-usage' === $request_page_slug ) {
            wp_enqueue_script(
                $this->plugin_name . '-usage-dashboard',
                SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-usage-dashboard.js',
                array( 'jquery' ),
                SITESTAFFR_VERSION,
                true
            );

            wp_localize_script(
                $this->plugin_name . '-usage-dashboard',
                'sitestaffrUsageDashboard',
                array(
                    'ajaxUrl'      => admin_url( 'admin-ajax.php' ),
                    'nonce'        => wp_create_nonce( 'sitestaffr_usage_filter_nonce' ),
                    'upgradeNonce' => wp_create_nonce( 'sitestaffr_billing_upgrade' ),
                    'cycleOffset'  => $request_cycle_offset,
                    'pageUrl'      => admin_url( 'admin.php?page=sitestaffr-usage' ),
                )
            );
        }

        // Dashboard page scripts (new calls toggle, copy phone, mark read/unread).
        if ( $hook === 'toplevel_page_sitestaffr' ||
             ( $screen && $screen->id === 'toplevel_page_sitestaffr' ) ||
             'sitestaffr' === $request_page_slug ) {
            // Only load on the dashboard (no call_id = not call detail view).
            if ( 0 === $request_call_id ) {
                wp_enqueue_script(
                    $this->plugin_name . '-dashboard',
                    SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-dashboard.js',
                    array( 'jquery' ),
                    SITESTAFFR_VERSION,
                    true
                );
                wp_localize_script( $this->plugin_name . '-dashboard', 'sitestaffrDashboardData', array(
                    'nonces' => array(
                        'ajax' => wp_create_nonce( 'sitestaffr_ajax_nonce' ),
                    ),
                    'i18n'   => array(
                        'marking'            => __( 'Marking...', 'sitestaffr' ),
                        'markAsReadTitle'    => __( 'Mark as read', 'sitestaffr' ),
                        'markRead'           => __( 'Mark Read', 'sitestaffr' ),
                        'markAsUnreadTitle'  => __( 'Mark as unread', 'sitestaffr' ),
                        'markUnread'         => __( 'Mark Unread', 'sitestaffr' ),
                        'newBadge'           => __( 'New', 'sitestaffr' ),
                        'errorMarkRead'      => __( 'Error marking conversation as read. Please try again.', 'sitestaffr' ),
                        'errorMarkUnread'    => __( 'Error marking conversation as unread. Please try again.', 'sitestaffr' ),
                        'connectionError'    => __( 'Connection error. Please try again.', 'sitestaffr' ),
                        'allCaughtUp'        => __( "You're all caught up!", 'sitestaffr' ),
                        'noNewConversations' => __( 'No new conversations to review', 'sitestaffr' ),
                    ),
                ) );

                // Calls list table controls (mobile select-all, date filter toggle).
                wp_enqueue_script(
                    $this->plugin_name . '-calls-list-controls',
                    SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-calls-list-controls.js',
                    array(),
                    SITESTAFFR_VERSION,
                    true
                );
                wp_localize_script( $this->plugin_name . '-calls-list-controls', 'sitestaffrCallsListData', array(
                    'i18n' => array(
                        'selectAll' => __( 'Select all', 'sitestaffr' ),
                    ),
                ) );
            }
        }

        // AI Knowledge page scripts
        if ( false !== strpos( $hook, 'sitestaffr-knowledge' ) ||
             'sitestaffr-knowledge' === $request_page_slug ) {
            wp_enqueue_script(
                $this->plugin_name . '-admin-knowledge',
                SITESTAFFR_PLUGIN_URL . 'admin/js/admin-knowledge.js',
                array( 'jquery' ),
                SITESTAFFR_VERSION,
                true
            );
            wp_localize_script( $this->plugin_name . '-admin-knowledge', 'sitestaffr_knowledge', array(
                'ajax_url'            => admin_url( 'admin-ajax.php' ),
                'nonce'               => wp_create_nonce( 'sitestaffr_ajax_nonce' ),
                'manual_nonce'        => wp_create_nonce( 'sitestaffr_knowledge_admin' ),
                'knowledge_mode_nonce' => wp_create_nonce( 'sitestaffr_save_knowledge_mode' ),
                'rest_url'            => esc_url_raw( rest_url( 'sitestaffr/v1/' ) ),
                'rest_nonce'          => wp_create_nonce( 'wp_rest' ),
                'version'             => SITESTAFFR_VERSION,
            ) );
            wp_add_inline_script( $this->plugin_name . '-admin-knowledge', $this->get_knowledge_mode_inline_js() );
        }

        // My AI Agent page scripts.
        if ( false !== strpos( $hook, 'sitestaffr-my-agent' ) ||
             'sitestaffr-my-agent' === $request_page_slug ) {
            // Knowledge tab JS.
            wp_enqueue_script(
                $this->plugin_name . '-admin-knowledge',
                SITESTAFFR_PLUGIN_URL . 'admin/js/admin-knowledge.js',
                array( 'jquery' ),
                SITESTAFFR_VERSION,
                true
            );
            wp_localize_script( $this->plugin_name . '-admin-knowledge', 'sitestaffr_knowledge', array(
                'ajax_url'            => admin_url( 'admin-ajax.php' ),
                'nonce'               => wp_create_nonce( 'sitestaffr_ajax_nonce' ),
                'manual_nonce'        => wp_create_nonce( 'sitestaffr_knowledge_admin' ),
                'knowledge_mode_nonce' => wp_create_nonce( 'sitestaffr_save_knowledge_mode' ),
                'rest_url'            => esc_url_raw( rest_url( 'sitestaffr/v1/' ) ),
                'rest_nonce'          => wp_create_nonce( 'wp_rest' ),
                'version'             => SITESTAFFR_VERSION,
            ) );
            wp_add_inline_script( $this->plugin_name . '-admin-knowledge', $this->get_knowledge_mode_inline_js() );

            // Voice selector JS (from settings page).
            wp_enqueue_script(
                $this->plugin_name . '-settings-voice',
                SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-settings-voice.js',
                array( 'jquery' ),
                SITESTAFFR_VERSION,
                true
            );

            wp_add_inline_script(
                $this->plugin_name . '-settings-voice',
                'var sitestaffrPluginUrl = ' . wp_json_encode( SITESTAFFR_PLUGIN_URL ) . ';',
                'before'
            );

            $voice_plan_key     = $this->get_current_plan_key();
            $voice_allowed_list = self::get_voices_for_plan( $voice_plan_key );

            wp_localize_script(
                $this->plugin_name . '-settings-voice',
                'sitestaffrSettingsVoice',
                array(
                    'ajaxUrl'       => admin_url( 'admin-ajax.php' ),
                    'nonce'         => wp_create_nonce( 'sitestaffr_admin_nonce' ),
                    'version'       => SITESTAFFR_VERSION,
                    'allowedVoices' => $voice_allowed_list,
                )
            );

            $voice_data_for_js = self::get_voice_metadata();
            $voice_data_for_js = array_intersect_key( $voice_data_for_js, array_flip( $voice_allowed_list ) );
            wp_add_inline_script(
                $this->plugin_name . '-settings-voice',
                'var sitestaffrVoiceData = ' . wp_json_encode( $voice_data_for_js ) . ';',
                'before'
            );

            // My AI Agent page JS (tabs, toggles, voice save).
            wp_enqueue_script(
                $this->plugin_name . '-admin-my-agent',
                SITESTAFFR_PLUGIN_URL . 'admin/js/admin-my-agent.js',
                array( 'jquery' ),
                SITESTAFFR_VERSION,
                true
            );
            wp_localize_script( $this->plugin_name . '-admin-my-agent', 'sitestaffrAgentData', array(
                'nonces' => array(
                    'agent' => wp_create_nonce( 'sitestaffr_agent_nonce' ),
                ),
                'i18n' => array(
                    'saving'              => __( 'Saving...', 'sitestaffr' ),
                    'saved'               => __( 'Saved!', 'sitestaffr' ),
                    'saveFailed'          => __( 'Save failed.', 'sitestaffr' ),
                    'confirmDelete'       => __( 'Delete this instruction?', 'sitestaffr' ),
                    'instructionEmpty'    => __( 'Instruction cannot be empty.', 'sitestaffr' ),
                    'instructionTooLong'  => __( 'Instruction exceeds 200 characters.', 'sitestaffr' ),
                    'limitReached'        => __( 'You have reached the maximum number of custom instructions for your plan.', 'sitestaffr' ),
                ),
                'customInstructions' => array(
                    'limit' => self::get_custom_instructions_limit( $this->get_current_plan_key() ),
                ),
            ) );
        }

        // Buy More Minutes modal assets on all SiteStaffr admin pages except setup wizard.
        $page_slug = $request_page_slug;
        $screen_id = ( $screen && isset( $screen->id ) ) ? (string) $screen->id : '';
        $is_sitestaffr_admin_page = (
            false !== strpos( (string) $hook, 'sitestaffr' ) ||
            false !== strpos( $screen_id, 'sitestaffr' ) ||
            0 === strpos( $page_slug, 'sitestaffr' )
        );
        $is_setup_wizard_page = (
            $this->is_setup_wizard_hook( $hook ) ||
            false !== strpos( $screen_id, '_page_sitestaffr-setup' ) ||
            $page_slug === 'sitestaffr-setup'
        );
        if ( $is_sitestaffr_admin_page && ! $is_setup_wizard_page ) {
            wp_enqueue_script(
                $this->plugin_name . '-buy-modal',
                SITESTAFFR_PLUGIN_URL . 'admin/js/sitestaffr-buy-modal.js',
                array( 'jquery' ),
                SITESTAFFR_VERSION,
                true
            );
        }

    }

    /**
     * Dequeue WordPress scripts on setup wizard page (late priority).
     *
     * This runs at priority 999 to catch any scripts enqueued by other plugins
     * or themes after our main enqueue_scripts() runs. This prevents wp.data
     * undefined errors and ".register" errors on the setup wizard page.
     *
     * CRITICAL: wp-hooks, wp-dom-ready, wp-i18n, and wp-a11y MUST be removed
     * because they cause JavaScript errors when wp.data doesn't exist.
     *
     * @since    1.0.0
     * @param    string    $hook    The current admin page hook.
     */
    public function dequeue_block_editor_scripts_late( $hook ) {
        // Check using both hook parameter and screen ID for maximum reliability
        $screen = get_current_screen();
        $is_setup_wizard = $this->is_setup_wizard_hook( $hook );

        // Only run on setup wizard page
        if ( ! $is_setup_wizard ) {
            return;
        }

        // Comprehensive list of WordPress scripts - MUST match enqueue_scripts() list
        // This catches any scripts that were enqueued by other plugins/themes after our
        // main enqueue_scripts() hook ran.
        $scripts_to_remove = array(
            // PRIMARY CONFLICT SCRIPTS - These cause .register errors
            'wp-hooks',
            'wp-dom-ready',
            'wp-i18n',
            'wp-a11y',
            // Block editor scripts that depend on wp.data
            'wp-edit-post',
            'wp-editor',
            'wp-block-library',
            'wp-block-editor',
            'wp-blocks',
            'wp-edit-site',
            'wp-format-library',
            'wp-list-reusable-blocks',
            'wp-nux',
            'wp-plugins',
            'wp-edit-widgets',
            'wp-customize-widgets',
            'wp-widgets',
            'wp-data',
            'wp-redux-routine',
            'wp-core-data',
            'wp-block-directory',
            'wp-reusable-blocks',
            'wp-patterns',
            'wp-element',
            'wp-components',
        );

        foreach ( $scripts_to_remove as $script_handle ) {
            wp_dequeue_script( $script_handle );
        }
    }

    /**
     * Add admin menu items.
     *
     * @since    1.0.0
     */
    public function add_admin_menu() {
        // Get count of new calls for notification badge
        $new_calls_count = SiteStaffr_Call_Manager::get_new_calls_count();
        $notification_badge = $new_calls_count > 0 ? sprintf( ' <span class="awaiting-mod">%d</span>', $new_calls_count ) : '';

        $svg_path = plugin_dir_path( __DIR__ ) . 'assets/images/menu-icon.svg';
        $svg = file_exists( $svg_path ) ? file_get_contents( $svg_path ) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin asset
        $icon = $svg ? 'data:image/svg+xml;base64,' . base64_encode( $svg ) : 'dashicons-admin-generic';

        $dashboard_page = add_menu_page(
            __( 'SiteStaffr', 'sitestaffr' ),
            __( 'SiteStaffr', 'sitestaffr' ) . $notification_badge,
            'manage_options',
            'sitestaffr',
            array( $this, 'display_admin_page' ),
            $icon,
            30
        );

        // 1. Dashboard
        add_submenu_page(
            'sitestaffr',
            __( 'Dashboard', 'sitestaffr' ),
            __( 'Dashboard', 'sitestaffr' ),
            'manage_options',
            'sitestaffr',
            array( $this, 'display_admin_page' )
        );

        // 2. Conversations
        $calls_page = add_submenu_page(
            'sitestaffr',
            __( 'Conversations', 'sitestaffr' ),
            __( 'Conversations', 'sitestaffr' ) . $notification_badge,
            'manage_options',
            'sitestaffr-view-calls',
            array( $this, 'display_calls_page' )
        );

        // 3. My AI Agent (consolidates Knowledge + Voice/Behavior settings)
        add_submenu_page(
            'sitestaffr',
            __( 'My AI Agent', 'sitestaffr' ),
            __( 'My AI Agent', 'sitestaffr' ),
            'manage_options',
            'sitestaffr-my-agent',
            array( $this, 'display_my_agent_page' )
        );

        // 4. Usage & Billing
        add_submenu_page(
            'sitestaffr',
            __( 'Usage & Billing', 'sitestaffr' ),
            __( 'Usage & Billing', 'sitestaffr' ),
            'manage_options',
            'sitestaffr-usage',
            array( $this, 'display_usage_dashboard' )
        );

        // 5. Widget & Button
        add_submenu_page(
            'sitestaffr',
            __( 'Widget & Button', 'sitestaffr' ),
            __( 'Widget & Button', 'sitestaffr' ),
            'manage_options',
            'sitestaffr-widget-settings',
            array( $this, 'display_widget_settings_page' )
        );

        // 6. Settings
        add_submenu_page(
            'sitestaffr',
            __( 'Settings', 'sitestaffr' ),
            __( 'Settings', 'sitestaffr' ),
            'manage_options',
            'sitestaffr-settings',
            array( $this, 'display_settings_page' )
        );

        // Diagnostics page (only when diagnostics files are present in the build).
        if ( file_exists( SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-diagnostics.php' ) ) {
            add_submenu_page(
                'sitestaffr',
                __( 'Diagnostics', 'sitestaffr' ),
                __( 'Diagnostics', 'sitestaffr' ),
                'manage_options',
                'sitestaffr-diagnostics',
                array( $this, 'display_diagnostics_page' )
            );
        }

        // Setup Wizard: hidden from nav menu, accessible via direct URL only.
        add_submenu_page(
            null,
            __( 'Setup Wizard', 'sitestaffr' ),
            __( 'Setup Wizard', 'sitestaffr' ),
            'manage_options',
            'sitestaffr-setup',
            array( $this, 'display_setup_wizard' )
        );

        // Hook to process call row/bulk actions before any output.
        add_action( 'load-' . $dashboard_page, array( $this, 'process_dashboard_actions' ) );
        add_action( 'load-' . $calls_page, array( $this, 'process_dashboard_actions' ) );
    }

    /**
     * Check if initial setup has been completed.
     *
     * @since    1.0.0
     * @return   bool    True if setup is complete, false otherwise.
     */
    public static function is_setup_complete() {
        // Check standalone setup completion option first (new method)
        $setup_completed = get_option( 'sitestaffr_setup_completed', false );
        if ( $setup_completed ) {
            return true;
        }

        // Fall back to settings array check (backwards compatibility).
        $settings = get_option( 'sitestaffr_settings', array() );

        // Check for the setup_complete flag.
        if ( isset( $settings['setup_complete'] ) && $settings['setup_complete'] ) {
            return true;
        }

        // Legacy installs may have a completion timestamp without the boolean flag.
        if ( ! empty( $settings['setup_completed_at'] ) ) {
            return true;
        }

        // Do not infer completion from partial fields (e.g., step 1 business info),
        // otherwise users can bypass required wizard steps.
        return false;
    }

    /**
     * Staging-only: reset description generation count via URL parameter.
     *
     * Visit wp-admin/?sitestaffr_reset_gen_count=1&_wpnonce=<nonce> on staging.sitestaffr.com
     * to reset the counter. Only works on the staging domain.
     */
    public function maybe_reset_staging_gen_count() {
        if ( wp_parse_url( home_url(), PHP_URL_HOST ) !== 'staging.sitestaffr.com' ) {
            return;
        }
        if ( ! isset( $_GET['sitestaffr_reset_gen_count'] ) ) {
            return;
        }
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        check_admin_referer( 'sitestaffr_reset_gen_count' );
        $settings = get_option( 'sitestaffr_settings', array() );
        $settings['description_gen_count'] = 0;
        update_option( 'sitestaffr_settings', $settings );
        wp_safe_redirect( admin_url( '?gen_count_reset=1' ) );
        exit;
    }

    /**
     * Redirect to setup wizard if setup not complete.
     *
     * This runs on admin_init to ensure all SiteStaffr pages are blocked
     * until the mandatory setup wizard is completed.
     *
     * @since    1.0.0
     */
    public function maybe_redirect_to_wizard() {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check.
        $current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';

        // Only check on SiteStaffr admin pages
        if ( '' === $current_page || strpos( $current_page, 'sitestaffr' ) !== 0 ) {
            return;
        }

        // Don't redirect if already on setup wizard
        if ( 'sitestaffr-setup' === $current_page ) {
            return;
        }

        // Don't redirect if setup is complete
        if ( self::is_setup_complete() ) {
            return;
        }

        // MANDATORY SETUP: Redirect all SiteStaffr pages to setup wizard
        // No exceptions - all features are blocked until setup is complete
        wp_safe_redirect( admin_url( 'admin.php?page=sitestaffr-setup' ) );
        exit;
    }

    /**
     * Redirect to setup wizard on first plugin activation.
     *
     * Uses a transient set during activation to redirect the admin once,
     * then marks the redirect as done so it never fires again.
     *
     * @since 1.13.8
     */
    public function maybe_redirect_after_activation() {
        if ( ! get_transient( 'sitestaffr_activation_redirect' ) ) {
            return;
        }

        delete_transient( 'sitestaffr_activation_redirect' );
        update_option( 'sitestaffr_activation_redirect_done', true );

        // Don't redirect on bulk activations.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only check for bulk activation flag.
        if ( isset( $_GET['activate-multi'] ) ) {
            return;
        }

        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        // Don't redirect if setup is already complete.
        if ( self::is_setup_complete() ) {
            return;
        }

        wp_safe_redirect( admin_url( 'admin.php?page=sitestaffr-setup' ) );
        exit;
    }

    /**
     * Filter the admin page title for conversation detail views.
     *
     * @since 1.13.8
     *
     * @param string $admin_title The page title with site name appended.
     * @param string $title       The original page title.
     * @return string Filtered admin title.
     */
    public function filter_admin_title( $admin_title, $title ) {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check for page title.
        $page   = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check for page title.
        $action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : '';

        if ( 'sitestaffr' === $page && 'view' === $action ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check for page title.
            $call_id = isset( $_GET['call_id'] ) ? absint( wp_unslash( $_GET['call_id'] ) ) : 0;
            if ( $call_id ) {
                return esc_html(
                    sprintf(
                        /* translators: %d: conversation ID number */
                        __( 'Conversation #%d', 'sitestaffr' ),
                        $call_id
                    )
                ) . ' &lsaquo; ' . esc_html__( 'SiteStaffr', 'sitestaffr' ) . ' &#8212; ' . esc_html( get_bloginfo( 'name' ) );
            }
        }

        return $admin_title;
    }

    /**
     * Show admin notice if setup is not complete.
     *
     * Displays a prominent notice on all admin pages (not just SiteStaffr pages)
     * to remind users to complete the setup wizard.
     *
     * @since    1.0.0
     */
    public function show_setup_incomplete_notice() {
        // Only show on admin pages
        if ( ! is_admin() ) {
            return;
        }

        // Don't show if setup is complete
        if ( self::is_setup_complete() ) {
            return;
        }

        // Don't show on the setup wizard page itself
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check.
        $current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
        if ( 'sitestaffr-setup' === $current_page ) {
            return;
        }

        // Show notice
        ?>
        <div class="notice notice-warning is-dismissible">
            <p>
                <strong><?php esc_html_e( 'SiteStaffr:', 'sitestaffr' ); ?></strong>
                <?php esc_html_e( 'Please complete the setup wizard to activate your AI voice agent.', 'sitestaffr' ); ?>
                <a href="<?php echo esc_url( admin_url( 'admin.php?page=sitestaffr-setup' ) ); ?>" class="button button-primary" style="margin-left: 10px;">
                    <?php esc_html_e( 'Complete Setup', 'sitestaffr' ); ?>
                </a>
            </p>
        </div>
        <?php
    }

    /**
     * Show admin notice when voice minutes are exhausted.
     *
     * Plan-aware messaging: "pro" users see add-on minutes CTA,
     * other plans see upgrade CTA.
     *
     * @since 1.13.8
     */
    public function show_minutes_exhausted_notice() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        if ( ! class_exists( 'SiteStaffr_Billing_State' ) || ! SiteStaffr_Billing_State::is_cutover_enabled() ) {
            return;
        }

        $balance = SiteStaffr_Billing_State::get_minutes_balance();
        if ( ! is_array( $balance ) || (int) $balance['total'] > 0 ) {
            return;
        }

        // Only show if the configured widget mode includes voice.
        $widget_settings = get_option( 'sitestaffr_widget_settings', array() );
        $widget_mode  = isset( $widget_settings['mode'] ) ? sanitize_key( $widget_settings['mode'] ) : 'both';
        $button_mode  = isset( $widget_settings['button_mode'] ) ? sanitize_key( $widget_settings['button_mode'] ) : 'both';
        if ( 'text' === $widget_mode && 'text' === $button_mode ) {
            return;
        }

        $cache    = SiteStaffr_Billing_State::get_cache();
        $plan_key = isset( $cache['plan_name'] ) ? sanitize_key( $cache['plan_name'] ) : 'starter';
        $usage_url = admin_url( 'admin.php?page=sitestaffr-usage' );

        if ( 'pro' === $plan_key ) {
            $cta_text = __( 'Purchase add-on minutes', 'sitestaffr' );
        } else {
            $cta_text = __( 'Upgrade your plan', 'sitestaffr' );
        }
        ?>
        <div class="notice notice-warning is-dismissible">
            <p>
                <strong><?php esc_html_e( 'SiteStaffr:', 'sitestaffr' ); ?></strong>
                <?php esc_html_e( 'Your voice AI minutes have been used up — visitors are seeing text-only chat.', 'sitestaffr' ); ?>
                <a href="<?php echo esc_url( $usage_url ); ?>" class="button button-primary" style="margin-left: 10px;">
                    <?php echo esc_html( $cta_text ); ?>
                </a>
            </p>
        </div>
        <?php
    }

    /**
     * Process dashboard actions before page load.
     *
     * Handles bulk actions and single row actions to prevent
     * "headers already sent" errors by processing before HTML output.
     *
     * @since    1.0.0
     */
    public function process_dashboard_actions() {
        // Process single row actions from GET requests.
        if ( isset( $_GET['action'] ) && isset( $_GET['call_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- presence check only; nonce verified immediately below.
            if ( ! current_user_can( 'manage_options' ) ) {
                wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'sitestaffr' ) );
            }

            $action  = sanitize_text_field( wp_unslash( $_GET['action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified below via $nonce_map.
            $call_id = absint( wp_unslash( $_GET['call_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified below via $nonce_map.

            // Verify nonce for the specific action before any processing.
            $nonce_map = array(
                'mark_read_single'        => 'sitestaffr_mark_read_' . $call_id,
                'mark_unread_single'      => 'sitestaffr_mark_unread_' . $call_id,
                'delete_single'           => 'sitestaffr_delete_' . $call_id,
                'restore_single'          => 'sitestaffr_restore_' . $call_id,
                'permanent_delete_single' => 'sitestaffr_permanent_delete_' . $call_id,
                'empty_trash'             => 'sitestaffr_empty_trash',
            );
            if ( ! isset( $nonce_map[ $action ] ) ) {
                return;
            }
            check_admin_referer( $nonce_map[ $action ] );

            $redirect_url = remove_query_arg( array( 'action', 'call_id', '_wpnonce' ) );

            switch ( $action ) {
                case 'mark_read_single':
                    SiteStaffr_Call_Manager::mark_as_read( array( $call_id ) );
                    $redirect_url = add_query_arg( array( 'message' => 'marked_read', '_wpnonce' => wp_create_nonce( 'sitestaffr_admin_notice' ) ), $redirect_url );
                    break;

                case 'mark_unread_single':
                    SiteStaffr_Call_Manager::mark_as_unread( array( $call_id ) );
                    $redirect_url = add_query_arg( array( 'message' => 'marked_unread', '_wpnonce' => wp_create_nonce( 'sitestaffr_admin_notice' ) ), $redirect_url );
                    break;

                case 'delete_single':
                    SiteStaffr_Call_Manager::trash_calls( array( $call_id ) );
                    $redirect_url = add_query_arg( array( 'message' => 'trashed', '_wpnonce' => wp_create_nonce( 'sitestaffr_admin_notice' ) ), $redirect_url );
                    break;

                case 'restore_single':
                    SiteStaffr_Call_Manager::restore_calls( array( $call_id ) );
                    $redirect_url = add_query_arg( array( 'message' => 'restored', '_wpnonce' => wp_create_nonce( 'sitestaffr_admin_notice' ) ), $redirect_url );
                    break;

                case 'permanent_delete_single':
                    SiteStaffr_Call_Manager::delete_calls( array( $call_id ) );
                    $redirect_url = add_query_arg( array( 'message' => 'permanently_deleted', 'view' => 'trash', '_wpnonce' => wp_create_nonce( 'sitestaffr_admin_notice' ) ), $redirect_url );
                    break;

                case 'empty_trash':
                    $trash_items = SiteStaffr_Call_Manager::get_calls( array( 'trash' => true, 'per_page' => 9999 ) );
                    $trash_ids = wp_list_pluck( $trash_items, 'id' );
                    if ( ! empty( $trash_ids ) ) {
                        SiteStaffr_Call_Manager::delete_calls( $trash_ids );
                    }
                    $redirect_url = add_query_arg( array( 'message' => 'trash_emptied', 'view' => 'trash', '_wpnonce' => wp_create_nonce( 'sitestaffr_admin_notice' ) ), $redirect_url );
                    break;
            }

            wp_safe_redirect( $redirect_url );
            exit;
        }

        // Process bulk actions from POST requests.
        if ( isset( $_POST['action'] ) || isset( $_POST['action2'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- presence check only; nonce verified immediately below.
            check_admin_referer( 'bulk-calls' );
            if ( ! current_user_can( 'manage_options' ) ) {
                wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'sitestaffr' ) );
            }

            $action = '-1';
            $action1_raw = isset( $_POST['action'] ) ? sanitize_text_field( wp_unslash( $_POST['action'] ) ) : '-1';
            $action2_raw = isset( $_POST['action2'] ) ? sanitize_text_field( wp_unslash( $_POST['action2'] ) ) : '-1';

            if ( $action1_raw !== '-1' ) {
                $action = $action1_raw;
            } elseif ( $action2_raw !== '-1' ) {
                $action = $action2_raw;
            }

            if ( $action !== '-1' && isset( $_POST['call_ids'] ) && is_array( $_POST['call_ids'] ) ) {
                $call_ids = array_map( 'absint', wp_unslash( $_POST['call_ids'] ) );

                if ( ! empty( $call_ids ) ) {
                    $message = '';

                    switch ( $action ) {
                        case 'mark_read':
                            SiteStaffr_Call_Manager::mark_as_read( $call_ids );
                            $message = 'bulk_marked_read';
                            break;

                        case 'mark_unread':
                            SiteStaffr_Call_Manager::mark_as_unread( $call_ids );
                            $message = 'bulk_marked_unread';
                            break;

                        case 'delete':
                            SiteStaffr_Call_Manager::trash_calls( $call_ids );
                            $message = 'bulk_trashed';
                            break;

                        case 'restore':
                            SiteStaffr_Call_Manager::restore_calls( $call_ids );
                            $message = 'bulk_restored';
                            break;

                        case 'permanent_delete':
                            SiteStaffr_Call_Manager::delete_calls( $call_ids );
                            $message = 'bulk_permanently_deleted';
                            break;
                    }

                    // Build redirect URL preserving filters and pagination
                    $redirect_url = remove_query_arg( array( 'action', 'action2' ) );

                    if ( ! empty( $message ) ) {
                        $redirect_url = add_query_arg( array( 'message' => $message, '_wpnonce' => wp_create_nonce( 'sitestaffr_admin_notice' ) ), $redirect_url );
                    }

                    // Preserve trash view for trash actions
                    if ( in_array( $action, array( 'restore', 'permanent_delete' ), true ) ) {
                        $redirect_url = add_query_arg( 'view', 'trash', $redirect_url );
                    }

                    wp_safe_redirect( $redirect_url );
                    exit;
                }
            }
        }
    }

    /**
     * Check if running in development environment.
     *
     * @since    1.0.0
     * @return   bool    True if dev environment, false otherwise.
     */
    private function is_dev_environment() {
        // Check if running on localhost or development site
        $http_host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
        $remote_addr = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
        $is_local = in_array( $remote_addr, array( '127.0.0.1', '::1' ), true ) ||
                    strpos( $http_host, '.local' ) !== false ||
                    strpos( $http_host, 'localhost' ) !== false;

        return $is_local || ( defined( 'WP_DEBUG' ) && WP_DEBUG );
    }

    /**
     * Register plugin settings.
     *
     * @since    1.0.0
     */
    public function register_settings() {

        // Register the settings
        register_setting(
            'sitestaffr_settings_group',
            'sitestaffr_settings',
            array( $this, 'sanitize_settings' )
        );

        // Register standalone realtime mode setting (boolean)
        register_setting(
            'sitestaffr_settings_group',
            'sitestaffr_use_realtime',
            array(
                'type'              => 'boolean',
                'default'           => false,
                'sanitize_callback' => 'rest_sanitize_boolean',
                'description'       => 'Enable low-latency voice streaming'
            )
        );

        // Register fragment-based prompt system setting (boolean) - FRAG-003
        register_setting(
            'sitestaffr_settings_group',
            'sitestaffr_enable_fragments',
            array(
                'type'              => 'boolean',
                'default'           => false,
                'sanitize_callback' => 'rest_sanitize_boolean',
                'description'       => 'Enable fragment-based prompt architecture for reduced latency'
            )
        );

        // Add Business Settings Section
        add_settings_section(
            'sitestaffr_business_section',
            __( 'Business Information', 'sitestaffr' ),
            array( $this, 'render_business_section' ),
            'sitestaffr-settings'
        );

        // Business Name
        add_settings_field(
            'business_name',
            __( 'Business Name', 'sitestaffr' ) . ' <span class="sitestaffr-tooltip-trigger" tabindex="0" role="button" aria-label="' . esc_attr__( 'Help', 'sitestaffr' ) . '" data-tooltip="' . esc_attr__( 'Your company or business name', 'sitestaffr' ) . '"><span class="dashicons dashicons-info" aria-hidden="true"></span></span>',
            array( $this, 'render_text_field' ),
            'sitestaffr-settings',
            'sitestaffr_business_section',
            array(
                'field_id' => 'business_name',
                'field_type' => 'text',
                'placeholder' => 'Your Business Name'
            )
        );

        // Business Hours - use custom editor instead of text field
        add_settings_field(
            'business_hours',
            __( 'Business Operating Hours', 'sitestaffr' ) . ' <span class="sitestaffr-tooltip-trigger" tabindex="0" role="button" aria-label="' . esc_attr__( 'Help', 'sitestaffr' ) . '" data-tooltip="' . esc_attr__( 'When is your physical business open? Your AI voice agent answers calls 24/7, but will inform visitors of your actual business hours.', 'sitestaffr' ) . '"><span class="dashicons dashicons-info" aria-hidden="true"></span></span>',
            array( $this, 'render_business_hours_field' ),
            'sitestaffr-settings',
            'sitestaffr_business_section',
            array(
                'field_id' => 'business_hours',
            )
        );

        // Business Phone Number
        add_settings_field(
            'business_phone',
            __( 'Business Phone Number', 'sitestaffr' ) . ' <span class="sitestaffr-tooltip-trigger" tabindex="0" role="button" aria-label="' . esc_attr__( 'Help', 'sitestaffr' ) . '" data-tooltip="' . esc_attr__( 'Your business\'s main contact number.', 'sitestaffr' ) . '"><span class="dashicons dashicons-info" aria-hidden="true"></span></span>',
            array( $this, 'render_phone_field' ),
            'sitestaffr-settings',
            'sitestaffr_business_section',
            array(
                'field_id' => 'business_phone',
                'placeholder' => '(555) 123-4567'
            )
        );

        // Business Description
        add_settings_field(
            'business_context',
            __( 'Business Identity', 'sitestaffr' ) . ' <span class="sitestaffr-tooltip-trigger" tabindex="0" role="button" aria-label="' . esc_attr__( 'Help', 'sitestaffr' ) . '" data-tooltip="' . esc_attr__( 'A brief identity for your business: what you do, who you serve, and what makes you different. Detailed service info lives in your Knowledge Base.', 'sitestaffr' ) . '"><span class="dashicons dashicons-info" aria-hidden="true"></span></span>',
            array( $this, 'render_textarea_field' ),
            'sitestaffr-settings',
            'sitestaffr_business_section',
            array(
                'field_id' => 'business_context',
                'placeholder' => 'e.g., We are a family-owned plumbing business serving the greater Austin area since 1995. We specialize in residential plumbing repairs, installations, and emergency services.',
                'rows' => 5
            )
        );

        // === Widget Settings Registration ===
        // Register widget settings option
        register_setting(
            'sitestaffr_widget_settings_group',
            'sitestaffr_widget_settings',
            array( $this, 'sanitize_widget_settings' )
        );

        // AI Voice Section
        add_settings_section(
            'sitestaffr_ai_voice_section',
            __( 'AI Voice Settings', 'sitestaffr' ),
            array( $this, 'render_ai_voice_section' ),
            'sitestaffr-widget-settings'
        );

        // AI Voice Selection
        add_settings_field(
            'ai_voice',
            __( 'AI Voice', 'sitestaffr' ) . ' <span class="sitestaffr-tooltip-trigger" tabindex="0" role="button" aria-label="' . esc_attr__( 'Help', 'sitestaffr' ) . '" data-tooltip="' . esc_attr__( 'Select the voice your AI voice agent will use. This setting applies to both widget types.', 'sitestaffr' ) . '"><span class="dashicons dashicons-info" aria-hidden="true"></span></span>',
            array( $this, 'render_voice_select_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_ai_voice_section',
            array(
                'field_id' => 'ai_voice',
            )
        );

        // Fixed Widget Section
        add_settings_section(
            'sitestaffr_fixed_widget_section',
            __( 'Fixed Widget Defaults', 'sitestaffr' ),
            array( $this, 'render_fixed_widget_section' ),
            'sitestaffr-widget-settings'
        );

        // Fixed Widget - Background Color
        add_settings_field(
            'fixed_background_color',
            __( 'Background Color', 'sitestaffr' ),
            array( $this, 'render_widget_color_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_fixed_widget_section',
            array(
                'field_id' => 'fixed_background_color',
                'default' => '#0073aa',
                'description' => __( 'Background color of the widget button.', 'sitestaffr' ),
            )
        );

        // Fixed Widget - Icon Color
        add_settings_field(
            'fixed_icon_color',
            __( 'Icon Color', 'sitestaffr' ),
            array( $this, 'render_widget_color_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_fixed_widget_section',
            array(
                'field_id' => 'fixed_icon_color',
                'default' => '#ffffff',
                'description' => __( 'Color of the microphone icon.', 'sitestaffr' ),
            )
        );

        // Fixed Widget - Size
        add_settings_field(
            'fixed_size',
            __( 'Size', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_fixed_widget_section',
            array(
                'field_id' => 'fixed_size',
                'default' => '60px',
                'description' => __( 'Size of the widget button (include units, e.g., 60px).', 'sitestaffr' ),
            )
        );

        // Button Widget Section
        add_settings_section(
            'sitestaffr_button_widget_section',
            __( 'Button Widget Defaults', 'sitestaffr' ),
            array( $this, 'render_button_widget_section' ),
            'sitestaffr-widget-settings'
        );

        // Button Widget - Text
        add_settings_field(
            'button_text',
            __( 'Button Text', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_text',
                'default' => 'Talk to Us',
                'description' => __( 'Text displayed on the button.', 'sitestaffr' ),
            )
        );

        // Button Widget - Background Color
        add_settings_field(
            'button_background_color',
            __( 'Background Color', 'sitestaffr' ),
            array( $this, 'render_widget_color_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_background_color',
                'default' => '#0073aa',
                'description' => __( 'Background color of the button.', 'sitestaffr' ),
            )
        );

        // Button Widget - Hover Background Color
        add_settings_field(
            'button_hover_background',
            __( 'Hover Background Color', 'sitestaffr' ),
            array( $this, 'render_widget_color_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_hover_background',
                'default' => '#005a87',
                'description' => __( 'Background color when hovering over button.', 'sitestaffr' ),
            )
        );

        // Button Widget - Text Color
        add_settings_field(
            'button_text_color',
            __( 'Text Color', 'sitestaffr' ),
            array( $this, 'render_widget_color_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_text_color',
                'default' => '#ffffff',
                'description' => __( 'Color of the button text.', 'sitestaffr' ),
            )
        );

        // Button Widget - Border Radius
        add_settings_field(
            'button_border_radius',
            __( 'Border Radius', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_border_radius',
                'default' => '4px',
                'description' => __( 'Roundness of button corners (e.g., 4px, 20px).', 'sitestaffr' ),
            )
        );

        // Button Widget - Icon
        add_settings_field(
            'button_icon',
            __( 'Icon', 'sitestaffr' ),
            array( $this, 'render_widget_select_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_icon',
                'options' => array(
                    'microphone' => __( 'Microphone', 'sitestaffr' ),
                    'phone' => __( 'Phone', 'sitestaffr' ),
                    'chat' => __( 'Chat', 'sitestaffr' ),
                    'none' => __( 'None', 'sitestaffr' ),
                ),
                'description' => __( 'Icon to display on the button.', 'sitestaffr' ),
            )
        );

        // Button Widget - Icon Position
        add_settings_field(
            'button_icon_position',
            __( 'Icon Position', 'sitestaffr' ),
            array( $this, 'render_widget_select_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_icon_position',
                'options' => array(
                    'left' => __( 'Left', 'sitestaffr' ),
                    'right' => __( 'Right', 'sitestaffr' ),
                ),
                'description' => __( 'Position of the icon relative to text.', 'sitestaffr' ),
            )
        );

        // Button Widget - Icon Size
        add_settings_field(
            'button_icon_size',
            __( 'Icon Size', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_icon_size',
                'default' => '18px',
                'description' => __( 'Size of the icon (e.g., 18px, 24px).', 'sitestaffr' ),
            )
        );

        // Button Widget - Font Size
        add_settings_field(
            'button_font_size',
            __( 'Font Size', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_font_size',
                'default' => '16px',
                'description' => __( 'Size of the button text (e.g., 14px, 16px, 18px).', 'sitestaffr' ),
            )
        );

        // Button Widget - Font Weight
        add_settings_field(
            'button_font_weight',
            __( 'Font Weight', 'sitestaffr' ),
            array( $this, 'render_widget_select_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_font_weight',
                'options' => array(
                    '400' => __( 'Normal (400)', 'sitestaffr' ),
                    '500' => __( 'Medium (500)', 'sitestaffr' ),
                    '600' => __( 'Semi-Bold (600)', 'sitestaffr' ),
                    '700' => __( 'Bold (700)', 'sitestaffr' ),
                ),
                'default' => '600',
                'description' => __( 'Weight of the button text.', 'sitestaffr' ),
            )
        );

        // Button Widget - Text Transform
        add_settings_field(
            'button_text_transform',
            __( 'Text Transform', 'sitestaffr' ),
            array( $this, 'render_widget_select_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_text_transform',
                'options' => array(
                    'none' => __( 'None', 'sitestaffr' ),
                    'uppercase' => __( 'UPPERCASE', 'sitestaffr' ),
                    'lowercase' => __( 'lowercase', 'sitestaffr' ),
                    'capitalize' => __( 'Capitalize', 'sitestaffr' ),
                ),
                'default' => 'none',
                'description' => __( 'Text capitalization style.', 'sitestaffr' ),
            )
        );

        // Button Widget - Gradient Toggle
        add_settings_field(
            'button_gradient',
            __( 'Gradient Background', 'sitestaffr' ),
            array( $this, 'render_widget_checkbox_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_gradient',
                'label' => __( 'Enable gradient background', 'sitestaffr' ),
                'description' => __( 'Use a gradient instead of solid color.', 'sitestaffr' ),
            )
        );

        // Button Widget - Gradient End Color
        add_settings_field(
            'button_gradient_end',
            __( 'Gradient End Color', 'sitestaffr' ),
            array( $this, 'render_widget_color_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_gradient_end',
                'default' => '#005a87',
                'description' => __( 'End color for gradient (background color is start color).', 'sitestaffr' ),
            )
        );

        // Button Widget - Border Width
        add_settings_field(
            'button_border_width',
            __( 'Border Width', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_border_width',
                'default' => '0',
                'description' => __( 'Width of the border (e.g., 0, 1px, 2px). Use 0 for no border.', 'sitestaffr' ),
            )
        );

        // Button Widget - Border Color
        add_settings_field(
            'button_border_color',
            __( 'Border Color', 'sitestaffr' ),
            array( $this, 'render_widget_color_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_border_color',
                'default' => '#0073aa',
                'description' => __( 'Color of the border (only visible if border width > 0).', 'sitestaffr' ),
            )
        );

        // Button Widget - Box Shadow Toggle
        add_settings_field(
            'button_box_shadow',
            __( 'Box Shadow', 'sitestaffr' ),
            array( $this, 'render_widget_checkbox_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_box_shadow',
                'label' => __( 'Enable box shadow', 'sitestaffr' ),
                'description' => __( 'Add a shadow effect to the button.', 'sitestaffr' ),
            )
        );

        // Button Widget - Shadow Color
        add_settings_field(
            'button_shadow_color',
            __( 'Shadow Color', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_shadow_color',
                'default' => 'rgba(0,0,0,0.2)',
                'description' => __( 'Color of the shadow (e.g., rgba(0,0,0,0.2), #000000).', 'sitestaffr' ),
            )
        );

        // Button Widget - Shadow Blur
        add_settings_field(
            'button_shadow_blur',
            __( 'Shadow Blur', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_shadow_blur',
                'default' => '10px',
                'description' => __( 'Blur radius of the shadow (e.g., 10px, 20px).', 'sitestaffr' ),
            )
        );

        // Button Widget - Shadow Offset
        add_settings_field(
            'button_shadow_offset',
            __( 'Shadow Offset', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_shadow_offset',
                'default' => '4px',
                'description' => __( 'Vertical offset of the shadow (e.g., 4px, 8px).', 'sitestaffr' ),
            )
        );

        // Button Widget - Padding X
        add_settings_field(
            'button_padding_x',
            __( 'Padding X (Horizontal)', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_padding_x',
                'default' => '24px',
                'description' => __( 'Horizontal padding (left/right) of the button (e.g., 24px, 32px).', 'sitestaffr' ),
            )
        );

        // Button Widget - Padding Y
        add_settings_field(
            'button_padding_y',
            __( 'Padding Y (Vertical)', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_padding_y',
                'default' => '12px',
                'description' => __( 'Vertical padding (top/bottom) of the button (e.g., 12px, 16px).', 'sitestaffr' ),
            )
        );

        // Button Widget - Full Width Toggle
        add_settings_field(
            'button_full_width',
            __( 'Full Width', 'sitestaffr' ),
            array( $this, 'render_widget_checkbox_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_full_width',
                'label' => __( 'Make button full width', 'sitestaffr' ),
                'description' => __( 'Button will expand to fill its container width.', 'sitestaffr' ),
            )
        );

        // Button Widget - Hover Animation
        add_settings_field(
            'button_hover_animation',
            __( 'Hover Animation', 'sitestaffr' ),
            array( $this, 'render_widget_select_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_hover_animation',
                'options' => array(
                    'none' => __( 'None', 'sitestaffr' ),
                    'scale' => __( 'Scale', 'sitestaffr' ),
                    'glow' => __( 'Glow', 'sitestaffr' ),
                    'pulse' => __( 'Pulse', 'sitestaffr' ),
                ),
                'default' => 'none',
                'description' => __( 'Animation effect when hovering over button.', 'sitestaffr' ),
            )
        );

        // Button Widget - Listening Text
        add_settings_field(
            'button_listening_text',
            __( 'Listening Text', 'sitestaffr' ),
            array( $this, 'render_widget_text_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_listening_text',
                'default' => 'Listening...',
                'description' => __( 'Text shown when the button is in listening state.', 'sitestaffr' ),
            )
        );

        // Button Widget - Listening Color
        add_settings_field(
            'button_listening_color',
            __( 'Listening Color', 'sitestaffr' ),
            array( $this, 'render_widget_color_field' ),
            'sitestaffr-widget-settings',
            'sitestaffr_button_widget_section',
            array(
                'field_id' => 'button_listening_color',
                'default' => '#e74c3c',
                'description' => __( 'Background color when button is in listening state.', 'sitestaffr' ),
            )
        );


        // Notification Email - MOVED to Email Notification Settings section in v0.5.10
        // Now manually rendered in admin/views/admin-settings.php within Email Notification Settings
        // This eliminates duplicate/redundant email field placement
        /*
        add_settings_field(
            'notification_email',
            __( 'Notification Email', 'sitestaffr' ) . ' <span class="sitestaffr-tooltip-trigger" tabindex="0" role="button" aria-label="' . esc_attr__( 'Help', 'sitestaffr' ) . '" data-tooltip="' . esc_attr__( 'Receive email notifications when calls are completed. You\'ll get transcripts and visitor information.', 'sitestaffr' ) . '"><span class="dashicons dashicons-info" aria-hidden="true"></span></span>',
            array( $this, 'render_text_field' ),
            'sitestaffr-settings',
            'sitestaffr_business_section',
            array(
                'field_id' => 'notification_email',
                'field_type' => 'email',
                'placeholder' => 'your@email.com'
            )
        );
        */
    }

    /**
     * Sanitize settings before saving.
     *
     * @since    1.0.0
     * @param    array    $input    The settings to sanitize.
     * @return   array              Sanitized settings.
     */
    public function sanitize_settings( $input ) {
        $sanitized = array();

        // Sanitize AI API credentials (middleware-managed)
        // Note: claude_api_key field kept for backward compatibility but no longer used
        if ( isset( $input['claude_api_key'] ) ) {
            $sanitized['claude_api_key'] = sanitize_text_field( $input['claude_api_key'] );
        }

        // Sanitize Business Name (required)
        if ( isset( $input['business_name'] ) ) {
            $business_name = trim( sanitize_text_field( $input['business_name'] ) );
            if ( '' === $business_name ) {
                add_settings_error(
                    'sitestaffr_settings',
                    'missing_business_name',
                    __( 'Business name is required.', 'sitestaffr' ),
                    'error'
                );

                $sanitized['business_name'] = $this->get_setting( 'business_name', 'sitestaffr_settings', '' );
            } else {
                $sanitized['business_name'] = $business_name;
            }
        }

        // Sanitize and validate Business Hours before saving
        if ( isset( $input['business_hours'] ) ) {
            $business_hours = sanitize_text_field( $input['business_hours'] );
            $validation = $this->validate_business_hours( $business_hours );

            if ( $validation['valid'] ) {
                update_option( 'sitestaffr_business_hours', $validation['sanitized'] );
                $sanitized['business_hours'] = $validation['sanitized'];
            } else {
                // Add settings error to show user the validation failure
                add_settings_error(
                    'sitestaffr_settings',
                    'invalid_business_hours',
                    $validation['error'],
                    'error'
                );
                // Don't save invalid hours - preserve existing value
                $existing_hours = get_option( 'sitestaffr_business_hours', '' );
                $sanitized['business_hours'] = $existing_hours;
            }
        }

        // Sanitize Business Phone and save to standalone option
        if ( isset( $input['business_phone'] ) ) {
            $business_phone = sanitize_text_field( $input['business_phone'] );
            update_option( 'sitestaffr_business_phone', $business_phone );
            $sanitized['business_phone'] = $business_phone;
        }

        // Sanitize Business Context and save to standalone option
        if ( isset( $input['business_context'] ) ) {
            $description_result = $this->sanitize_business_description( $input['business_context'] );
            $business_description = $description_result['value'];

            if ( $description_result['truncated'] ) {
                add_settings_error(
                    'sitestaffr_settings',
                    'business_description_truncated',
                    sprintf(
                        /* translators: %d is the max business description length */
                        __( 'Business description was too long and was trimmed to %d characters.', 'sitestaffr' ),
                        self::BUSINESS_DESCRIPTION_MAX_LENGTH
                    ),
                    'warning'
                );
            }

            update_option( 'sitestaffr_business_description', $business_description );
            $sanitized['business_context'] = $business_description;
        }

        if ( isset( $input['custom_greeting'] ) ) {
            $plan_key = $this->get_current_plan_key();

            if ( $this->is_custom_greeting_allowed_for_plan( $plan_key ) ) {
                $custom_greeting_result = $this->sanitize_custom_greeting( $input['custom_greeting'] );
                $custom_greeting = $custom_greeting_result['value'];

                if ( $custom_greeting_result['truncated'] ) {
                    add_settings_error(
                        'sitestaffr_settings',
                        'custom_greeting_truncated',
                        sprintf(
                            /* translators: %d is the max custom greeting length */
                            __( 'Custom greeting was too long and was trimmed to %d characters.', 'sitestaffr' ),
                            self::CUSTOM_GREETING_MAX_LENGTH
                        ),
                        'warning'
                    );
                }

                $sanitized['custom_greeting'] = $custom_greeting;
                if ( isset( $input['custom_greeting_tone'] ) ) {
                    $sanitized['custom_greeting_tone'] = $this->sanitize_custom_greeting_tone( $input['custom_greeting_tone'] );
                } else {
                    $sanitized['custom_greeting_tone'] = self::CUSTOM_GREETING_TONE_DEFAULT;
                }
            } else {
                $sanitized['custom_greeting'] = '';
                $sanitized['custom_greeting_tone'] = self::CUSTOM_GREETING_TONE_DEFAULT;
            }
        }

        // Sanitize AI Voice (v0.12.12)
        // Voice is stored in both places for compatibility:
        // - sitestaffr_settings[ai_voice] - displayed on Settings page
        // - sitestaffr_widget_settings[ai_voice] - used by widgets
        if ( isset( $input['ai_voice'] ) ) {
            $valid_voices = array(
                'marin', 'cedar', // Recommended
                'alloy', 'ash', 'coral', 'echo', 'sage', 'shimmer', // Standard
                'ballad', 'verse' // Expressive
            );
            $ai_voice = sanitize_text_field( $input['ai_voice'] );

            if ( in_array( $ai_voice, $valid_voices, true ) ) {
                $sanitized['ai_voice'] = $ai_voice;

                // Also update widget settings so widgets pick up the change
                $widget_settings = get_option( 'sitestaffr_widget_settings', array() );
                $widget_settings['ai_voice'] = $ai_voice;
                update_option( 'sitestaffr_widget_settings', $widget_settings );
            } else {
                $sanitized['ai_voice'] = 'marin'; // Default
            }
        }

        // Sanitize Notification Email
        if ( isset( $input['notification_email'] ) ) {
            $sanitized['notification_email'] = sanitize_email( $input['notification_email'] );
        }

        // Preserve setup completion status from existing settings
        $existing = get_option( 'sitestaffr_settings', array() );
        if ( isset( $existing['setup_complete'] ) ) {
            $sanitized['setup_complete'] = $existing['setup_complete'];
        }
        if ( isset( $existing['setup_completed_at'] ) ) {
            $sanitized['setup_completed_at'] = $existing['setup_completed_at'];
        }

        // Preserve email notification settings (managed by separate AJAX handler)
        if ( isset( $input['email_notifications_enabled'] ) ) {
            $sanitized['email_notifications_enabled'] = (bool) $input['email_notifications_enabled'];
        } elseif ( array_key_exists( 'email_notifications_enabled', $existing ) ) {
            $sanitized['email_notifications_enabled'] = $existing['email_notifications_enabled'];
        }
        if ( isset( $input['notification_email'] ) && ! isset( $sanitized['notification_email'] ) ) {
            $sanitized['notification_email'] = sanitize_email( $input['notification_email'] );
        } elseif ( isset( $existing['notification_email'] ) && ! isset( $sanitized['notification_email'] ) ) {
            $sanitized['notification_email'] = $existing['notification_email'];
        }
        if ( isset( $input['notification_cc_emails'] ) ) {
            $cc_parts = preg_split( '/[,;\r\n]+/', (string) $input['notification_cc_emails'] );
            $cc_clean = array();
            $cc_seen  = array();
            $max_cc_recipients = 5;

            if ( false !== $cc_parts ) {
                foreach ( $cc_parts as $cc_part ) {
                    $cc_candidate = trim( (string) $cc_part );
                    if ( '' === $cc_candidate ) {
                        continue;
                    }

                    $cc_email = sanitize_email( $cc_candidate );
                    if ( empty( $cc_email ) || ! is_email( $cc_email ) ) {
                        continue;
                    }

                    $cc_key = strtolower( $cc_email );
                    if ( isset( $cc_seen[ $cc_key ] ) ) {
                        continue;
                    }

                    $cc_seen[ $cc_key ] = true;
                    $cc_clean[] = $cc_email;
                }
            }
            if ( count( $cc_clean ) > $max_cc_recipients ) {
                $cc_clean = array_slice( $cc_clean, 0, $max_cc_recipients );
            }

            $sanitized['notification_cc_emails'] = ! empty( $cc_clean ) ? implode( ', ', $cc_clean ) : '';
        } elseif ( isset( $existing['notification_cc_emails'] ) ) {
            $sanitized['notification_cc_emails'] = sanitize_text_field( $existing['notification_cc_emails'] );
        }

        // Preserve AI instruction toggles (managed by separate AJAX handler on My AI Agent page).
        if ( isset( $input['ai_collect_contact'] ) ) {
            $sanitized['ai_collect_contact'] = (bool) $input['ai_collect_contact'];
        } elseif ( isset( $existing['ai_collect_contact'] ) ) {
            $sanitized['ai_collect_contact'] = (bool) $existing['ai_collect_contact'];
        }
        if ( isset( $input['ai_disregard_personal_info'] ) ) {
            $sanitized['ai_disregard_personal_info'] = (bool) $input['ai_disregard_personal_info'];
        } elseif ( isset( $existing['ai_disregard_personal_info'] ) ) {
            $sanitized['ai_disregard_personal_info'] = (bool) $existing['ai_disregard_personal_info'];
        }

        // Preserve knowledge_mode (managed by separate AJAX handler on AI Knowledge page).
        if ( isset( $input['knowledge_mode'] ) ) {
            $sanitized['knowledge_mode'] = sanitize_key( $input['knowledge_mode'] );
        } elseif ( isset( $existing['knowledge_mode'] ) ) {
            $sanitized['knowledge_mode'] = sanitize_key( $existing['knowledge_mode'] );
        }

        // Preserve custom_instructions_list (managed by separate AJAX handler on My AI Agent page).
        if ( isset( $input['custom_instructions_list'] ) && is_array( $input['custom_instructions_list'] ) ) {
            $sanitized['custom_instructions_list'] = $input['custom_instructions_list'];
        } elseif ( isset( $existing['custom_instructions_list'] ) && is_array( $existing['custom_instructions_list'] ) ) {
            $sanitized['custom_instructions_list'] = $existing['custom_instructions_list'];
        }

        // Preserve description generation billing-cycle usage fields so AJAX counter updates persist.
        $gen_max = $this->get_max_generations_for_plan( $this->get_current_plan_key() );
        if ( isset( $input['description_gen_count'] ) ) {
            $sanitized['description_gen_count'] = max(
                0,
                min( $gen_max, (int) $input['description_gen_count'] )
            );
        } elseif ( isset( $existing['description_gen_count'] ) ) {
            $sanitized['description_gen_count'] = max(
                0,
                min( $gen_max, (int) $existing['description_gen_count'] )
            );
        }

        $description_cycle_source = '';
        if ( isset( $input['description_gen_cycle_start'] ) ) {
            $description_cycle_source = sanitize_text_field( (string) $input['description_gen_cycle_start'] );
        } elseif ( isset( $existing['description_gen_cycle_start'] ) ) {
            $description_cycle_source = sanitize_text_field( (string) $existing['description_gen_cycle_start'] );
        }

        if ( '' !== $description_cycle_source ) {
            $description_cycle_ts = strtotime( $description_cycle_source );
            if ( false !== $description_cycle_ts ) {
                $sanitized['description_gen_cycle_start'] = gmdate( 'Y-m-d H:i:s', $description_cycle_ts );
            } elseif ( preg_match( '/^\d{4}-\d{2}$/', $description_cycle_source ) ) {
                // Legacy month key support (auto-migrated on next generation check).
                $sanitized['description_gen_cycle_start'] = $description_cycle_source;
            } else {
                $sanitized['description_gen_cycle_start'] = gmdate( 'Y-m-d H:i:s' );
            }
        }

        return $sanitized;
    }

    /**
     * Render Business section description.
     *
     * @since    1.0.0
     */
    public function render_business_section() {
        // Section is self-explanatory; no description needed.
    }

    /**
     * Render AI Voice section description.
     *
     * @since    0.12.4
     */
    public function render_ai_voice_section() {
        echo '<p>' . esc_html__( 'Choose the voice your AI voice agent will use. This setting applies to both the fixed and button widgets.', 'sitestaffr' ) . '</p>';
    }

    /**
     * Render AI voice selection field.
     *
     * @since    0.12.4
     * @param    array    $args    Field arguments.
     */
    public function render_voice_select_field( $args ) {
        $field_id = $args['field_id'];
        $current_value = $this->get_setting( $field_id, 'sitestaffr_widget_settings', 'marin' );

        // All voices in plan-tier order.
        $all_voices = array(
            'marin'   => 'Marin',
            'cedar'   => 'Cedar',
            'sage'    => 'Sage',
            'coral'   => 'Coral',
            'ash'     => 'Ash',
            'alloy'   => 'Alloy',
            'echo'    => 'Echo',
            'shimmer' => 'Shimmer',
            'verse'   => 'Verse',
            'ballad'  => 'Ballad',
        );

        if ( ! array_key_exists( $current_value, $all_voices ) ) {
            $current_value = 'marin';
        }

        // Determine allowed voices for current plan.
        $plan_key       = $this->get_current_plan_key();
        $allowed_voices = self::get_voices_for_plan( $plan_key );
        // Split into included vs locked.
        $included_voices = array();
        $locked_voices   = array();
        foreach ( $all_voices as $value => $label ) {
            if ( in_array( $value, $allowed_voices, true ) ) {
                $included_voices[ $value ] = $label;
            } else {
                $locked_voices[ $value ] = $label;
            }
        }

        ?>
        <div class="sitestaffr-voice-selector">
            <?php foreach ( $included_voices as $value => $label ) : ?>
                <div class="voice-option">
                    <input type="radio"
                           name="sitestaffr_widget_settings[<?php echo esc_attr( $field_id ); ?>]"
                           value="<?php echo esc_attr( $value ); ?>"
                           id="voice-<?php echo esc_attr( $value ); ?>"
                           <?php checked( $current_value, $value ); ?> />
                    <label for="voice-<?php echo esc_attr( $value ); ?>">
                        <span class="voice-name"><?php echo esc_html( $label ); ?></span>
                    </label>
                    <button type="button"
                            class="voice-preview-btn"
                            data-voice="<?php echo esc_attr( $value ); ?>"
                            title="<?php esc_attr_e( 'Preview voice', 'sitestaffr' ); ?>">
                        <span class="dashicons dashicons-controls-play"></span>
                        <?php esc_html_e( 'Preview', 'sitestaffr' ); ?>
                    </button>
                </div>
            <?php endforeach; ?>
        </div>
        <p class="description">
            <?php esc_html_e( 'Select the voice that best represents your brand. Click "Preview" to hear a sample.', 'sitestaffr' ); ?>
        </p>
        <?php if ( ! empty( $locked_voices ) ) : ?>
        <p class="description">
            <?php esc_html_e( 'Additional AI voices are available on higher plans.', 'sitestaffr' ); ?>
            <a href="<?php echo esc_url( admin_url( 'admin.php?page=sitestaffr-usage#purchase-minutes' ) ); ?>">
                <?php esc_html_e( 'View plans', 'sitestaffr' ); ?>
            </a>
        </p>
        <?php endif; ?>
        <?php
    }

    /**
     * Render AI Voice Agent section description.
     *
     * @since    0.12.11
     */
    public function render_ai_receptionist_voice_section() {
        echo '<p>' . esc_html__( 'Select and preview the AI voice that represents your business.', 'sitestaffr' ) . '</p>';
    }

    /**
     * Render AI Voice Selector for Settings Page.
     *
     * @since    0.12.11
     */
    public function render_ai_voice_selector() {
        // Get current voice from main settings (v0.12.12 - fixed to use correct settings array)
        $current_voice = $this->get_setting( 'ai_voice', 'sitestaffr_settings', 'marin' );

        // Voice data from shared method.
        $voice_data = self::get_voice_metadata();

        // Defensive fallback for legacy/removed voice IDs still stored in options.
        if ( ! isset( $voice_data[ $current_voice ] ) ) {
            $current_voice = 'marin';
        }

        // Filter to only voices available for the current plan.
        $plan_key_for_voices = $this->get_current_plan_key();
        $allowed_voices      = self::get_voices_for_plan( $plan_key_for_voices );
        $all_voice_count     = count( $voice_data );
        $voice_data          = array_intersect_key( $voice_data, array_flip( $allowed_voices ) );

        // If current voice is not in the allowed set, fall back to first available.
        if ( ! isset( $voice_data[ $current_voice ] ) ) {
            $current_voice = array_key_first( $voice_data );
        }

        // Output dropdown and display area
        ?>
        <div class="sitestaffr-ai-voice-container">
            <select name="sitestaffr_settings[ai_voice]" id="sitestaffr-voice-select" class="sitestaffr-voice-select-hidden" aria-hidden="true" tabindex="-1">
                <?php foreach ( $voice_data as $voice_id => $voice_info ) : ?>
                    <option value="<?php echo esc_attr( $voice_id ); ?>" <?php selected( $current_voice, $voice_id ); ?>>
                        <?php echo esc_html( $voice_info['name'] ); ?>
                    </option>
                <?php endforeach; ?>
            </select>

            <!-- Voice selector wrapper with arrows outside card -->
            <div class="sitestaffr-voice-selector-wrapper">
                <!-- Fixed-height voice display card -->
                <div class="sitestaffr-voice-display" id="sitestaffr-voice-display">
                    <!-- AI Voice Agent Avatar Image -->
                    <div class="sitestaffr-voice-image">
                        <button type="button" class="sitestaffr-voice-nav sitestaffr-voice-prev" aria-label="Previous voice">
                            <span class="dashicons dashicons-arrow-left-alt2"></span>
                        </button>

                        <img id="sitestaffr-voice-avatar"
                             src="<?php echo esc_url( SITESTAFFR_PLUGIN_URL . 'assets/images/' . $voice_data[$current_voice]['avatar'] ); ?>"
                             alt="<?php echo esc_attr( $voice_data[$current_voice]['name'] . ' avatar' ); ?>"
                             data-avatar-path="<?php echo esc_attr( $voice_data[$current_voice]['avatar'] ); ?>" />

                        <button type="button" class="sitestaffr-voice-nav sitestaffr-voice-next" aria-label="Next voice">
                            <span class="dashicons dashicons-arrow-right-alt2"></span>
                        </button>
                    </div>

                    <!-- Voice details -->
                    <div class="sitestaffr-voice-details">
                        <p class="sitestaffr-voice-save-status" id="sitestaffr-voice-save-status" aria-live="polite"></p>
                        <div class="sitestaffr-voice-header">
                            <h3 class="sitestaffr-voice-name" id="sitestaffr-voice-name">
                                <?php echo esc_html( $voice_data[$current_voice]['name'] ); ?>
                            </h3>
                            <span class="sitestaffr-voice-badge sitestaffr-voice-badge-<?php echo esc_attr( $voice_data[$current_voice]['category'] ); ?>" id="sitestaffr-voice-badge">
                                <?php
                                $category = $voice_data[$current_voice]['category'];
                                if ( $category === 'recommended' ) {
                                    echo '⭐ Recommended';
                                } elseif ( $category === 'expressive' ) {
                                    echo 'Expressive';
                                } else {
                                    echo 'Standard';
                                }
                                ?>
                            </span>
                        </div>
                        <p class="sitestaffr-voice-tagline" id="sitestaffr-voice-tagline">
                            <?php echo esc_html( $voice_data[$current_voice]['tagline'] ); ?>
                        </p>
                        <!-- Fixed-height description wrapper to prevent layout shift -->
                        <div class="sitestaffr-voice-description-wrapper">
                            <p class="sitestaffr-voice-description" id="sitestaffr-voice-description">
                                <?php echo esc_html( $voice_data[$current_voice]['description'] ); ?>
                            </p>
                        </div>
                        <div class="sitestaffr-voice-best-for" id="sitestaffr-voice-best-for">
                            <div class="sitestaffr-voice-best-for-label"><?php esc_html_e( 'Best for', 'sitestaffr' ); ?></div>
                            <div class="sitestaffr-voice-best-for-values">
                                <?php
                                $best_for_values = isset( $voice_data[ $current_voice ]['best_for'] ) ? $voice_data[ $current_voice ]['best_for'] : '';
                                $best_for_items = array_filter(
                                    array_map(
                                        'trim',
                                        explode( '|', (string) $best_for_values )
                                    )
                                );
                                if ( ! empty( $best_for_items ) ) :
                                    foreach ( $best_for_items as $best_for_index => $best_for_item ) :
                                        if ( $best_for_index > 0 ) :
                                            ?>
                                            <span class="sitestaffr-voice-best-for-separator">|</span>
                                            <?php
                                        endif;
                                        echo esc_html( $best_for_item );
                                    endforeach;
                                endif;
                                ?>
                            </div>
                        </div>
                    </div>

                    <div class="sitestaffr-voice-preview-column">
                        <button
                            type="button"
                            class="button sitestaffr-voice-preview-btn sitestaffr-voice-preview-btn--icon"
                            id="sitestaffr-voice-preview-btn"
                            data-voice="<?php echo esc_attr( $current_voice ); ?>"
                            aria-label="<?php esc_attr_e( 'Preview Voice', 'sitestaffr' ); ?>"
                            title="<?php esc_attr_e( 'Preview Voice', 'sitestaffr' ); ?>">
                            <span class="dashicons dashicons-controls-play" aria-hidden="true"></span>
                        </button>
                        <span class="sitestaffr-voice-preview-caption"><?php esc_html_e( 'Preview Voice', 'sitestaffr' ); ?></span>
                    </div>
                </div>

            </div>

            <div class="sitestaffr-voice-avatar-strip" role="radiogroup" aria-label="<?php esc_attr_e( 'Choose AI Voice', 'sitestaffr' ); ?>">
                <?php foreach ( $voice_data as $voice_id => $voice_info ) : ?>
                    <button
                        type="button"
                        class="sitestaffr-voice-avatar-option<?php echo $current_voice === $voice_id ? ' is-active' : ''; ?>"
                        data-voice="<?php echo esc_attr( $voice_id ); ?>"
                        role="radio"
                        aria-checked="<?php echo $current_voice === $voice_id ? 'true' : 'false'; ?>"
                        title="<?php echo esc_attr( $voice_info['name'] ); ?>">
                        <img
                            src="<?php echo esc_url( SITESTAFFR_PLUGIN_URL . 'assets/images/' . $voice_info['avatar'] ); ?>"
                            alt="<?php echo esc_attr( $voice_info['name'] . ' avatar' ); ?>" />
                        <span class="sitestaffr-voice-avatar-name"><?php echo esc_html( $voice_info['name'] ); ?></span>
                    </button>
                <?php endforeach; ?>
            </div>
        </div>

        <?php if ( $all_voice_count > count( $voice_data ) ) : ?>
        <p class="description">
            <?php esc_html_e( 'Additional AI voices are available on higher plans.', 'sitestaffr' ); ?>
            <a href="<?php echo esc_url( admin_url( 'admin.php?page=sitestaffr-usage#purchase-minutes' ) ); ?>">
                <?php esc_html_e( 'View plans', 'sitestaffr' ); ?>
            </a>
        </p>
        <?php endif; ?>
        <?php
    }

    /**
     * Render a text input field.
     *
     * @since    1.0.0
     * @param    array    $args    Field arguments.
     */
    public function render_text_field( $args ) {
        $field_id = $args['field_id'];
        $field_type = $args['field_type'];
        $placeholder = isset( $args['placeholder'] ) ? $args['placeholder'] : '';
        // Required attribute is applied inline in the printf below for PCP escaping compliance.

        // Check standalone option first for business_hours and business_phone, then fall back to settings array
        if ( $field_id === 'business_hours' ) {
            $value = get_option( 'sitestaffr_business_hours', '' );
            if ( empty( $value ) ) {
                $value = $this->get_setting( $field_id );
            }
        } elseif ( $field_id === 'business_phone' ) {
            $value = get_option( 'sitestaffr_business_phone', '' );
            if ( empty( $value ) ) {
                $value = $this->get_setting( $field_id );
            }
        } else {
            $value = $this->get_setting( $field_id );
        }

        // Mask sensitive fields if they have a value
        if ( $field_type === 'password' && ! empty( $value ) ) {
            $display_value = str_repeat( '•', 20 );
        } else {
            $display_value = esc_attr( $value );
        }

        $is_required = ( 'business_name' === $field_id );
        printf(
            '<input type="%s" id="%s" name="sitestaffr_settings[%s]" value="%s" placeholder="%s" class="regular-text"%s />',
            esc_attr( $field_type ),
            esc_attr( $field_id ),
            esc_attr( $field_id ),
            esc_attr( $display_value ),
            esc_attr( $placeholder ),
            $is_required ? ' required aria-required="true"' : ''
        );

        // Add description for password fields
        if ( $field_type === 'password' && ! empty( $value ) ) {
            echo '<p class="description">' . esc_html__( 'Leave blank to keep existing value.', 'sitestaffr' ) . '</p>';
        }
    }

    /**
     * Render a textarea field.
     *
     * @since    1.0.0
     * @param    array    $args    Field arguments.
     */
    public function render_textarea_field( $args ) {
        $field_id = $args['field_id'];
        $placeholder = isset( $args['placeholder'] ) ? $args['placeholder'] : '';
        $rows = isset( $args['rows'] ) ? intval( $args['rows'] ) : 5;

        // Check standalone option first for business_context, then fall back to settings array
        if ( $field_id === 'business_context' ) {
            $value = get_option( 'sitestaffr_business_description', '' );
            if ( empty( $value ) ) {
                $value = $this->get_setting( $field_id );
            }
        } else {
            $value = $this->get_setting( $field_id );
        }

        $is_business_context = ( 'business_context' === $field_id );
        printf(
            '<textarea id="%s" name="sitestaffr_settings[%s]" placeholder="%s" rows="%d" class="large-text"%s>%s</textarea>',
            esc_attr( $field_id ),
            esc_attr( $field_id ),
            esc_attr( $placeholder ),
            intval( $rows ),
            $is_business_context ? ' maxlength="' . intval( self::BUSINESS_DESCRIPTION_MAX_LENGTH ) . '" data-maxlength="' . intval( self::BUSINESS_DESCRIPTION_MAX_LENGTH ) . '"' : '',
            esc_textarea( $value )
        );

        if ( $is_business_context ) {
            $current_length = strlen( $value );
            printf(
                '<p id="sitestaffr-business-context-counter" class="sitestaffr-char-count" data-maxlength="%d"><span class="sitestaffr-char-current">%d</span> / %d</p>',
                intval( self::BUSINESS_DESCRIPTION_MAX_LENGTH ),
                intval( $current_length ),
                intval( self::BUSINESS_DESCRIPTION_MAX_LENGTH )
            );
        }
    }

    /**
     * Render custom greeting field with plan-based lock state.
     *
     * @since 0.14.210
     * @param array $args Field arguments.
     * @return void
     */
    public function render_custom_greeting_field( $args ) {
        $placeholder = isset( $args['placeholder'] ) ? (string) $args['placeholder'] : '';
        $value = $this->get_setting( 'custom_greeting', 'sitestaffr_settings', '' );
        $value = is_string( $value ) ? $value : '';
        $tone_value = $this->sanitize_custom_greeting_tone( $this->get_setting( 'custom_greeting_tone', 'sitestaffr_settings', self::CUSTOM_GREETING_TONE_DEFAULT ) );
        $tone_options = $this->get_custom_greeting_tone_options();
        $business_name = $this->get_setting( 'business_name', 'sitestaffr_settings', '' );
        $business_name = is_string( $business_name ) ? trim( $business_name ) : '';
        $placeholder_business_name = ! empty( $business_name ) ? $business_name : __( 'your business', 'sitestaffr' );
        $placeholder = str_replace( '[business name]', $placeholder_business_name, $placeholder );
        $current_length = function_exists( 'mb_strlen' ) ? mb_strlen( $value, 'UTF-8' ) : strlen( $value );
        ?>
        <input
            type="text"
            id="custom_greeting"
            name="sitestaffr_settings[custom_greeting]"
            value="<?php echo esc_attr( $value ); ?>"
            placeholder="<?php echo esc_attr( $placeholder ); ?>"
            maxlength="<?php echo esc_attr( self::CUSTOM_GREETING_MAX_LENGTH ); ?>"
            class="regular-text sitestaffr-custom-greeting-input"
            data-maxlength="<?php echo esc_attr( self::CUSTOM_GREETING_MAX_LENGTH ); ?>" />
        <p id="sitestaffr-custom-greeting-counter" class="sitestaffr-char-count" data-maxlength="<?php echo esc_attr( self::CUSTOM_GREETING_MAX_LENGTH ); ?>">
            <span class="sitestaffr-char-current"><?php echo esc_html( (string) $current_length ); ?></span> / <?php echo esc_html( (string) self::CUSTOM_GREETING_MAX_LENGTH ); ?>
        </p>
        <div class="sitestaffr-custom-greeting-tone-row">
            <label for="custom_greeting_tone" class="sitestaffr-custom-greeting-tone-label">
                <?php esc_html_e( 'Greeting Tone', 'sitestaffr' ); ?>
            </label>
            <select id="custom_greeting_tone" name="sitestaffr_settings[custom_greeting_tone]" class="sitestaffr-custom-greeting-tone-select">
                <?php foreach ( $tone_options as $tone_key => $tone_label ) : ?>
                    <option value="<?php echo esc_attr( $tone_key ); ?>" <?php selected( $tone_value, $tone_key ); ?>>
                    <?php echo esc_html( $tone_label ); ?>
                </option>
            <?php endforeach; ?>
            </select>
            <button
                type="button"
                id="sitestaffr-greeting-tone-preview-btn"
                class="button button-secondary sitestaffr-greeting-tone-preview-btn"
                data-tone="<?php echo esc_attr( $tone_value ); ?>"
                aria-label="<?php esc_attr_e( 'Preview greeting tone', 'sitestaffr' ); ?>"
                title="<?php esc_attr_e( 'Preview greeting tone', 'sitestaffr' ); ?>">
                <span class="dashicons dashicons-controls-play" aria-hidden="true"></span>
                <span class="sitestaffr-greeting-tone-preview-text"><?php esc_html_e( 'Preview', 'sitestaffr' ); ?></span>
            </button>
        </div>
        <?php
    }

    /**
     * Render a phone input field with auto-formatting.
     *
     * @since    1.2.8
     * @param    array    $args    Field arguments.
     */
    public function render_phone_field( $args ) {
        $field_id = $args['field_id'];
        $placeholder = isset( $args['placeholder'] ) ? $args['placeholder'] : '';

        // Check standalone option first for business_phone, then fall back to settings array
        $value = get_option( 'sitestaffr_business_phone', '' );
        if ( empty( $value ) ) {
            $value = $this->get_setting( $field_id );
        }


        printf(
            '<input type="tel" id="%s" name="sitestaffr_settings[%s]" value="%s" placeholder="%s" class="regular-text sitestaffr-phone-input" maxlength="14" pattern="[\d\s\(\)\-]+" />',
            esc_attr( $field_id ),
            esc_attr( $field_id ),
            esc_attr( $value ),
            esc_attr( $placeholder )
        );

    }

    /**
     * Render business hours field with display/edit interface.
     *
     * @since    1.3.9
     * @param    array    $args    Field arguments.
     */
    public function render_business_hours_field( $args ) {
        $field_id = $args['field_id'];

        // Check standalone option first
        $value = get_option( 'sitestaffr_business_hours', '' );
        if ( empty( $value ) ) {
            $value = $this->get_setting( $field_id );
        }

        // Default to Standard if empty
        if ( empty( $value ) ) {
            $value = 'Mon-Fri 9am-5pm, Sat-Sun Closed';
        }

        // Include the business hours editor template
        include SITESTAFFR_PLUGIN_DIR . 'admin/views/partials/business-hours-editor.php';
    }

    /**
     * Render a select dropdown field.
     *
     * @since    1.0.0
     * @param    array    $args    Field arguments.
     */
    public function render_select_field( $args ) {
        $field_id = $args['field_id'];
        $options = isset( $args['options'] ) ? $args['options'] : array();
        $default = isset( $args['default'] ) ? $args['default'] : '';
        $description = isset( $args['description'] ) ? $args['description'] : '';

        // Use default value if setting is not set
        $value = $this->get_setting( $field_id, 'sitestaffr_settings', $default );

        // Special handling for ai_model field - default to middleware-managed model if not set
        if ( $field_id === 'ai_model' && empty( $value ) ) {
            $value = 'middleware-default';
        }

        printf(
            '<select id="%s" name="sitestaffr_settings[%s]" class="regular-text">',
            esc_attr( $field_id ),
            esc_attr( $field_id )
        );

        foreach ( $options as $option_value => $option_label ) {
            printf(
                '<option value="%s" %s>%s</option>',
                esc_attr( $option_value ),
                selected( $value, $option_value, false ),
                esc_html( $option_label )
            );
        }

        echo '</select>';

        // Display description if provided
        if ( ! empty( $description ) ) {
            printf(
                '<p class="description">%s</p>',
                esc_html( $description )
            );
        }
    }

    /**
     * Render realtime voice mode toggle.
     *
     * @since    0.11.60
     */
    public function render_realtime_toggle() {
        $use_realtime = get_option( 'sitestaffr_use_realtime', false );
        ?>
        <label style="display: flex; align-items: center; gap: 8px;">
            <input
                type="checkbox"
                name="sitestaffr_use_realtime"
                id="sitestaffr_use_realtime"
                value="1"
                <?php checked( $use_realtime, true ); ?>
                style="width: 20px; height: 20px; vertical-align: middle;"
            />
            <span><?php esc_html_e( 'Enable Realtime Voice (Natural AI conversations with instant responses)', 'sitestaffr' ); ?></span>
        </label>
        <p class="description" style="margin-top: 10px;">
            <?php esc_html_e( 'When enabled, calls use low-latency voice streaming for natural conversations with instant responses. When disabled, traditional text-based mode is used (slower but more stable).', 'sitestaffr' ); ?>
        </p>

        <div id="sitestaffr-realtime-status" style="margin-top: 15px; padding: 12px; background: #f0f0f1; border-left: 4px solid #2271b1; border-radius: 4px;">
            <strong><?php esc_html_e( 'Current Mode:', 'sitestaffr' ); ?></strong>
            <span id="current-mode-status" style="font-weight: bold; color: #2271b1;">
                <?php echo $use_realtime ? esc_html__( 'Realtime Voice (Fast & Natural)', 'sitestaffr' ) : esc_html__( 'Text-Based (Traditional)', 'sitestaffr' ); ?>
            </span>
        </div>

        <?php
    }

    /**
     * Render fixed widget section description.
     *
     * @since    0.11.94
     */
    public function render_fixed_widget_section() {
        echo '<p>' . esc_html__( 'Configure default settings for the fixed floating widget.', 'sitestaffr' ) . '</p>';

        // Live Preview Panel
        echo '<div class="sitestaffr-preview-panel">';
        echo '<h4>' . esc_html__( 'Live Preview', 'sitestaffr' ) . '</h4>';
        echo '<div id="sitestaffr-fixed-widget-preview"></div>';
        echo '</div>';

        // Shortcode Reference
        echo '<div class="sitestaffr-shortcode-reference">';
        echo '<h4>' . esc_html__( 'Shortcode Usage', 'sitestaffr' ) . '</h4>';
        echo '<p>' . esc_html__( 'Add the fixed widget to your site:', 'sitestaffr' ) . '</p>';
        echo '<pre><code>[sitestaffr_widget]</code></pre>';
        echo '<p>' . esc_html__( 'The settings below apply as defaults. You can override them with shortcode attributes:', 'sitestaffr' ) . '</p>';
        echo '<pre><code>[sitestaffr_widget position="bottom-left" background_color="#ff5733"]</code></pre>';
        echo '</div>';
    }

    /**
     * Render button widget section description.
     *
     * @since    0.11.94
     */
    public function render_button_widget_section() {
        echo '<p>' . esc_html__( 'Configure default settings for the inline button widget.', 'sitestaffr' ) . '</p>';

        // Live Preview Panel
        echo '<div class="sitestaffr-preview-panel">';
        echo '<h4>' . esc_html__( 'Live Preview', 'sitestaffr' ) . '</h4>';
        echo '<div id="sitestaffr-button-widget-preview"></div>';
        echo '</div>';

        // Shortcode Reference
        echo '<div class="sitestaffr-shortcode-reference">';
        echo '<h4>' . esc_html__( 'Shortcode Usage', 'sitestaffr' ) . '</h4>';
        echo '<p>' . esc_html__( 'Add a button widget to any page or post:', 'sitestaffr' ) . '</p>';
        echo '<pre><code>[sitestaffr_button]</code></pre>';
        echo '<p>' . esc_html__( 'The settings below apply as defaults. You can override them with shortcode attributes:', 'sitestaffr' ) . '</p>';
        echo '<pre><code>[sitestaffr_button text="Call Now" background_color="#ff5733" icon="phone"]</code></pre>';
        echo '<p>' . esc_html__( 'Available icons: microphone, phone, chat, none', 'sitestaffr' ) . '</p>';
        echo '</div>';
    }

    /**
     * Render Advanced Settings section description.
     *
     * @since    0.12.18
     */
    public function render_advanced_section() {
        echo '<p>' . esc_html__( 'Experimental features for advanced users. Use with caution.', 'sitestaffr' ) . '</p>';
    }

    /**
     * Render Fragment System toggle field.
     *
     * @since    0.12.18
     * @param    array    $args    Field arguments.
     */
    public function render_fragment_toggle_field( $args ) {
        $enabled = get_option( 'sitestaffr_enable_fragments', false );

        printf(
            '<label><input type="checkbox" id="sitestaffr_enable_fragments" name="sitestaffr_enable_fragments" value="1" %s /> %s</label>',
            checked( $enabled, true, false ),
            esc_html__( 'Enable fragment-based prompt system', 'sitestaffr' )
        );

        echo '<p class="description">';
        esc_html_e( 'This experimental feature sends smaller, context-relevant prompts instead of the full prompt on every turn. This can reduce AI response latency by 20-30% but is still in testing. If you notice any drop in conversation quality, disable this setting.', 'sitestaffr' );
        echo '</p>';

        // Show warning if constant override is active
        if ( defined( 'SITESTAFFR_ENABLE_FRAGMENTS' ) ) {
            $constant_value = SITESTAFFR_ENABLE_FRAGMENTS ? 'enabled' : 'disabled';
            echo '<p class="description" style="color: #d63638; font-weight: 600;">';
            printf(
                // translators: %s: the constant value (enabled or disabled).
                esc_html__( 'Note: This setting is currently overridden by the SITESTAFFR_ENABLE_FRAGMENTS constant in wp-config.php (set to %s).', 'sitestaffr' ),
                esc_html( $constant_value )
            );
            echo '</p>';
        }
    }

    /**
     * Render a text field for widget settings.
     *
     * @since    0.11.94
     * @param    array    $args    Field arguments.
     */
    public function render_widget_text_field( $args ) {
        $field_id = $args['field_id'];
        $default = isset( $args['default'] ) ? $args['default'] : '';
        $description = isset( $args['description'] ) ? $args['description'] : '';

        $value = $this->get_setting( $field_id, 'sitestaffr_widget_settings', $default );

        printf(
            '<input type="text" id="%s" name="sitestaffr_widget_settings[%s]" value="%s" class="regular-text" />',
            esc_attr( $field_id ),
            esc_attr( $field_id ),
            esc_attr( $value )
        );

        if ( ! empty( $description ) ) {
            printf( '<p class="description">%s</p>', esc_html( $description ) );
        }
    }

    /**
     * Render a select field for widget settings.
     *
     * @since    0.11.94
     * @param    array    $args    Field arguments.
     */
    public function render_widget_select_field( $args ) {
        $field_id = $args['field_id'];
        $options = isset( $args['options'] ) ? $args['options'] : array();
        $default = isset( $args['default'] ) ? $args['default'] : '';
        $description = isset( $args['description'] ) ? $args['description'] : '';

        // Get first option as default if no default specified
        if ( empty( $default ) && ! empty( $options ) ) {
            $default = key( $options );
        }

        $value = $this->get_setting( $field_id, 'sitestaffr_widget_settings', $default );

        printf(
            '<select id="%s" name="sitestaffr_widget_settings[%s]" class="regular-text">',
            esc_attr( $field_id ),
            esc_attr( $field_id )
        );

        foreach ( $options as $option_value => $option_label ) {
            printf(
                '<option value="%s" %s>%s</option>',
                esc_attr( $option_value ),
                selected( $value, $option_value, false ),
                esc_html( $option_label )
            );
        }

        echo '</select>';

        if ( ! empty( $description ) ) {
            printf( '<p class="description">%s</p>', esc_html( $description ) );
        }
    }

    /**
     * Render a color field for widget settings.
     *
     * @since    0.11.94
     * @param    array    $args    Field arguments.
     */
    public function render_widget_color_field( $args ) {
        $field_id = $args['field_id'];
        $default = isset( $args['default'] ) ? $args['default'] : '#0073aa';
        $description = isset( $args['description'] ) ? $args['description'] : '';

        $value = $this->get_setting( $field_id, 'sitestaffr_widget_settings', $default );

        printf(
            '<input type="text" id="%s" name="sitestaffr_widget_settings[%s]" value="%s" class="sitestaffr-color-field" data-default-color="%s" />',
            esc_attr( $field_id ),
            esc_attr( $field_id ),
            esc_attr( $value ),
            esc_attr( $default )
        );

        if ( ! empty( $description ) ) {
            printf( '<p class="description">%s</p>', esc_html( $description ) );
        }
    }

    /**
     * Render a checkbox field for widget settings.
     *
     * @since    0.11.94
     * @param    array    $args    Field arguments.
     */
    public function render_widget_checkbox_field( $args ) {
        $field_id = $args['field_id'];
        $label = isset( $args['label'] ) ? $args['label'] : '';
        $description = isset( $args['description'] ) ? $args['description'] : '';

        $value = $this->get_setting( $field_id, 'sitestaffr_widget_settings', false );

        printf(
            '<label><input type="checkbox" id="%s" name="sitestaffr_widget_settings[%s]" value="1" %s /> %s</label>',
            esc_attr( $field_id ),
            esc_attr( $field_id ),
            checked( $value, true, false ),
            esc_html( $label )
        );

        if ( ! empty( $description ) ) {
            printf( '<p class="description">%s</p>', esc_html( $description ) );
        }
    }

    /**
     * Sanitize widget settings before saving.
     *
     * @since    0.11.94
     * @param    array    $input    The settings to sanitize.
     * @return   array              Sanitized settings.
     */
    public function sanitize_widget_settings( $input ) {
        $sanitized = array();
        $sanitize_px_radius = static function( $raw_value, $fallback_px ) {
            $radius_int = intval( $raw_value );
            if ( $radius_int < 0 || $radius_int > 200 ) {
                return $fallback_px;
            }
            return $radius_int . 'px';
        };

        // AI Voice Setting (applies to both widget types)
        if ( isset( $input['ai_voice'] ) ) {
            // Valid voice options (13 total)
            $valid_voices = array(
                'marin', 'cedar', // Recommended
                'alloy', 'ash', 'coral', 'echo', 'fable', 'nova', 'onyx', 'sage', 'shimmer', // Standard
                'ballad', 'verse' // Premium
            );
            $sanitized['ai_voice'] = in_array( $input['ai_voice'], $valid_voices, true )
                ? $input['ai_voice']
                : 'marin'; // Default to marin (recommended)
        }

        // Widget Mode (fixed widget)
        $valid_modes = array( 'voice', 'text', 'both' );
        if ( isset( $input['mode'] ) && in_array( $input['mode'], $valid_modes, true ) ) {
            $sanitized['mode'] = $input['mode'];
        }

        // Button Mode (button widget)
        if ( isset( $input['button_mode'] ) && in_array( $input['button_mode'], $valid_modes, true ) ) {
            $sanitized['button_mode'] = $input['button_mode'];
        }

        // Fixed Widget Auto-Display
        if ( isset( $input['fixed_auto_display'] ) ) {
            $sanitized['fixed_auto_display'] = ! empty( $input['fixed_auto_display'] );
        }

        // Fixed Widget Settings
        $valid_fixed_icons = array( 'phone', 'microphone', 'chat', 'headset', 'sitestaffr' );
        $sanitized['fixed_icon'] = isset( $input['fixed_icon'] ) && in_array( $input['fixed_icon'], $valid_fixed_icons, true ) ? $input['fixed_icon'] : 'sitestaffr';

        if ( isset( $input['fixed_icon_size'] ) ) {
            $icon_size_int = intval( $input['fixed_icon_size'] );
            $sanitized['fixed_icon_size'] = ( $icon_size_int >= 12 && $icon_size_int <= 80 ) ? $icon_size_int . 'px' : '24px';
        }

        if ( isset( $input['fixed_background_color'] ) ) {
            $sanitized['fixed_background_color'] = sanitize_hex_color( $input['fixed_background_color'] );
        }

        if ( isset( $input['fixed_icon_color'] ) ) {
            $sanitized['fixed_icon_color'] = sanitize_hex_color( $input['fixed_icon_color'] );
        }

        if ( isset( $input['fixed_hover_icon_color'] ) ) {
            $sanitized['fixed_hover_icon_color'] = sanitize_hex_color( $input['fixed_hover_icon_color'] );
        }

        if ( isset( $input['fixed_tooltip_text'] ) ) {
            $sanitized['fixed_tooltip_text'] = sanitize_text_field( $input['fixed_tooltip_text'] );
        }
        if ( isset( $input['fixed_tooltip_always_show'] ) ) {
            $sanitized['fixed_tooltip_always_show'] = ! empty( $input['fixed_tooltip_always_show'] );
        }
        if ( isset( $input['fixed_tooltip_position'] ) ) {
            $sanitized['fixed_tooltip_position'] = in_array( $input['fixed_tooltip_position'], array( 'top', 'bottom' ), true ) ? $input['fixed_tooltip_position'] : 'top';
        }

        if ( isset( $input['fixed_hover_background_color'] ) ) {
            $sanitized['fixed_hover_background_color'] = sanitize_hex_color( $input['fixed_hover_background_color'] );
        }

        if ( isset( $input['fixed_size'] ) ) {
            $size_int = intval( $input['fixed_size'] );
            $sanitized['fixed_size'] = ( $size_int > 0 ) ? $size_int . 'px' : '60px';
        }

        if ( isset( $input['fixed_border_radius_top'] ) ) {
            $sanitized['fixed_border_radius_top'] = $sanitize_px_radius( $input['fixed_border_radius_top'], '20px' );
        }

        if ( isset( $input['fixed_border_radius_right'] ) ) {
            $sanitized['fixed_border_radius_right'] = $sanitize_px_radius( $input['fixed_border_radius_right'], '20px' );
        }

        if ( isset( $input['fixed_border_radius_bottom'] ) ) {
            $sanitized['fixed_border_radius_bottom'] = $sanitize_px_radius( $input['fixed_border_radius_bottom'], '20px' );
        }

        if ( isset( $input['fixed_border_radius_left'] ) ) {
            $sanitized['fixed_border_radius_left'] = $sanitize_px_radius( $input['fixed_border_radius_left'], '0px' );
        }

        if ( isset( $input['fixed_border_radius_locked'] ) ) {
            $sanitized['fixed_border_radius_locked'] = ! empty( $input['fixed_border_radius_locked'] );
        }

        // Fixed Widget Border Style
        if ( isset( $input['fixed_border_style'] ) ) {
            $valid_border_styles = array( 'none', 'solid', 'dashed', 'dotted', 'double' );
            $sanitized['fixed_border_style'] = in_array( $input['fixed_border_style'], $valid_border_styles, true )
                ? $input['fixed_border_style']
                : 'none';
        }

        // Fixed Widget Border Width
        if ( isset( $input['fixed_border_width'] ) ) {
            $fixed_bw_int = absint( $input['fixed_border_width'] );
            $sanitized['fixed_border_width'] = min( $fixed_bw_int, 10 ) . 'px';
        }

        // Fixed Widget Border Color
        if ( isset( $input['fixed_border_color'] ) ) {
            $sanitized['fixed_border_color'] = sanitize_hex_color( $input['fixed_border_color'] );
        }

        // Legacy fallback support for older single-value radius setting.
        if ( isset( $input['fixed_border_radius'] ) ) {
            $legacy_fixed_radius = $sanitize_px_radius( $input['fixed_border_radius'], '20px' );
            $sanitized['fixed_border_radius'] = $legacy_fixed_radius;

            if ( ! isset( $input['fixed_border_radius_top'] ) ) {
                $sanitized['fixed_border_radius_top'] = $legacy_fixed_radius;
            }
            if ( ! isset( $input['fixed_border_radius_right'] ) ) {
                $sanitized['fixed_border_radius_right'] = $legacy_fixed_radius;
            }
            if ( ! isset( $input['fixed_border_radius_bottom'] ) ) {
                $sanitized['fixed_border_radius_bottom'] = $legacy_fixed_radius;
            }
            if ( ! isset( $input['fixed_border_radius_left'] ) ) {
                $sanitized['fixed_border_radius_left'] = $legacy_fixed_radius;
            }
        }

        // Button Widget Settings
        if ( isset( $input['button_text'] ) ) {
            $sanitized['button_text'] = sanitize_text_field( $input['button_text'] );
        }

        if ( isset( $input['button_background_color'] ) ) {
            $sanitized['button_background_color'] = sanitize_hex_color( $input['button_background_color'] );
        }

        if ( isset( $input['button_hover_background'] ) ) {
            $sanitized['button_hover_background'] = sanitize_hex_color( $input['button_hover_background'] );
        }

        if ( isset( $input['button_text_color'] ) ) {
            $sanitized['button_text_color'] = sanitize_hex_color( $input['button_text_color'] );
        }

        if ( isset( $input['button_icon_color'] ) ) {
            $sanitized['button_icon_color'] = sanitize_hex_color( $input['button_icon_color'] );
        }

        if ( isset( $input['button_hover_icon_color'] ) ) {
            $sanitized['button_hover_icon_color'] = sanitize_hex_color( $input['button_hover_icon_color'] );
        }

        if ( isset( $input['button_tooltip_text'] ) ) {
            $sanitized['button_tooltip_text'] = sanitize_text_field( $input['button_tooltip_text'] );
        }
        if ( isset( $input['button_tooltip_always_show'] ) ) {
            $sanitized['button_tooltip_always_show'] = ! empty( $input['button_tooltip_always_show'] );
        }

        if ( isset( $input['button_border_radius_top'] ) ) {
            $sanitized['button_border_radius_top'] = $sanitize_px_radius( $input['button_border_radius_top'], '80px' );
        }

        if ( isset( $input['button_border_radius_right'] ) ) {
            $sanitized['button_border_radius_right'] = $sanitize_px_radius( $input['button_border_radius_right'], '80px' );
        }

        if ( isset( $input['button_border_radius_bottom'] ) ) {
            $sanitized['button_border_radius_bottom'] = $sanitize_px_radius( $input['button_border_radius_bottom'], '80px' );
        }

        if ( isset( $input['button_border_radius_left'] ) ) {
            $sanitized['button_border_radius_left'] = $sanitize_px_radius( $input['button_border_radius_left'], '80px' );
        }

        if ( isset( $input['button_border_radius_locked'] ) ) {
            $sanitized['button_border_radius_locked'] = ! empty( $input['button_border_radius_locked'] );
        }

        // Legacy fallback support for older single-value radius setting.
        if ( isset( $input['button_border_radius'] ) ) {
            $legacy_button_radius = $sanitize_px_radius( $input['button_border_radius'], '80px' );
            $sanitized['button_border_radius'] = $legacy_button_radius;

            if ( ! isset( $input['button_border_radius_top'] ) ) {
                $sanitized['button_border_radius_top'] = $legacy_button_radius;
            }
            if ( ! isset( $input['button_border_radius_right'] ) ) {
                $sanitized['button_border_radius_right'] = $legacy_button_radius;
            }
            if ( ! isset( $input['button_border_radius_bottom'] ) ) {
                $sanitized['button_border_radius_bottom'] = $legacy_button_radius;
            }
            if ( ! isset( $input['button_border_radius_left'] ) ) {
                $sanitized['button_border_radius_left'] = $legacy_button_radius;
            }
        }

        if ( isset( $input['button_icon'] ) ) {
            $valid_icons = array( 'microphone', 'phone', 'chat', 'headset', 'sitestaffr', 'none' );
            $sanitized['button_icon'] = in_array( $input['button_icon'], $valid_icons, true )
                ? $input['button_icon']
                : 'sitestaffr';
        }

        if ( isset( $input['button_icon_position'] ) ) {
            $valid_positions = array( 'left', 'right' );
            $sanitized['button_icon_position'] = in_array( $input['button_icon_position'], $valid_positions, true )
                ? $input['button_icon_position']
                : 'left';
        }

        // Icon Size
        if ( isset( $input['button_icon_size'] ) ) {
            $icon_size_int = intval( $input['button_icon_size'] );
            $sanitized['button_icon_size'] = ( $icon_size_int > 0 ) ? $icon_size_int . 'px' : '18px';
        }

        // Font Size
        if ( isset( $input['button_font_size'] ) ) {
            $font_size_int = intval( $input['button_font_size'] );
            $sanitized['button_font_size'] = ( $font_size_int > 0 ) ? $font_size_int . 'px' : '16px';
        }

        // Font Weight
        if ( isset( $input['button_font_weight'] ) ) {
            $valid_weights = array( '400', '500', '600', '700' );
            $sanitized['button_font_weight'] = in_array( $input['button_font_weight'], $valid_weights, true )
                ? $input['button_font_weight']
                : '600';
        }

        // Text Transform
        if ( isset( $input['button_text_transform'] ) ) {
            $valid_transforms = array( 'none', 'uppercase', 'lowercase', 'capitalize' );
            $sanitized['button_text_transform'] = in_array( $input['button_text_transform'], $valid_transforms, true )
                ? $input['button_text_transform']
                : 'none';
        }

        // Gradient Toggle
        if ( isset( $input['button_gradient'] ) ) {
            $sanitized['button_gradient'] = (bool) $input['button_gradient'];
        }

        // Gradient End Color
        if ( isset( $input['button_gradient_end'] ) ) {
            $sanitized['button_gradient_end'] = sanitize_hex_color( $input['button_gradient_end'] );
        }

        // Border Width
        if ( isset( $input['button_border_width'] ) ) {
            $border_width_int = intval( $input['button_border_width'] );
            $sanitized['button_border_width'] = ( $border_width_int >= 0 ) ? $border_width_int . 'px' : '0px';
        }

        // Border Color
        if ( isset( $input['button_border_color'] ) ) {
            $sanitized['button_border_color'] = sanitize_hex_color( $input['button_border_color'] );
        }

        // Button Border Style
        if ( isset( $input['button_border_style'] ) ) {
            $valid_border_styles = array( 'none', 'solid', 'dashed', 'dotted', 'double' );
            $sanitized['button_border_style'] = in_array( $input['button_border_style'], $valid_border_styles, true )
                ? $input['button_border_style']
                : 'solid';
        }

        // Box Shadow Toggle
        if ( isset( $input['button_box_shadow'] ) ) {
            $sanitized['button_box_shadow'] = (bool) $input['button_box_shadow'];
        }

        // Shadow Color
        if ( isset( $input['button_shadow_color'] ) ) {
            $sanitized['button_shadow_color'] = sanitize_text_field( $input['button_shadow_color'] );
        }

        // Shadow Blur
        if ( isset( $input['button_shadow_blur'] ) ) {
            $shadow_blur_int = intval( $input['button_shadow_blur'] );
            $sanitized['button_shadow_blur'] = ( $shadow_blur_int >= 0 ) ? $shadow_blur_int . 'px' : '10px';
        }

        // Shadow Offset
        if ( isset( $input['button_shadow_offset'] ) ) {
            $shadow_offset_int = intval( $input['button_shadow_offset'] );
            $sanitized['button_shadow_offset'] = ( $shadow_offset_int >= 0 ) ? $shadow_offset_int . 'px' : '4px';
        }

        // Padding X
        if ( isset( $input['button_padding_x'] ) ) {
            $padding_x_int = intval( $input['button_padding_x'] );
            $sanitized['button_padding_x'] = ( $padding_x_int >= 0 ) ? $padding_x_int . 'px' : '24px';
        }

        // Padding Y
        if ( isset( $input['button_padding_y'] ) ) {
            $padding_y_int = intval( $input['button_padding_y'] );
            $sanitized['button_padding_y'] = ( $padding_y_int >= 0 ) ? $padding_y_int . 'px' : '12px';
        }

        // Full Width Toggle
        if ( isset( $input['button_full_width'] ) ) {
            $sanitized['button_full_width'] = (bool) $input['button_full_width'];
        }

        // Hover Animation
        if ( isset( $input['button_hover_animation'] ) ) {
            $valid_animations = array( 'none', 'scale', 'glow', 'pulse' );
            $sanitized['button_hover_animation'] = in_array( $input['button_hover_animation'], $valid_animations, true )
                ? $input['button_hover_animation']
                : 'none';
        }

        // Listening Text
        if ( isset( $input['button_listening_text'] ) ) {
            $sanitized['button_listening_text'] = sanitize_text_field( $input['button_listening_text'] );
        }

        // Listening Color
        if ( isset( $input['button_listening_color'] ) ) {
            $sanitized['button_listening_color'] = sanitize_hex_color( $input['button_listening_color'] );
        }

        return $sanitized;
    }

    /**
     * Display the main admin page.
     *
     * @since    1.0.0
     */
    public function display_admin_page() {
        $sitestaffr_admin_view_query_service = new SiteStaffr_Admin_View_Query_Service();
        include_once SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-dashboard.php';
    }

    /**
     * Display the calls page (all calls table view).
     *
     * Renders the dashboard in calls-list mode to avoid brittle redirect-only behavior.
     *
     * @since    0.9.94
     */
    public function display_calls_page() {
        $sitestaffr_default_view = 'all';
        $sitestaffr_admin_view_query_service = new SiteStaffr_Admin_View_Query_Service();
        include_once SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-dashboard.php';
    }

    /**
     * Display the settings page.
     *
     * @since    1.0.0
     */
    public function display_settings_page() {
        include_once SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-settings.php';
    }

    /**
     * Display the widget settings page.
     *
     * @since    0.11.94
     */
    public function display_widget_settings_page() {
        include_once SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-widget-settings.php';
    }

    /**
     * Display the AI Knowledge page.
     *
     * @since 1.7.0
     */
    public function display_knowledge_page() {
        require_once SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-knowledge.php';
    }

    /**
     * Display the My AI Agent page.
     *
     * @since 1.17.0
     */
    public function display_my_agent_page() {
        require_once SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-my-agent.php';
    }

    /**
     * Display the diagnostics page.
     *
     * DEVELOPER ONLY: This page is for SiteStaffr support team internal use.
     * Business owners should NOT have access to token metrics and internal diagnostics.
     *
     * @since    0.7.1
     * @updated  0.11.56 - Added developer-only access control
     */
    public function display_diagnostics_page() {
        // Guard — prevent direct URL access when diagnostics files are not present.
        if ( ! file_exists( SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-diagnostics.php' ) ) {
            wp_die( esc_html__( 'This page is not available.', 'sitestaffr' ) );
        }

        // Check user capability
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'sitestaffr' ) );
        }

        include_once SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-diagnostics.php';
    }

    /**
     * Display the usage dashboard page.
     *
     * @since    1.0.0
     */
    public function display_usage_dashboard() {
        $sitestaffr_admin_view_query_service = new SiteStaffr_Admin_View_Query_Service();
        include_once SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-usage-dashboard.php';
    }

    /**
     * Display the setup wizard.
     *
     * @since    1.0.0
     */
    public function display_setup_wizard() {
        include_once SITESTAFFR_PLUGIN_DIR . 'admin/views/admin-setup-wizard.php';
    }

    /**
     * Display low balance admin notice.
     *
     * Shows a notice banner when the minute balance is low relative to the
     * user's plan quota. Thresholds are hybrid (percentage + absolute) so that
     * small plans are not over-alerted and large plans are not under-alerted:
     *
     *  Critical : < 5% remaining  OR < 2 minutes  (notice-error)
     *  Warning  : < 15% remaining OR < 5 minutes  (notice-warning)
     *  Info     : < 25% remaining OR < 15 minutes (notice-info)
     *  Otherwise: no notice
     *
     * @since    1.0.0
     */
    public function display_low_balance_notice() {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check.
        $current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';

        // Only show on SiteStaffr admin pages
        if ( '' === $current_page || strpos( $current_page, 'sitestaffr' ) !== 0 ) {
            return;
        }

        // Get minute balance
        $usage_manager = SiteStaffr_Usage_Manager::get_instance();
        $balance = $usage_manager->get_minutes_balance();
        $total = $balance['total'];

        // Read billing cache to determine the active plan and compute percentage.
        $billing_cache = array();
        if ( class_exists( 'SiteStaffr_Billing_State' ) ) {
            $billing_cache = SiteStaffr_Billing_State::get_cache();
        }
        $plan_key = isset( $billing_cache['plan_name'] ) ? sanitize_key( $billing_cache['plan_name'] ) : 'starter';
        $plan_quotas = array( 'trial' => 30, 'starter' => 60, 'business' => 300, 'pro' => 700 );
        $included_quota = isset( $plan_quotas[ $plan_key ] ) ? (int) $plan_quotas[ $plan_key ] : 60;
        $total_quota = $included_quota + $balance['purchased'];
        $percentage = ( $total_quota > 0 ) ? ( $total / $total_quota ) * 100 : 0;

        // Determine notice level using hybrid plan-aware thresholds.
        if ( $percentage < 5 || $total < 2 ) {
            // Critical: Red alert
            $notice_class = 'notice-error';
            $icon = 'warning';
            $message = sprintf(
                /* translators: 1: Remaining minutes, 2: Percentage */
                __( 'Critical: Only %1$d minutes remaining (%2$d%%)! Purchase more minutes to avoid service interruption.', 'sitestaffr' ),
                $total,
                round( $percentage )
            );
        } elseif ( $percentage < 15 || $total < 5 ) {
            // Warning: Yellow warning
            $notice_class = 'notice-warning';
            $icon = 'info';
            $message = sprintf(
                /* translators: 1: Remaining minutes, 2: Total quota minutes, 3: Percentage */
                __( 'Low Minute Balance: You have %1$d of %2$d minutes remaining (%3$d%%). Consider purchasing more minutes.', 'sitestaffr' ),
                $total,
                $total_quota,
                round( $percentage )
            );
        } elseif ( $percentage < 25 || $total < 15 ) {
            // Info: Soft reminder
            $notice_class = 'notice-info';
            $icon = 'info';
            $message = sprintf(
                /* translators: 1: Remaining minutes, 2: Total quota minutes, 3: Percentage */
                __( 'Approaching Low Balance: You have %1$d of %2$d minutes remaining (%3$d%%). Consider purchasing more to avoid interruption.', 'sitestaffr' ),
                $total,
                $total_quota,
                round( $percentage )
            );
        } else {
            // No notice needed
            return;
        }

        // Display notice
        ?>
        <div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible">
            <p>
                <span class="dashicons dashicons-<?php echo esc_attr( $icon ); ?>" style="vertical-align: middle; margin-right: 5px;"></span>
                <strong><?php echo esc_html( $message ); ?></strong>
            </p>
            <p>
                <a href="<?php echo esc_url( admin_url( 'admin.php?page=sitestaffr-usage#purchase-minutes' ) ); ?>" class="button button-primary">
                    <?php esc_html_e( 'Purchase Minutes', 'sitestaffr' ); ?>
                </a>
            </p>
        </div>
        <?php
    }



    /**
     * AJAX handler: dismiss the global low balance alert.
     *
     * Stores the current remaining minutes in user meta so the alert
     * stays hidden until the balance is replenished (purchase/renewal)
     * and drops low again.
     *
     * @since 0.14.158
     */
    public function ajax_dismiss_low_balance_alert() {
        check_ajax_referer( 'sitestaffr_low_balance_dismiss', 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
            return;
        }

        $user_id       = get_current_user_id();
        $dismissed_until = time() + DAY_IN_SECONDS;
        $alert_level_raw = isset( $_POST['alert_level'] ) ? sanitize_key( wp_unslash( $_POST['alert_level'] ) ) : '';
        $allowed_levels = array( 'warning', 'low', 'critical', 'exhausted' );
        $dismissed_level = in_array( $alert_level_raw, $allowed_levels, true ) ? $alert_level_raw : '';

        // Store expiry timestamp; banner reappears automatically after 24 hours.
        update_user_meta( $user_id, 'sitestaffr_alert_dismissed_until', $dismissed_until );
        if ( '' !== $dismissed_level ) {
            update_user_meta( $user_id, 'sitestaffr_alert_dismissed_level', $dismissed_level );
        } else {
            delete_user_meta( $user_id, 'sitestaffr_alert_dismissed_level' );
        }

        wp_send_json_success();
    }

    /**
     * AJAX handler to save wizard settings.
     *
     * @since    1.0.0
     */
    public function ajax_save_wizard() {

        // Verify nonce — dies automatically on failure.
        check_ajax_referer( 'sitestaffr_setup_wizard', 'nonce' );

        // Verify capability.
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => __( 'You do not have permission to do this.', 'sitestaffr' ) ) );
            return;
        }

        $wizard_complete_raw = '0';
        if ( isset( $_POST['wizard_complete'] ) ) {
            $wizard_complete_raw = sanitize_text_field( wp_unslash( $_POST['wizard_complete'] ) );
        }
        $is_final_wizard_save = 1 === absint( $wizard_complete_raw );

        // Get existing settings (preserve any existing API keys, etc.)
        $settings = get_option( 'sitestaffr_settings', array() );

        if ( ! is_array( $settings ) ) {
            $settings = array();
        }

        // Ultra-minimal registration: ONLY business_name is required
        $settings['business_name'] = isset( $_POST['business_name'] ) ? sanitize_text_field( wp_unslash( $_POST['business_name'] ) ) : '';

        // Validate business_name is not empty
        if ( empty( $settings['business_name'] ) ) {
            SiteStaffr_Logger::legacy( 'SiteStaffr: Business name is empty - validation failed' );
            wp_send_json_error( array( 'message' => __( 'Business name is required.', 'sitestaffr' ) ) );
            return;
        }

        // Optional fields - PRESERVE existing values if not posted (they may have been saved earlier by ajax_save_enhancement)
        // Business description - check standalone option first, then settings array
        $business_description_raw = isset( $_POST['business_description'] ) ? wp_unslash( $_POST['business_description'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated/sanitized below.
        if ( ! empty( $business_description_raw ) ) {
            if ( strlen( sanitize_textarea_field( $business_description_raw ) ) > self::BUSINESS_DESCRIPTION_MAX_LENGTH ) {
                wp_send_json_error( array(
                    'message' => sprintf(
                        /* translators: %d is the max business description length */
                        __( 'Business description is too long. Maximum %d characters.', 'sitestaffr' ),
                        self::BUSINESS_DESCRIPTION_MAX_LENGTH
                    ),
                ) );
                return;
            }

            $description_result = $this->sanitize_business_description( $business_description_raw );
            $business_description = $description_result['value'];
            update_option( 'sitestaffr_business_description', $business_description );
            $settings['business_context'] = $business_description;
        } else {
            // Preserve existing value from standalone option or settings array
            $existing_desc = get_option( 'sitestaffr_business_description', '' );
            if ( ! empty( $existing_desc ) ) {
                $settings['business_context'] = $existing_desc;
            } elseif ( isset( $settings['business_context'] ) ) {
                // Keep existing value in settings array
                // (no action needed, already in $settings)
            }
        }

        // Business hours - validate before saving
        $business_hours_raw = isset( $_POST['business_hours'] ) ? sanitize_text_field( wp_unslash( $_POST['business_hours'] ) ) : '';
        if ( ! empty( $business_hours_raw ) ) {
            $business_hours = $business_hours_raw;
            $validation = $this->validate_business_hours( $business_hours );

            if ( $validation['valid'] ) {
                update_option( 'sitestaffr_business_hours', $validation['sanitized'] );
                $settings['business_hours'] = $validation['sanitized'];
            } else {
                SiteStaffr_Logger::legacy( 'SiteStaffr: Validation failed for business hours from wizard: ' . $validation['error'] );
                // Don't save invalid hours - will preserve existing value below
            }
        } else {
            // Preserve existing value from standalone option or settings array
            $existing_hours = get_option( 'sitestaffr_business_hours', '' );
            if ( ! empty( $existing_hours ) ) {
                $settings['business_hours'] = $existing_hours;
            } elseif ( isset( $settings['business_hours'] ) ) {
                // Keep existing value in settings array
                // (no action needed, already in $settings)
            }
        }

        // Business phone - check standalone option first, then settings array
        $business_phone_raw = isset( $_POST['business_phone_step1'] ) ? sanitize_text_field( wp_unslash( $_POST['business_phone_step1'] ) ) : '';
        if ( ! empty( $business_phone_raw ) ) {
            $business_phone = $business_phone_raw;
            update_option( 'sitestaffr_business_phone', $business_phone );
            $settings['business_phone'] = $business_phone;
        } else {
            // Preserve existing value from standalone option or settings array
            $existing_phone = get_option( 'sitestaffr_business_phone', '' );
            if ( ! empty( $existing_phone ) ) {
                $settings['business_phone'] = $existing_phone;
            } elseif ( isset( $settings['business_phone'] ) ) {
                // Keep existing value in settings array
                // (no action needed, already in $settings)
            }
        }
        // Business email — save from wizard, preserve existing on intermediate saves
        $business_email_raw = isset( $_POST['business_email'] ) ? sanitize_email( wp_unslash( $_POST['business_email'] ) ) : '';
        if ( ! empty( $business_email_raw ) ) {
            $business_email = $business_email_raw;
            update_option( 'sitestaffr_account_email', $business_email );
            $settings['notification_email'] = $business_email;
        } else {
            // Preserve existing: account email → current notification_email → WP admin email
            $existing_email = get_option( 'sitestaffr_account_email', '' );
            if ( ! empty( $existing_email ) ) {
                $settings['notification_email'] = $existing_email;
            } elseif ( ! isset( $settings['notification_email'] ) || empty( $settings['notification_email'] ) ) {
                $settings['notification_email'] = get_option( 'admin_email' );
            }
        }

        // Mark setup as complete only when explicitly finishing the wizard.
        if ( $is_final_wizard_save ) {
            $settings['setup_complete']      = true;
            $settings['setup_completed_at']  = current_time( 'mysql' );
        }

        // Update settings - update_option returns false if value unchanged, which is OK
        $update_result = update_option( 'sitestaffr_settings', $settings );

        // Verify save by reading back and checking business_name
        $saved_settings = get_option( 'sitestaffr_settings', array() );
        $name_matches = isset( $saved_settings['business_name'] ) &&
                        ! empty( $saved_settings['business_name'] ) &&
                        $saved_settings['business_name'] === $settings['business_name'];

        if ( $name_matches ) {

            if ( $is_final_wizard_save ) {
                // Set standalone setup completion flag (prevents blocking on next page load)
                update_option( 'sitestaffr_setup_completed', true );
            }

            wp_send_json_success(
                array(
                    'message' => $is_final_wizard_save
                        ? __( 'Settings saved successfully.', 'sitestaffr' )
                        : __( 'Step saved successfully.', 'sitestaffr' ),
                )
            );
        } else {
            SiteStaffr_Logger::legacy( 'SiteStaffr: Setup wizard failed to save settings - business_name mismatch or not found' );

            wp_send_json_error( array(
                'message' => __( 'Failed to save settings. Please try again.', 'sitestaffr' ),
            ) );
        }
    }

    /**
     * AJAX handler to reset training counter
     *
     * @since    1.0.0
     */
    public function ajax_reset_training_counter() {

        // Use the same nonce as other training functions
        if ( ! check_ajax_referer( 'sitestaffr_train_ai', 'nonce', false ) ) {
            wp_send_json_error( array( 'message' => __( 'Security check failed', 'sitestaffr' ) ) );
            return;
        }

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => __( 'Unauthorized access', 'sitestaffr' ) ) );
            return;
        }

        // Reset the training counter to 0 (meaning 10 available)
        $current_month = gmdate( 'Y-m' );
        $option_name   = 'sitestaffr_training_updates_' . $current_month;

        $result = update_option( $option_name, 0 );

        if ( $result || get_option( $option_name ) == 0 ) {
            wp_send_json_success( array(
                'message'   => __( 'Training counter reset to 10/10 successfully', 'sitestaffr' ),
                'remaining' => 10,
            ) );
        } else {
            wp_send_json_error( array( 'message' => __( 'Failed to reset training counter', 'sitestaffr' ) ) );
        }
    }

    /**
     * AJAX handler: register site with middleware before wizard Step 3.
     *
     * Ensures the middleware has valid credentials for description generation.
     *
     * @since 1.7.0
     */
    public function ajax_wizard_register_site() {
        check_ajax_referer( 'sitestaffr_setup_wizard', 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
            return;
        }

        // Skip if already registered — avoids redundant middleware round-trip.
        if ( SiteStaffr_Site_Registration::is_registered() ) {
            wp_send_json_success( array( 'registered' => true ) );
            return;
        }

        // register_site() returns true on success, false on failure.
        $result = SiteStaffr_Site_Registration::register_site();

        if ( ! $result ) {
            wp_send_json_error( array( 'message' => 'Site registration failed. Please try again.' ) );
        }

        wp_send_json_success( array( 'registered' => true ) );
    }

}
