<?php

namespace Kanzu\StoreVoiceAI;

// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

use Kanzu\StoreVoiceAI\Services\Store_Voice_AI_API;

/**
 * Drives the first-run onboarding wizard: meet Grace, customize her, configure
 * transfers, then go live. Renders as a full-screen admin takeover, persists
 * choices via AJAX, and replaces the Freemius opt-in modal as the entry point.
 */
class Onboarding
{
    use Working_Hours_Trait;

    const PAGE_SLUG          = 'storevoiceai-onboarding';
    const NONCE              = 'storevoiceai-onboarding-nonce';
    const OPT_COMPLETE       = '_storevoiceai_onboarding_complete';
    const OPT_REDIRECT       = '_storevoiceai_onboarding_redirect';
    const OPT_STEP           = '_storevoiceai_onboarding_step';
    const OPT_ESCALATION     = '_storevoiceai_onboarding_escalation';
    const OPT_ESCALATION_VAL = '_storevoiceai_onboarding_escalation_value';
    const OPT_WORKING_HOURS  = '_storevoiceai_onboarding_working_hours';

    const LOGO_URL = 'https://pub-2fd37cd476bb4de3ae18101d6d0bd116.r2.dev/logo-black.png';

    /**
     * Wire the onboarding into WordPress. Called once from Hook_Registry.
     *
     * Note: the takeover is hooked to `init` priority 1 — not `admin_init` —
     * because `wp-admin/menu.php` runs a permission gate that fires BEFORE
     * `admin_init`. If anything in the menu registration chain doesn't line
     * up (e.g., parent menu added by a later priority, capability filter
     * race), that gate can `wp_die()` with "Sorry, you are not allowed to
     * access this page" before our takeover ever runs. Hooking on `init`
     * (which fires during the wp-load.php phase, well before menu.php) lets
     * us intercept the URL unconditionally.
     */
    public function register_hooks()
    {
        add_action( 'admin_menu', array( $this, 'register_page' ), 99 );
        add_action( 'admin_init', array( $this, 'maybe_redirect' ) );
        // Priority 20 so WooCommerce (which registers the `product` post type
        // on `init` priority 5) has run first — we read the product count for
        // the wizard. All `init` priorities still complete before wp-admin's
        // menu permission gate, so we keep intercepting ahead of it.
        add_action( 'init',       array( $this, 'maybe_render_takeover' ), 20 );

        add_action( 'wp_ajax_storevoiceai_onboarding_save_step',  array( $this, 'ajax_save_step' ) );
        add_action( 'wp_ajax_storevoiceai_onboarding_save_agent', array( $this, 'ajax_save_agent' ) );
        add_action( 'wp_ajax_storevoiceai_onboarding_finish',     array( $this, 'ajax_finish' ) );

        // "Try a call" step: place a live web call, sync the logs, then fetch
        // the recording and transcript so the merchant sees what a customer
        // experiences. Sync and retrieve are separate so the browser can show a
        // processing pause between them while the call finishes processing.
        add_action( 'wp_ajax_storevoiceai_onboarding_create_call',   array( $this, 'ajax_create_call' ) );
        add_action( 'wp_ajax_storevoiceai_onboarding_sync_calls',    array( $this, 'ajax_sync_calls' ) );
        add_action( 'wp_ajax_storevoiceai_onboarding_retrieve_call', array( $this, 'ajax_retrieve_call' ) );
    }

    /**
     * Register a hidden submenu page so the wizard URL resolves but is not
     * listed in the admin sidebar.
     */
    public function register_page()
    {
        add_submenu_page(
            'storevoiceai-for-woocommerce',
            esc_html__( 'StoreVoiceAI Setup', 'storevoiceai-for-woocommerce' ),
            esc_html__( 'StoreVoiceAI Setup', 'storevoiceai-for-woocommerce' ),
            'manage_woocommerce',
            self::PAGE_SLUG,
            array( $this, 'render_placeholder' )
        );

        remove_submenu_page( 'storevoiceai-for-woocommerce', self::PAGE_SLUG );
    }

    /**
     * The takeover renders before this fires; only used if the takeover hook
     * was bypassed (e.g., direct render call).
     */
    public function render_placeholder()
    {
        echo '<div class="wrap"><h1>' . esc_html__( 'Loading…', 'storevoiceai-for-woocommerce' ) . '</h1></div>';
    }

    /**
     * On the next admin page load after activation, redirect a privileged user
     * straight into the wizard. Consumed-once: the flag is cleared regardless
     * of whether the user completes the flow.
     */
    public function maybe_redirect()
    {
        if ( ! get_option( self::OPT_REDIRECT ) ) {
            return;
        }
        if ( get_option( self::OPT_COMPLETE ) ) {
            delete_option( self::OPT_REDIRECT );
            return;
        }
        if ( wp_doing_ajax() || wp_doing_cron() ) {
            return;
        }
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            return;
        }
        if ( isset( $_GET['activate-multi'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only check on WP-provided query var.
            return;
        }
        // Skip if we're already inside the wizard.
        $current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page detection.
        if ( $current_page === self::PAGE_SLUG ) {
            return;
        }

        delete_option( self::OPT_REDIRECT );
        wp_safe_redirect( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) );
        exit;
    }

    /**
     * When the user lands on the wizard page, take over the entire admin
     * viewport — we render our own HTML document instead of the standard
     * wp-admin shell. Runs on `init` priority 1 so it executes before
     * wp-admin/menu.php's capability gate.
     */
    public function maybe_render_takeover()
    {
        // Only intercept full admin page loads. Let AJAX, cron, REST, CLI,
        // login screen, and frontend pages flow through normally.
        if ( wp_doing_ajax() || wp_doing_cron() ) {
            return;
        }
        if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
            return;
        }
        if ( defined( 'WP_CLI' ) && WP_CLI ) {
            return;
        }
        if ( ! is_admin() ) {
            return;
        }

        // Match the page slug ourselves rather than trusting WP's $plugin_page
        // global — at init priority 1 that global may not be populated yet.
        $current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page detection.
        if ( $current_page !== self::PAGE_SLUG ) {
            return;
        }

        if ( ! is_user_logged_in() ) {
            auth_redirect();
            exit;
        }

        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            wp_die(
                esc_html__( 'You need WooCommerce shop manager or admin permissions to run the StoreVoiceAI setup.', 'storevoiceai-for-woocommerce' ),
                esc_html__( 'StoreVoiceAI Setup', 'storevoiceai-for-woocommerce' ),
                array( 'response' => 403, 'back_link' => true )
            );
        }

        $this->render_takeover();
        exit;
    }

    /**
     * Renders the wizard as a standalone, full-viewport HTML page using the
     * same pattern WooCommerce uses for its setup wizard.
     */
    private function render_takeover()
    {
        $assets_url = plugins_url( '../assets/', __FILE__ );
        $assets_dir = dirname( __FILE__ ) . '/../assets/';

        // Bust caches on every change to the wizard's assets — the wizard
        // iterates frequently and a hardcoded version was making browsers
        // serve stale JS/CSS, producing visible desync (e.g., stepper not
        // matching the rendered page).
        $css_ver = file_exists( $assets_dir . 'css/onboarding.css' )
            ? (string) filemtime( $assets_dir . 'css/onboarding.css' )
            : '0.0.7';
        $js_ver  = file_exists( $assets_dir . 'js/onboarding.js' )
            ? (string) filemtime( $assets_dir . 'js/onboarding.js' )
            : '0.0.7';

        wp_enqueue_style(
            'storevoiceai-onboarding',
            $assets_url . 'css/onboarding.css',
            array( 'dashicons' ),
            $css_ver
        );

        // The takeover renders its own document and bypasses the normal admin
        // shell, so the call-widget asset chain enqueued on `admin_enqueue_scripts`
        // never runs here. Load the Retell Web Client SDK ourselves so the
        // "Try a call" step can place a real browser call. Mirrors Scripts.php.
        wp_enqueue_script(
            'storevoiceai-eventemitter3',
            $assets_url . 'vendor/retell/eventemitter3.umd.min.js',
            array(),
            '5.0.1',
            true
        );
        wp_enqueue_script(
            'storevoiceai-livekit-client',
            $assets_url . 'vendor/retell/livekit-client.umd.js',
            array(),
            '2.5.1',
            true
        );
        wp_enqueue_script(
            'storevoiceai-retell-globals-shim',
            $assets_url . 'vendor/retell/retell-globals-shim.js',
            array( 'storevoiceai-eventemitter3', 'storevoiceai-livekit-client' ),
            '0.0.7',
            true
        );
        wp_enqueue_script(
            'storevoiceai-retell-client',
            $assets_url . 'vendor/retell/retell-client.umd.js',
            array( 'storevoiceai-retell-globals-shim' ),
            '2.0.7',
            true
        );

        wp_enqueue_script(
            'storevoiceai-onboarding',
            $assets_url . 'js/onboarding.js',
            array( 'storevoiceai-retell-client' ),
            $js_ver,
            true
        );

        wp_localize_script( 'storevoiceai-onboarding', 'storevoiceaiOnboarding', $this->bootstrap_data() );

        // We render before the admin shell loads, so emit our own no-cache
        // headers and document. set_current_screen()/admin shell helpers are
        // intentionally not used — they aren't loaded this early in the request.
        if ( ! headers_sent() ) {
            nocache_headers();
            header( 'Content-Type: text/html; charset=' . get_bloginfo( 'charset' ) );
        }

        ?><!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
    <meta name="viewport" content="width=device-width" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title><?php esc_html_e( 'StoreVoiceAI Setup', 'storevoiceai-for-woocommerce' ); ?></title>
    <?php
        wp_print_styles( 'storevoiceai-onboarding' );
        wp_print_head_scripts();
    ?>
</head>
<body class="storevoiceai-onboarding-body">
    <div id="storevoiceai-onboarding-root" class="svai-app" data-step="<?php echo esc_attr( $this->current_step() ); ?>">
        <header class="svai-app__header">
            <a class="svai-app__brand" href="<?php echo esc_url( admin_url( 'admin.php?page=storevoiceai-for-woocommerce-dashboard' ) ); ?>">
                <img src="<?php echo esc_url( self::LOGO_URL ); ?>" alt="StoreVoiceAI" />
            </a>
            <div class="svai-app__steps" id="svai-stepper-mount"></div>
            <button type="button" class="svai-app__skip" data-action="skip">
                <?php esc_html_e( 'Skip setup', 'storevoiceai-for-woocommerce' ); ?>
            </button>
        </header>

        <main class="svai-app__main" id="svai-main">
            <noscript>
                <div class="svai-noscript">
                    <?php esc_html_e( 'StoreVoiceAI setup requires JavaScript. Please enable it in your browser to continue.', 'storevoiceai-for-woocommerce' ); ?>
                </div>
            </noscript>
        </main>

        <footer class="svai-app__footer">
            <span>
                <?php esc_html_e( 'You can change every setting later under StoreVoiceAI → Settings.', 'storevoiceai-for-woocommerce' ); ?>
            </span>
        </footer>
    </div>
    <?php wp_print_footer_scripts(); ?>
</body>
</html>
        <?php
    }

    /**
     * Build the data the JS app needs to render the wizard. Mostly current
     * values plus AJAX wiring.
     */
    private function bootstrap_data()
    {
        $store_name      = get_bloginfo( 'name' );
        $product_count   = $this->count_published_products();
        $email           = wp_get_current_user()->user_email;
        $assistant_name  = get_option( '_storevoiceai_assistant_name', 'Grace' );
        $assistant_greet = wp_unslash( get_option(
            '_storevoiceai_assistant_first_message',
            sprintf(
                /* translators: %s: store name */
                __( "Hi, this is Grace with %s. How can I help you today?", 'storevoiceai-for-woocommerce' ),
                $store_name
            )
        ) );

        // Policies: prefer the merchant's saved values. Otherwise seed shipping
        // from their WooCommerce shipping zones (which earns the "Auto-filled"
        // badge), falling back to a generic worldwide default when there are no
        // zones to read. Returns gets a sensible default too.
        $saved_shipping = get_option( '_storevoiceai_shipping_policy', '' );
        $saved_returns  = get_option( '_storevoiceai_returns_policy', '' );
        $zone_shipping  = $this->build_shipping_policy_from_zones();
        if ( $saved_shipping !== '' ) {
            $shipping_policy     = wp_unslash( $saved_shipping );
            $shipping_autofilled = false;
        } elseif ( $zone_shipping !== '' ) {
            $shipping_policy     = $zone_shipping;
            $shipping_autofilled = true;
        } else {
            $shipping_policy     = $this->default_shipping_policy();
            $shipping_autofilled = false;
        }
        $returns_policy = ( $saved_returns !== '' ) ? wp_unslash( $saved_returns ) : $this->default_returns_policy();

        // Contact email for questions the agent can't answer. Defaults to the
        // site's admin email until the merchant overrides it.
        $owner_email = get_option( '_storevoiceai_owner_email', '' );
        if ( $owner_email === '' ) {
            $owner_email = (string) get_option( 'admin_email', '' );
        }

        return array(
            'ajaxUrl'    => admin_url( 'admin-ajax.php' ),
            'nonce'      => wp_create_nonce( self::NONCE ),
            'dashboard'  => admin_url( 'admin.php?page=storevoiceai-for-woocommerce-dashboard' ),
            'settings'   => admin_url( 'admin.php?page=storevoiceai-for-woocommerce-settings' ),
            'billing'    => admin_url( 'admin.php?page=storevoiceai-for-woocommerce-billing' ),
            'pricing'    => admin_url( 'admin.php?page=storevoiceai-for-woocommerce-pricing' ),
            'logoUrl'    => self::LOGO_URL,
            'startStep'  => (int) $this->current_step(),
            'store'      => array(
                'name'         => $store_name,
                'email'        => $email,
                'productCount' => $product_count,
            ),
            'agent'      => array(
                'name'     => $assistant_name,
                'greeting' => $assistant_greet,
            ),
            'escalation' => array(
                'mode'  => get_option( self::OPT_ESCALATION, 'hours' ),
                'value' => get_option( self::OPT_ESCALATION_VAL, '' ),
            ),
            'policies'   => array(
                'shipping'           => $shipping_policy,
                'returns'            => $returns_policy,
                'ownerEmail'         => $owner_email,
                // Badge only when shipping copy came from WooCommerce zones.
                'shippingAutofilled' => $shipping_autofilled,
            ),
            'workingHours' => $this->get_working_hours(),
            'i18n'       => array(
                'training'         => __( '%s, your AI Agent, is learning your store', 'storevoiceai-for-woocommerce' ),
                'trainingSub'      => __( 'Usually takes 20–30 seconds', 'storevoiceai-for-woocommerce' ),
                'readingProducts'  => __( 'Reading your %d products', 'storevoiceai-for-woocommerce' ),
                'readingPolicies'  => __( 'Learning shipping & return policies', 'storevoiceai-for-woocommerce' ),
                'readingPages'     => __( 'Scanning your pages and FAQs', 'storevoiceai-for-woocommerce' ),
                'buildingPersona'  => __( 'Building Grace for %s', 'storevoiceai-for-woocommerce' ),
                'stepLabels'       => array(
                    __( 'Customize', 'storevoiceai-for-woocommerce' ),
                    __( 'Policies', 'storevoiceai-for-woocommerce' ),
                    __( 'Try a call', 'storevoiceai-for-woocommerce' ),
                    __( 'Go live', 'storevoiceai-for-woocommerce' ),
                ),
                'errorSaving'      => __( 'Could not save. Please check your connection and try again.', 'storevoiceai-for-woocommerce' ),
                'onboarding.default_agent_name' => __( 'Grace', 'storevoiceai-for-woocommerce' ),
                'onboarding.day.mon' => __( 'Mon', 'storevoiceai-for-woocommerce' ),
                'onboarding.day.tue' => __( 'Tue', 'storevoiceai-for-woocommerce' ),
                'onboarding.day.wed' => __( 'Wed', 'storevoiceai-for-woocommerce' ),
                'onboarding.day.thu' => __( 'Thu', 'storevoiceai-for-woocommerce' ),
                'onboarding.day.fri' => __( 'Fri', 'storevoiceai-for-woocommerce' ),
                'onboarding.day.sat' => __( 'Sat', 'storevoiceai-for-woocommerce' ),
                'onboarding.day.sun' => __( 'Sun', 'storevoiceai-for-woocommerce' ),
                'onboarding.saving' => __( 'Saving…', 'storevoiceai-for-woocommerce' ),
                'onboarding.skipping' => __( 'Skipping…', 'storevoiceai-for-woocommerce' ),
                'onboarding.play_demo' => __( 'Play demo', 'storevoiceai-for-woocommerce' ),
                'onboarding.preview' => __( 'Preview', 'storevoiceai-for-woocommerce' ),
                'onboarding.step_count' => __( 'Step %d of 4', 'storevoiceai-for-woocommerce' ),
                'onboarding.title' => __( 'Make Grace your own', 'storevoiceai-for-woocommerce' ),
                'onboarding.subtitle' => __( 'Rename her and pick a greeting. You can change both anytime.', 'storevoiceai-for-woocommerce' ),
                'onboarding.knows_products' => __( 'Knows all %d products', 'storevoiceai-for-woocommerce' ),
                'onboarding.handles_shipping' => __( 'Handles shipping & order status', 'storevoiceai-for-woocommerce' ),
                'onboarding.explains_returns' => __( 'Explains your returns policy', 'storevoiceai-for-woocommerce' ),
                'onboarding.agent_name' => __( 'Agent name', 'storevoiceai-for-woocommerce' ),
                'onboarding.intro_label' => __( 'How should %s introduce herself?', 'storevoiceai-for-woocommerce' ),
                'onboarding.custom_greeting_placeholder' => __( 'Type your custom greeting here…', 'storevoiceai-for-woocommerce' ),
                'onboarding.custom_intro' => __( 'Custom introduction', 'storevoiceai-for-woocommerce' ),
                'onboarding.looks_good' => __( 'Looks good', 'storevoiceai-for-woocommerce' ),
                'onboarding.greeting.preset_1' => __( 'Hi, this is %s with %s. How can I help you today?', 'storevoiceai-for-woocommerce' ),
                'onboarding.greeting.preset_2' => __( 'Hi, this is %s with %s on a recorded line. How can I help you today?', 'storevoiceai-for-woocommerce' ),
                'onboarding.greeting.preset_3' => __( 'Hi, this is %s, your AI support rep at %s. By continuing, you consent to recording. How can I help?', 'storevoiceai-for-woocommerce' ),
                'onboarding.day_off' => __( 'Day off', 'storevoiceai-for-woocommerce' ),
                'onboarding.day_off_aria' => __( '%s day off', 'storevoiceai-for-woocommerce' ),
                'onboarding.start_time_aria' => __( '%s start time', 'storevoiceai-for-woocommerce' ),
                'onboarding.end_time_aria' => __( '%s end time', 'storevoiceai-for-woocommerce' ),
                'onboarding.transfer_title' => __( 'When %s can’t resolve a call', 'storevoiceai-for-woocommerce' ),
                'onboarding.transfer_subtitle' => __( 'Add a phone number where calls are transferred when a customer needs a human. This is optional — skip it and set it up later.', 'storevoiceai-for-woocommerce' ),
                'onboarding.transfer_feature_team' => __( 'Hand the caller to your team', 'storevoiceai-for-woocommerce' ),
                'onboarding.transfer_feature_hours' => __( 'Limit transfers to working hours', 'storevoiceai-for-woocommerce' ),
                'onboarding.transfer_feature_skip' => __( 'Skip if you’re not ready yet', 'storevoiceai-for-woocommerce' ),
                'onboarding.settings_note' => __( 'You can change this anytime from Settings.', 'storevoiceai-for-woocommerce' ),
                'onboarding.phone_error' => __( 'Enter a phone number, or use “Skip for now” below.', 'storevoiceai-for-woocommerce' ),
                'onboarding.hours_only' => __( 'Only transfer during working hours', 'storevoiceai-for-woocommerce' ),
                'onboarding.hours_hint' => __( 'Outside your hours, the caller is asked to leave a message instead.', 'storevoiceai-for-woocommerce' ),
                'onboarding.transfer_phone' => __( 'Transfer phone number', 'storevoiceai-for-woocommerce' ),
                'onboarding.transfer_phone_hint' => __( 'Every call %s can’t resolve is transferred here.', 'storevoiceai-for-woocommerce' ),
                'onboarding.back' => __( 'Back', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.title' => __( 'Talk to %s yourself', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.subtitle' => __( 'Your customers will experience this. You should too.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.ready_title' => __( '%s is ready', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.ready_sub' => __( 'Try asking about a product, your return policy, or shipping times.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.suggested' => __( 'Suggested questions', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.suggested_list' => __( '“What’s your return policy?”   ·   “Do you ship internationally?”   ·   “Tell me about a product”', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.start' => __( 'Start a call with %s', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.connecting' => __( 'Connecting…', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.live' => __( 'Live call', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.in_progress_sub' => __( 'When you’re done, hang up — we’ll show you what %s captured.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.end' => __( 'End call', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.ended_title' => __( '%s took notes.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.ended_sub' => __( 'See the summary, transcript, and recording from your call.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.retrieve' => __( 'Retrieve %s’s notes', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.retrieving' => __( 'Retrieving…', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.summary_label' => __( 'What %s understood', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.no_summary' => __( 'No summary was captured for this call.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.recording_label' => __( 'Recording', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.transcript_label' => __( 'Full transcript', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.processing_title' => __( 'Notes are still being prepared', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.processing_sub' => __( 'Recording and transcript can take a few moments after the call ends. Refresh to check again.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.refresh' => __( 'Refresh', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.golive' => __( 'Go live', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.skip' => __( 'Skip for now — I’ll try it later', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.sdk_error' => __( 'Voice calling could not load. Please refresh the page and try again.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.call_error' => __( 'We couldn’t start the call. Please allow microphone access and try again.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.minutes_exhausted' => __( 'You’ve used up your trial minutes — but you can still finish setup.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.not_found' => __( 'We couldn’t find your call yet. Please try again in a moment.', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.wrapping' => __( 'Wrapping up your call…', 'storevoiceai-for-woocommerce' ),
                'onboarding.try.retrieving_msg' => __( 'Retrieving %s’s notes…', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.title' => __( 'Teach %s your policies', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.subtitle' => __( 'These answer the questions customers ask most. %s reads from them on every call.', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.feature_shipping' => __( 'Answers “where do you ship?”', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.feature_returns' => __( 'Handles refund & return questions', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.feature_edit' => __( 'Edit anytime from Settings', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.shipping_label' => __( 'Shipping policy', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.returns_label' => __( 'Returns policy', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.autofilled' => __( 'Auto-filled', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.shipping_hint' => __( 'Auto-filled from your WooCommerce shipping zones. Add carrier names or delivery times if needed.', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.returns_hint' => __( '%s will use this to answer customer questions. Edit to match your actual policy.', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.shipping_placeholder' => __( 'Describe where you ship and any delivery details…', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.returns_placeholder' => __( 'Describe how returns and refunds work…', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.owner_label' => __( 'Contact email for unanswered questions', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.owner_hint' => __( 'If %s can’t answer a question, they’ll direct the customer here.', 'storevoiceai-for-woocommerce' ),
                'onboarding.policies.owner_placeholder' => __( 'you@store.com', 'storevoiceai-for-woocommerce' ),
                'onboarding.save_continue' => __( 'Save and continue', 'storevoiceai-for-woocommerce' ),
                'onboarding.skip_transfers' => __( 'Skip for now — I’ll set up transfers later', 'storevoiceai-for-woocommerce' ),
                'onboarding.all_done' => __( 'All done', 'storevoiceai-for-woocommerce' ),
                'onboarding.live_title' => __( '%s is live for %s', 'storevoiceai-for-woocommerce' ),
                'onboarding.live_subtitle' => __( 'Your store now has 24/7 AI voice support. Your free minutes never expire.', 'storevoiceai-for-woocommerce' ),
                'onboarding.free_minutes' => __( 'free minutes', 'storevoiceai-for-woocommerce' ),
                'onboarding.never_expire' => __( 'Never expire', 'storevoiceai-for-woocommerce' ),
                'onboarding.trained_products' => __( '%s trained on %d products', 'storevoiceai-for-woocommerce' ),
                'onboarding.configured' => __( 'Greeting configured', 'storevoiceai-for-woocommerce' ),
                'onboarding.widget_ready' => __( 'Web widget ready to embed', 'storevoiceai-for-woocommerce' ),
                'onboarding.next_heading' => __( 'You’re all set. What’s next?', 'storevoiceai-for-woocommerce' ),
                'onboarding.next_hint' => __( 'Jump straight into the place that fits — you can move between them anytime from the menu.', 'storevoiceai-for-woocommerce' ),
                'onboarding.open_dashboard' => __( 'Open your dashboard', 'storevoiceai-for-woocommerce' ),
                'onboarding.open_dashboard_desc' => __( 'See calls, transcripts and live analytics.', 'storevoiceai-for-woocommerce' ),
                'onboarding.fine_tune' => __( 'Fine-tune %s', 'storevoiceai-for-woocommerce' ),
                'onboarding.fine_tune_desc' => __( 'Voice, greeting, knowledge base and more.', 'storevoiceai-for-woocommerce' ),
                'onboarding.add_phone' => __( 'Add a phone number', 'storevoiceai-for-woocommerce' ),
                'onboarding.add_phone_desc' => __( 'Let customers reach %s by phone.', 'storevoiceai-for-woocommerce' ),
                'onboarding.ready_more' => __( 'Ready for more?', 'storevoiceai-for-woocommerce' ),
                'onboarding.upsell' => __( 'Unlimited minutes, a dedicated number, and full call transcripts.', 'storevoiceai-for-woocommerce' ),
                'onboarding.view_plans' => __( 'View plans', 'storevoiceai-for-woocommerce' ),
                'onboarding.confirm_default_title' => __( 'Are you sure?', 'storevoiceai-for-woocommerce' ),
                'onboarding.confirm_cancel' => __( 'Cancel', 'storevoiceai-for-woocommerce' ),
                'onboarding.confirm_confirm' => __( 'Confirm', 'storevoiceai-for-woocommerce' ),
                'onboarding.skip_title' => __( 'Skip the rest of setup?', 'storevoiceai-for-woocommerce' ),
                'onboarding.skip_message' => __( 'Anything you’ve already filled in will be saved. You can run this setup again anytime from the dashboard.', 'storevoiceai-for-woocommerce' ),
                'onboarding.skip_confirm' => __( 'Skip for now', 'storevoiceai-for-woocommerce' ),
                'onboarding.skip_cancel' => __( 'Stay and finish', 'storevoiceai-for-woocommerce' ),
            ),
        );
    }

    private function current_step()
    {
        $step = (int) get_option( self::OPT_STEP, 0 );
        if ( $step > 4 ) {
            return 4;
        }
        return ( $step >= 0 ) ? $step : 0;
    }

    private function count_published_products()
    {
        $counts = wp_count_posts( 'product' );
        if ( ! $counts || ! isset( $counts->publish ) ) {
            return 0;
        }
        return (int) $counts->publish;
    }

    /**
     * Build a first-draft shipping policy from the store's WooCommerce shipping
     * zones: which regions are served, plus a free-shipping threshold if one is
     * configured. Best-effort — returns '' when WooCommerce or zones are absent
     * so the caller can fall back to default_shipping_policy().
     */
    private function build_shipping_policy_from_zones()
    {
        if ( ! class_exists( '\WC_Shipping_Zones' ) ) {
            return '';
        }

        $regions         = array();
        $region_free_min = array();

        foreach ( \WC_Shipping_Zones::get_zones() as $zone ) {
            $label = '';
            if ( ! empty( $zone['formatted_zone_location'] ) ) {
                $label = $zone['formatted_zone_location'];
            } elseif ( ! empty( $zone['zone_name'] ) ) {
                $label = $zone['zone_name'];
            }
            if ( $label !== '' ) {
                $regions[] = $label;
            }

            if ( empty( $zone['shipping_methods'] ) || ! is_array( $zone['shipping_methods'] ) ) {
                continue;
            }
            foreach ( $zone['shipping_methods'] as $method ) {
                if ( ! isset( $method->id ) || $method->id !== 'free_shipping' ) {
                    continue;
                }
                $min = '';
                if ( isset( $method->min_amount ) ) {
                    $min = $method->min_amount;
                } elseif ( method_exists( $method, 'get_option' ) ) {
                    $min = $method->get_option( 'min_amount' );
                }
                if ( $label !== '' && $min !== '' && $min !== null && (float) $min > 0 ) {
                    $region_free_min[ $label ] = $min;
                }
            }
        }

        if ( empty( $regions ) ) {
            return '';
        }

        $policy = sprintf(
            /* translators: %s: comma-separated list of regions the store ships to */
            __( 'We ship to %s.', 'storevoiceai-for-woocommerce' ),
            $this->join_human_list( $regions )
        );

        if ( ! empty( $region_free_min ) ) {
            $symbol = function_exists( 'get_woocommerce_currency_symbol' ) ? html_entity_decode( get_woocommerce_currency_symbol() ) : '';
            $format_amount = static function ( $min ) use ( $symbol ) {
                return $symbol . rtrim( rtrim( number_format( (float) $min, 2, '.', '' ), '0' ), '.' );
            };
            $unique_thresholds = array_unique( array_map( 'floatval', array_values( $region_free_min ) ) );

            if ( count( $unique_thresholds ) === 1 ) {
                $policy .= ' ' . sprintf(
                    /* translators: %s: minimum order amount including currency symbol, e.g. $50 */
                    __( 'Free shipping on orders over %s.', 'storevoiceai-for-woocommerce' ),
                    $format_amount( reset( $region_free_min ) )
                );
            } else {
                foreach ( $region_free_min as $region_name => $min ) {
                    $policy .= ' ' . sprintf(
                        /* translators: 1: minimum order amount including currency symbol, e.g. $50, 2: shipping region name */
                        __( 'Free shipping on orders over %1$s in %2$s.', 'storevoiceai-for-woocommerce' ),
                        $format_amount( $min ),
                        $region_name
                    );
                }
            }
        }

        return $policy;
    }

    private function default_shipping_policy()
    {
        return __( 'We ship worldwide. Contact us for delivery timeframes to your location.', 'storevoiceai-for-woocommerce' );
    }

    private function default_returns_policy()
    {
        return __( 'We accept returns within 30 days of purchase. Contact us to initiate a return.', 'storevoiceai-for-woocommerce' );
    }

    /**
     * Join a list into human-readable prose: "A", "A and B", "A, B, and C".
     */
    private function join_human_list( array $items )
    {
        $items = array_values( array_filter( array_map( 'trim', $items ), 'strlen' ) );
        $count = count( $items );

        if ( $count === 0 ) {
            return '';
        }
        if ( $count === 1 ) {
            return $items[0];
        }
        if ( $count === 2 ) {
            /* translators: 1: first item, 2: second item */
            return sprintf( __( '%1$s and %2$s', 'storevoiceai-for-woocommerce' ), $items[0], $items[1] );
        }

        $last = array_pop( $items );
        /* translators: 1: comma-separated leading items, 2: final item */
        return sprintf( __( '%1$s, and %2$s', 'storevoiceai-for-woocommerce' ), implode( ', ', $items ), $last );
    }

    /**
     * Persist which step the user is on. Lets us resume mid-flow if they
     * close the tab.
     */
    public function ajax_save_step()
    {
        $this->verify();
        $step = isset( $_POST['step'] ) ? max( 0, min( 4, (int) $_POST['step'] ) ) : 0;
        update_option( self::OPT_STEP, $step );
        wp_send_json_success( array( 'step' => $step ) );
    }

    /**
     * Save the agent name + greeting + escalation in one round-trip
     * from the customize/activate steps. Each field is optional so the same
     * endpoint can serve multiple steps.
     */
    public function ajax_save_agent()
    {
        $this->verify();

        $updated = array();

        if ( isset( $_POST['agent_name'] ) ) {
            $name = sanitize_text_field( wp_unslash( $_POST['agent_name'] ) );
            if ( $name !== '' ) {
                update_option( '_storevoiceai_assistant_name', $name );
                $updated['agent_name'] = $name;
            }
        }

        if ( isset( $_POST['greeting'] ) ) {
            $greeting = sanitize_textarea_field( wp_unslash( $_POST['greeting'] ) );
            if ( $greeting !== '' ) {
                update_option( '_storevoiceai_assistant_first_message', $greeting );
                $updated['greeting'] = $greeting;
            }
        }

        if ( isset( $_POST['escalation_mode'] ) ) {
            $mode = sanitize_key( wp_unslash( $_POST['escalation_mode'] ) );
            if ( in_array( $mode, array( 'always', 'hours', 'email' ), true ) ) {
                update_option( self::OPT_ESCALATION, $mode );
                $updated['escalation_mode'] = $mode;
            }
        }

        if ( isset( $_POST['escalation_value'] ) ) {
            $value = sanitize_text_field( wp_unslash( $_POST['escalation_value'] ) );
            update_option( self::OPT_ESCALATION_VAL, $value );
            $updated['escalation_value'] = $value;
        }

        if ( isset( $_POST['shipping_policy'] ) ) {
            $shipping = sanitize_textarea_field( wp_unslash( $_POST['shipping_policy'] ) );
            update_option( '_storevoiceai_shipping_policy', $shipping );
            $updated['shipping_policy'] = $shipping;
        }

        if ( isset( $_POST['returns_policy'] ) ) {
            $returns = sanitize_textarea_field( wp_unslash( $_POST['returns_policy'] ) );
            update_option( '_storevoiceai_returns_policy', $returns );
            $updated['returns_policy'] = $returns;
        }

        if ( isset( $_POST['owner_email'] ) ) {
            $raw_owner_email = wp_unslash( $_POST['owner_email'] );
            if ( is_array( $raw_owner_email ) ) {
                wp_send_json_error(
                    array( 'message' => __( 'Please enter a valid contact email address.', 'storevoiceai-for-woocommerce' ) ),
                    400
                );
            }
            $raw_owner_email = trim( (string) $raw_owner_email );
            if ( $raw_owner_email !== '' && ! is_email( $raw_owner_email ) ) {
                wp_send_json_error(
                    array( 'message' => __( 'Please enter a valid contact email address.', 'storevoiceai-for-woocommerce' ) ),
                    400
                );
            }
            $owner_email = sanitize_email( $raw_owner_email );
            update_option( '_storevoiceai_owner_email', $owner_email );
            $updated['owner_email'] = $owner_email;
        }

        if ( isset( $_POST['working_hours'] ) ) {
            $raw = wp_unslash( $_POST['working_hours'] );
            $decoded = is_string( $raw ) ? json_decode( $raw, true ) : ( is_array( $raw ) ? $raw : null );
            if ( is_array( $decoded ) ) {
                $hours = $this->sanitize_working_hours( $decoded );
                update_option( self::OPT_WORKING_HOURS, $hours );
                $updated['working_hours'] = $hours;
            }
        }

        // Best-effort push to the upstream agent — non-fatal since the user
        // can still edit any of these later from Settings.
        $agent_id = get_option( '_storevoiceai_agentid' );
        if ( $agent_id && (
            isset( $updated['agent_name'] ) ||
            isset( $updated['greeting'] ) ||
            isset( $updated['escalation_value'] )
        ) ) {
            $body = array();
            if ( isset( $updated['agent_name'] ) ) {
                $body['personaName'] = $updated['agent_name'];
            }
            if ( isset( $updated['greeting'] ) ) {
                $body['firstMessage'] = $updated['greeting'];
            }
            // Push the transfer number (may be empty to clear it upstream).
            if ( isset( $updated['escalation_value'] ) ) {
                $body['transferPhoneNumber'] = $updated['escalation_value'];
            }
            if ( ! empty( $body ) ) {
                $api = new Store_Voice_AI_API();
                $api->update_agent( $agent_id, $body );
            }
        }

        // Policies and contact email are site-level settings, pushed to the
        // dedicated site endpoint. Best-effort, like the agent push above.
        if (
            isset( $updated['shipping_policy'] ) ||
            isset( $updated['returns_policy'] ) ||
            isset( $updated['owner_email'] )
        ) {
            $site_body = array();
            if ( isset( $updated['shipping_policy'] ) ) {
                $site_body['shippingPolicy'] = $updated['shipping_policy'];
            }
            if ( isset( $updated['returns_policy'] ) ) {
                $site_body['returnsPolicy'] = $updated['returns_policy'];
            }
            if ( isset( $updated['owner_email'] ) ) {
                $site_body['ownerEmail'] = $updated['owner_email'];
            }
            if ( ! empty( $site_body ) ) {
                $api = new Store_Voice_AI_API();
                $api->update_site( $site_body );
            }
        }

        // Sync the weekly schedule to the upstream default availability
        // profile. Best-effort: the option is already saved locally, so a
        // network failure here doesn't block the wizard.
        if ( isset( $updated['working_hours'] ) ) {
            $this->sync_default_availability_profile( $updated['working_hours'] );
        }

        wp_send_json_success( $updated );
    }

    /**
     * Mark onboarding done and clear transient flags. The JS then redirects
     * to the dashboard.
     */
    public function ajax_finish()
    {
        $this->verify();
        update_option( self::OPT_COMPLETE, 1 );
        update_option( self::OPT_STEP, 5 );
        delete_option( self::OPT_REDIRECT );
        wp_send_json_success( array(
            'redirect' => admin_url( 'admin.php?page=storevoiceai-for-woocommerce-dashboard' ),
        ) );
    }

    /**
     * Mint a Retell web-call access token for the "Try a call" step. The
     * browser hands this token to the Retell Web Client SDK to open a live
     * call to the agent. Mirrors Call_Widget::create_call() but authenticated
     * with the onboarding nonce + capability check.
     */
    public function ajax_create_call()
    {
        $this->verify();

        $api    = new Store_Voice_AI_API();
        $result = $api->create_call();

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array(
                'message' => $result->get_error_message(),
                'code'    => $result->get_error_code(),
            ) );
        }

        wp_send_json_success( $result );
    }

    /**
     * Trigger an upstream call-log sync. Called on its own (ahead of
     * ajax_retrieve_call) so the wizard can show a short processing pause while
     * the call we just placed finishes processing on the backend.
     */
    public function ajax_sync_calls()
    {
        $this->verify();

        $api    = new Store_Voice_AI_API();
        $result = $api->sync_calls();

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array( 'message' => $result->get_error_message() ) );
        }

        wp_send_json_success( is_array( $result ) ? $result : array() );
    }

    /**
     * Retrieve the agent's "notes" for the call just placed: return the summary,
     * recording URL and transcript for that call. The caller is expected to have
     * already run ajax_sync_calls so the logs are fresh. Falls back to the most
     * recent call on record if the browser didn't pass a call id.
     */
    public function ajax_retrieve_call()
    {
        $this->verify();

        $call_id = isset( $_POST['call_id'] ) ? sanitize_text_field( wp_unslash( $_POST['call_id'] ) ) : '';

        $api = new Store_Voice_AI_API();

        // Resolve against the synced logs so we hand get_call_details()
        // the same id shape the call-logs UI uses. Prefer the row matching the
        // call we just placed; otherwise fall back to the most recent call.
        $logs = $api->get_call_logs();
        if ( ! is_wp_error( $logs ) ) {
            $resolved = $this->resolve_call_id( $logs, $call_id );
            if ( $resolved !== '' ) {
                $call_id = $resolved;
            }
        }

        if ( $call_id === '' ) {
            wp_send_json_error( array(
                'message' => __( 'We couldn’t find your call yet. Please try again in a moment.', 'storevoiceai-for-woocommerce' ),
            ) );
        }

        $details = $api->get_call_details( $call_id );
        if ( is_wp_error( $details ) ) {
            wp_send_json_error( array( 'message' => $details->get_error_message() ) );
        }

        $details = is_array( $details ) ? $details : array();

        wp_send_json_success( array(
            'callId'       => $call_id,
            'summary'      => isset( $details['summary'] ) ? $details['summary'] : '',
            'recordingUrl' => isset( $details['recordingUrl'] ) ? $details['recordingUrl'] : '',
            'transcript'   => isset( $details['transcript'] ) ? $details['transcript'] : '',
        ) );
    }

    /**
     * Resolve which call to show from a call-logs payload. If $prefer matches a
     * row's id (or its provider call id), that row wins; otherwise we return the
     * most recently started call. Returns '' when there are no usable rows.
     */
    private function resolve_call_id( $logs, $prefer = '' )
    {
        $rows = ( is_array( $logs ) && isset( $logs['data'] ) && is_array( $logs['data'] ) ) ? $logs['data'] : array();

        $best_id = '';
        $best_ts = -1;
        foreach ( $rows as $row ) {
            if ( ! is_array( $row ) || empty( $row['id'] ) ) {
                continue;
            }

            // Exact match on the call we placed — use it regardless of recency.
            if ( $prefer !== '' ) {
                foreach ( array( 'id', 'callId', 'call_id', 'providerCallId' ) as $key ) {
                    if ( isset( $row[ $key ] ) && (string) $row[ $key ] === $prefer ) {
                        return (string) $row['id'];
                    }
                }
            }

            $ts = isset( $row['startedAt'] ) ? strtotime( (string) $row['startedAt'] ) : false;
            $ts = ( $ts === false ) ? 0 : $ts;
            if ( $ts >= $best_ts ) {
                $best_ts = $ts;
                $best_id = (string) $row['id'];
            }
        }

        if ( $prefer !== '' ) {
            return '';
        }

        return $best_id;
    }

    private function verify()
    {
        check_ajax_referer( self::NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            wp_send_json_error(
                array( 'message' => __( 'You do not have permission to perform this action.', 'storevoiceai-for-woocommerce' ) ),
                403
            );
        }
    }
}
