<?php
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.SafeRedirect.wp_redirect_wp_redirect, WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText, WordPress.DateTime.RestrictedFunctions.date_date, WordPress.WP.AlternativeFunctions.rand_mt_rand, WordPress.PHP.DevelopmentFunctions.error_log_error_log, Squiz.PHP.DiscouragedFunctions.Discouraged, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table names are built from the WordPress database prefix and escaped before use.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class YATOON_AI {

    private static function get_prompt_builder( $prompt ) {
        $function = 'wp_ai_client_' . 'prompt';
        return $function( $prompt );
    }

    public function __construct() {
        add_action( 'wp_ajax_yatoon_ai_service_recommendations', array( $this, 'ajax_service_recommendations' ) );
        add_action( 'wp_ajax_nopriv_yatoon_ai_service_recommendations', array( $this, 'ajax_service_recommendations' ) );
        add_action( 'wp_ajax_yatoon_ai_consent_receipt', array( $this, 'ajax_consent_receipt' ) );
        add_action( 'wp_ajax_nopriv_yatoon_ai_consent_receipt', array( $this, 'ajax_consent_receipt' ) );
        add_action( 'wp_ajax_yatoon_ai_message_reply', array( $this, 'ajax_message_reply' ) );
        add_action( 'wp_ajax_yatoon_ai_service_description', array( $this, 'ajax_service_description' ) );
    }

    public static function consent_text() {
        return __( 'I agree that this request may be sent to the AI provider configured by this site. I have not included medical, payment, or account details.', 'yatoon-booking-system' );
    }

    public function ajax_consent_receipt() {
        $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : '';
        // The same receipt protects the public booking helper and the verified
        // customer portal. Keep the original check_ajax_referer() path for the
        // public form while accepting the portal's independently scoped nonce.
        if ( ! wp_verify_nonce( $nonce, 'yatoon_portal_nonce' ) ) {
            check_ajax_referer( 'yatoon_frontend_nonce', 'nonce' );
        }
        if ( '1' !== (string) ( $_POST['agreed'] ?? '' ) ) {
            wp_send_json_error( array( 'message' => __( 'Please agree before sending your request to the configured third-party AI provider.', 'yatoon-booking-system' ) ) );
        }
        YATOON_Request_Guard::enforce( 'ai_consent_receipt', 30, 10 );

        $token = wp_generate_password( 40, false, false );
        set_transient(
            'yatoon_ai_consent_' . hash( 'sha256', $token ),
            array(
                'consented_at' => current_time( 'mysql', true ),
                'text_hash'    => hash( 'sha256', wp_strip_all_tags( self::consent_text() ) ),
                'client_hash'  => $this->client_hash(),
            ),
            15 * MINUTE_IN_SECONDS
        );
        wp_send_json_success( array( 'receipt' => $token ) );
    }

    public static function is_text_available() {
        if ( ! function_exists( 'wp_ai_client_prompt' ) ) {
            return false;
        }

        try {
            $builder = self::get_prompt_builder( 'test' );
            if ( method_exists( $builder, 'is_supported_for_text_generation' ) ) {
                return (bool) $builder->is_supported_for_text_generation();
            }
        } catch ( \Throwable $e ) {
            yatoon_safe_log( 'AI support check failed: ' . $e->getMessage() );
            return false;
        }

        return true;
    }

    public static function status_message() {
        if ( ! function_exists( 'wp_ai_client_prompt' ) ) {
            return __( 'AI requires WordPress 7.0 or newer. This site will keep working normally without AI.', 'yatoon-booking-system' );
        }
        if ( ! self::is_text_available() ) {
            return __( 'AI is not available yet. Connect an AI provider in WordPress Settings > Connectors.', 'yatoon-booking-system' );
        }
        return __( 'AI is ready.', 'yatoon-booking-system' );
    }

    public function ajax_service_recommendations() {
        check_ajax_referer( 'yatoon_frontend_nonce', 'nonce' );

        if ( get_option( 'yatoon_ai_service_helper_enabled', '1' ) !== '1' ) {
            wp_send_json_error( array( 'message' => __( 'AI service helper is disabled.', 'yatoon-booking-system' ) ) );
        }

        $consent_receipt = isset( $_POST['ai_consent_receipt'] ) ? sanitize_text_field( wp_unslash( $_POST['ai_consent_receipt'] ) ) : '';
        if ( ! $this->consume_consent_receipt( $consent_receipt ) ) {
            wp_send_json_error( array( 'message' => __( 'Please agree before sending your request to the configured third-party AI provider.', 'yatoon-booking-system' ) ) );
        }

        $need = isset( $_POST['need'] ) ? sanitize_textarea_field( wp_unslash( $_POST['need'] ) ) : '';
        if ( strlen( $need ) < 3 ) {
            wp_send_json_error( array( 'message' => __( 'Tell us what you are looking for first.', 'yatoon-booking-system' ) ) );
        }
        $screened = $this->screen_ai_input( $need );
        if ( ! empty( $screened['blocked'] ) ) {
            wp_send_json_error( array( 'message' => __( 'For your privacy, remove payment-card, password, or account details before using the AI helper.', 'yatoon-booking-system' ) ) );
        }
        $need           = (string) $screened['safe_text'];
        $privacy_notice = ! empty( $screened['redacted'] )
            ? __( 'A sentence containing health information was kept on this device and was not sent to the AI provider. Contact the salon directly for advice involving allergies, pregnancy, medication, or other health needs.', 'yatoon-booking-system' )
            : '';
        if ( strlen( trim( $need ) ) < 3 ) {
            wp_send_json_success( array(
                'answer'          => wp_kses_post( wpautop( $privacy_notice ) ),
                'recommendations' => array(),
                'privacy_notice'  => $privacy_notice,
                'local_only'      => true,
            ) );
        }

        $services = $this->get_services_for_prompt();
        if ( empty( $services ) ) {
            wp_send_json_error( array( 'message' => __( 'No services are available for AI recommendations yet.', 'yatoon-booking-system' ) ) );
        }

        $profile = sanitize_key( get_option( 'yatoon_ai_frontend_profile', 'fast' ) );
        if ( ! in_array( $profile, array( 'fast', 'balanced', 'quality' ), true ) ) {
            $profile = 'fast';
        }
        $cache_minutes = min( 120, max( 5, absint( get_option( 'yatoon_ai_cache_minutes', 30 ) ) ) );
        $cache_key = 'yatoon_ai_rec_' . md5( get_current_blog_id() . '|' . mb_strtolower( trim( $need ) ) . '|' . wp_json_encode( $services ) );
        $cached = get_transient( $cache_key );
        if ( is_array( $cached ) && ! empty( $cached['answer'] ) ) {
            wp_send_json_success( array(
                'answer'          => wp_kses_post( wpautop( $cached['answer'] ) ),
                'recommendations' => isset( $cached['recommendations'] ) && is_array( $cached['recommendations'] ) ? $cached['recommendations'] : array(),
                'cached'          => true,
            ) );
        }

        // Keep abusive automation out without making normal customers wait ten
        // minutes because earlier provider errors were counted as completed AI
        // uses. The burst guard counts every valid submission; the larger quota
        // counts only successful recommendations.
        $this->enforce_rate_limit( 'ai_service_burst', 12, 1 );
        $this->enforce_rate_limit( 'ai_service_provider', 20, 10 );
        $this->enforce_global_ai_cap();

        // A style, maintenance, or budget question does not need fourteen days
        // of schedule generation. Only pay that cost when the customer actually
        // asks about dates, staff, or open times.
        $availability = $this->request_needs_availability( $need )
            ? $this->get_availability_snapshot( $this->pick_likely_services( $need, $services, 4 ), $need )
            : array();
        $prompt_services = $this->pick_likely_services( $need, $services, 30 );

        $business_name = sanitize_text_field( get_option( 'yatoon_business_name', get_bloginfo( 'name' ) ) );
        $prompt = "A customer is choosing an appointment service for {$business_name}.\n\n";
        $prompt .= "Customer request:\n" . $need . "\n\n";
        $prompt .= "Available services:\n" . wp_json_encode( $prompt_services ) . "\n\n";
        if ( ! empty( $availability ) ) {
            $prompt .= "Realtime availability snapshot from the booking system:\n" . wp_json_encode( array_values( $availability ) ) . "\n\n";
        }
        $prompt .= "Recommend up to 3 matching services. Use exact service names from the available services list so the booking form can make them clickable. Use the customer's language when possible. Be concise, friendly, and practical. Format as clean plain text with numbered recommendations. Do not use Markdown, asterisks, headings, or tables. For each recommendation include: service name, why it matches, and any useful note about length, art, maintenance, budget, or availability. If the customer asks when, earliest, latest, today, tomorrow, available staff, or free time, use the realtime availability snapshot when available. Do not invent appointment times, staff, prices, durations, policies, medical advice, or guarantees. Say that the customer should pick a service on the booking form to confirm the live time slot before submitting.";

        // Provider instruction, not UI copy: keep stable and untranslated so locale changes do not alter AI behavior.
        $text = $this->generate_text( $prompt, 'You are a polished salon consultation assistant. You help customers describe their desired result and choose from the provided service menu only.', $profile );
        if ( is_wp_error( $text ) ) {
            $fallback = $this->local_service_fallback( $need, $services, $availability );
            wp_send_json_success( array(
                'answer'          => wp_kses_post( wpautop( $fallback['answer'] ) ),
                'recommendations' => $fallback['recommendations'],
                'fallback'        => true,
            ) );
        }

        $clean_text = $this->trim_ai_text( $text );
        $recommendations = $this->match_services_from_text( $clean_text, $services, $availability );
        set_transient( $cache_key, array(
            'answer'          => $clean_text,
            'recommendations' => $recommendations,
        ), $cache_minutes * MINUTE_IN_SECONDS );
        wp_send_json_success( array(
            'answer'          => wp_kses_post( wpautop( $clean_text ) ),
            'recommendations' => $recommendations,
            'privacy_notice'  => $privacy_notice,
        ) );
    }

    public function ajax_message_reply() {
        check_ajax_referer( 'yatoon_ai_message_reply', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => __( 'Permission denied.', 'yatoon-booking-system' ) ) );
        }
        if ( ! yatoon_is_pro() ) {
            wp_send_json_error( array( 'message' => __( 'AI reply drafts require an active Pro license.', 'yatoon-booking-system' ) ) );
        }
        if ( get_option( 'yatoon_ai_reply_drafts_enabled', '1' ) !== '1' ) {
            wp_send_json_error( array( 'message' => __( 'AI reply drafts are disabled.', 'yatoon-booking-system' ) ) );
        }

        $message_id = isset( $_POST['message_id'] ) ? absint( $_POST['message_id'] ) : 0;
        if ( ! $message_id ) {
            wp_send_json_error( array( 'message' => __( 'Invalid message.', 'yatoon-booking-system' ) ) );
        }

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_contact_messages' );
        $msg = yatoon_db_get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $message_id ) );
        if ( ! $msg ) {
            wp_send_json_error( array( 'message' => __( 'Message not found.', 'yatoon-booking-system' ) ) );
        }

        $business_name = sanitize_text_field( get_option( 'yatoon_business_name', get_bloginfo( 'name' ) ) );
        $business_phone = sanitize_text_field( get_option( 'yatoon_business_phone', get_option( 'yatoon_contact_phone', '' ) ) );
        $prompt = "Write a reply draft for this salon/spa customer message.\n\n";
        $prompt .= "Business: {$business_name}\n";
        if ( $business_phone ) {
            $prompt .= "Business phone: {$business_phone}\n";
        }
        $prompt .= 'Customer name: ' . sanitize_text_field( $msg->name ) . "\n";
        $prompt .= "Customer message:\n" . sanitize_textarea_field( $msg->message ) . "\n\n";
        $prompt .= 'Keep it under 120 words. Sound warm and professional. Do not promise a booking time, price, refund, diagnosis, or medical/safety outcome. If the question needs staff confirmation, say the team will confirm.';

        // Provider instruction, not UI copy: keep stable and untranslated so locale changes do not alter AI behavior.
        $text = $this->generate_text( $prompt, 'You write short customer-service reply drafts for a salon booking team.' );
        if ( is_wp_error( $text ) ) {
            wp_send_json_error( array( 'message' => $text->get_error_message(), 'status' => self::status_message() ) );
        }

        wp_send_json_success( array(
            'reply' => $this->trim_ai_text( $text ),
        ) );
    }

    public function ajax_service_description() {
        check_ajax_referer( 'yatoon_ai_service_description', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => __( 'Permission denied.', 'yatoon-booking-system' ) ) );
        }

        $service_name = isset( $_POST['service_name'] ) ? sanitize_text_field( wp_unslash( $_POST['service_name'] ) ) : '';
        if ( $service_name === '' ) {
            wp_send_json_error( array( 'message' => __( 'Enter a service name first.', 'yatoon-booking-system' ) ) );
        }

        $category     = isset( $_POST['category'] ) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : '';
        $price        = isset( $_POST['price'] ) ? sanitize_text_field( wp_unslash( $_POST['price'] ) ) : '';
        $duration     = isset( $_POST['duration'] ) ? absint( $_POST['duration'] ) : 0;
        $description  = isset( $_POST['description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['description'] ) ) : '';
        $business     = sanitize_text_field( get_option( 'yatoon_business_name', get_bloginfo( 'name' ) ) );

        $prompt = "Write a polished customer-facing service description for a salon/spa booking menu.\n\n";
        $prompt .= "Business: {$business}\n";
        $prompt .= "Service name: {$service_name}\n";
        if ( $category ) {
            $prompt .= "Category: {$category}\n";
        }
        if ( $duration ) {
            $prompt .= "Duration: {$duration} minutes\n";
        }
        if ( $price !== '' ) {
            $prompt .= "Price: {$price}\n";
        }
        if ( $description !== '' ) {
            $prompt .= "Existing description:\n{$description}\n";
        }
        $prompt .= "\nCreate 2 to 4 short sentences that help a customer understand the result, who it is best for, and what to mention when booking. Make it useful for AI service matching too. Do not use Markdown, headings, asterisks, emojis, or bullet points. Do not invent policies, guarantees, medical claims, exact products, discounts, or hidden inclusions. Keep the tone professional, clear, and inviting.";

        // Provider instruction, not UI copy: keep stable and untranslated so locale changes do not alter AI behavior.
        $text = $this->generate_text( $prompt, 'You write accurate, polished service descriptions for salon and spa booking menus. Use only the supplied service details and avoid unsupported claims.' );
        if ( is_wp_error( $text ) ) {
            wp_send_json_error( array( 'message' => $text->get_error_message(), 'status' => self::status_message() ) );
        }

        wp_send_json_success( array(
            'description' => mb_substr( $this->trim_ai_text( $text ), 0, 900 ),
        ) );
    }

    private function get_services_for_prompt() {
        global $wpdb;
        $table = $wpdb->prefix . 'yatoon_services';
        $exists = yatoon_db_get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        if ( $exists !== $table ) {
            return array();
        }

        $rows = yatoon_db_get_results( "SELECT id, name, description, category, price, duration_minutes FROM {$table} WHERE is_active = 1 ORDER BY sort_order ASC, name ASC LIMIT 60" );
        $services = array();
        foreach ( (array) $rows as $row ) {
            $services[] = array(
                'id'          => (int) $row->id,
                'name'        => sanitize_text_field( $row->name ),
                'category'    => sanitize_text_field( $row->category ),
                // Long marketing copy increases latency and free-tier token use
                // without improving a three-item recommendation very much.
                'description' => mb_substr( wp_strip_all_tags( (string) $row->description ), 0, 220 ),
                'price'       => (float) $row->price,
                'duration'    => (int) $row->duration_minutes,
            );
        }
        return $services;
    }

    private function pick_likely_services( $need, $services, $limit = 6 ) {
        $need_lc = mb_strtolower( wp_strip_all_tags( (string) $need ) );
        $terms = preg_split( '/[^\p{L}\p{N}]+/u', $need_lc, -1, PREG_SPLIT_NO_EMPTY );
        $terms = array_values( array_filter( array_unique( (array) $terms ), function ( $term ) {
            return mb_strlen( $term ) >= 3;
        } ) );

        $scored = array();
        foreach ( (array) $services as $service ) {
            $haystack = mb_strtolower( implode( ' ', array(
                $service['name'] ?? '',
                $service['category'] ?? '',
                $service['description'] ?? '',
            ) ) );
            $score = 0;
            foreach ( $terms as $term ) {
                if ( mb_strpos( $haystack, $term ) !== false ) {
                    $score += 3;
                }
            }
            if ( $score === 0 && ! empty( $service['name'] ) && mb_strpos( $need_lc, mb_strtolower( (string) $service['name'] ) ) !== false ) {
                $score += 8;
            }
            if ( $score > 0 ) {
                $scored[] = array( 'score' => $score, 'service' => $service );
            }
        }

        usort( $scored, function ( $a, $b ) {
            return $b['score'] <=> $a['score'];
        } );

        if ( empty( $scored ) ) {
            return array_slice( $services, 0, $limit );
        }

        return array_map( function ( $item ) {
            return $item['service'];
        }, array_slice( $scored, 0, $limit ) );
    }

    private function get_availability_snapshot( $services, $need = '' ) {
        if ( empty( $services ) || ! class_exists( 'YATOON_Business_Hours' ) ) {
            return array();
        }

        $snapshot = array();
        $platform = get_option( 'yatoon_active_platform', 'local' );
        $intent   = $this->get_availability_intent( $need );
        foreach ( (array) $services as $service ) {
            $service_id = absint( $service['id'] ?? 0 );
            if ( ! $service_id ) {
                continue;
            }

            $openings = $this->get_next_openings_for_service( $service_id, 14, 4, $intent );

            // Provider context, not rendered UI: keep stable and untranslated to avoid locale-dependent model behavior.
            $snapshot[ $service_id ] = array(
                'service_id'      => $service_id,
                'service_name'    => sanitize_text_field( $service['name'] ?? '' ),
                'staff_available' => $this->get_staff_names_for_service( $service_id, 6 ),
                'next_openings'   => $openings,
                'note'            => empty( $openings )
                    ? 'No synced open times found in the next 14 days.'
                    : ( $platform === 'local'
                        ? 'Times are a local availability snapshot and must be confirmed in the booking form.'
                        : 'Times are based on synced local availability data, not a fresh Square or Vagaro live query, and must be confirmed in the booking form.' ),
            );
        }

        return $snapshot;
    }

    private function request_needs_availability( $need ) {
        $text = mb_strtolower( wp_strip_all_tags( (string) $need ) );
        return self::text_has_any( $text, array(
            'when', 'available', 'availability', 'opening', 'open time', 'free time', 'earliest', 'latest',
            'today', 'tomorrow', 'morning', 'afternoon', 'evening', 'staff', 'technician',
            '什么时候', '几点', '有空', '空闲', '最早', '最晚', '今天', '明天', '后天', '上午', '下午', '晚上', '技师', '员工',
        ) );
    }

    /** Return useful, clickable service choices even during a provider outage. */
    private function local_service_fallback( $need, $services, $availability = array() ) {
        $likely = array_slice( $this->pick_likely_services( $need, $services, 3 ), 0, 3 );
        $recommendations = array();
        foreach ( $likely as $service ) {
            $service_id = absint( $service['id'] ?? 0 );
            $item = array(
                'id'       => $service_id,
                'name'     => sanitize_text_field( $service['name'] ?? __( 'Service', 'yatoon-booking-system' ) ),
                'price'    => (float) ( $service['price'] ?? 0 ),
                'duration' => absint( $service['duration'] ?? 0 ),
            );
            if ( isset( $availability[ $service_id ]['next_openings'][0]['label'] ) ) {
                $item['next_opening'] = sanitize_text_field( $availability[ $service_id ]['next_openings'][0]['label'] );
            }
            $recommendations[] = $item;
        }

        $is_cjk = (bool) preg_match( '/[\x{3400}-\x{9fff}]/u', (string) $need );
        $answer = $is_cjk
            ? '智能服务现在响应较慢，我先根据本店真实服务菜单列出几个可以直接查看的项目。请点击项目确认具体款式、价格和实时空档。'
            : 'The AI provider is responding slowly, so here are a few services from the live menu you can review now. Select one to confirm the exact options, price, and live availability.';

        return array( 'answer' => $answer, 'recommendations' => $recommendations );
    }

    private function get_availability_intent( $need ) {
        $text = mb_strtolower( wp_strip_all_tags( (string) $need ) );

        $intent = array(
            'day_offset'              => null,
            'earliest_start_minute'   => null,
            'latest_start_before_min' => null,
        );

        // NOTE (4.4.3): the CJK keywords here were mangled by a GBK/UTF-8
        // double-encoding accident, so Chinese input never matched any branch
        // below. Restored, widened with synonyms, and routed through
        // text_has_any() so matching is case-insensitive.
        if ( self::text_has_any( $text, array( 'day after tomorrow', '后天' ) ) ) {
            $intent['day_offset'] = 2;
        } elseif ( self::text_has_any( $text, array( 'tomorrow', '明天', '明日' ) ) ) {
            $intent['day_offset'] = 1;
        } elseif ( self::text_has_any( $text, array( 'today', '今天', '今日' ) ) ) {
            $intent['day_offset'] = 0;
        }

        if ( self::text_has_any( $text, array( 'morning', 'before noon', 'before 12', '上午', '早上', '中午之前' ) ) ) {
            $intent['latest_start_before_min'] = 12 * 60;
        } elseif ( self::text_has_any( $text, array( 'afternoon', '下午' ) ) ) {
            $intent['earliest_start_minute'] = 12 * 60;
        } elseif ( self::text_has_any( $text, array( 'evening', 'tonight', '晚上', '夜间' ) ) ) {
            $intent['earliest_start_minute'] = 17 * 60;
        }

        return $intent;
    }

    /**
     * Case-insensitive "does $text contain any of these needles" check.
     *
     * Uses mb_* so multibyte (CJK) needles match on character boundaries
     * rather than raw bytes.
     *
     * @param string   $text    Haystack.
     * @param string[] $needles Needles to look for.
     * @return bool
     */
    private static function text_has_any( $text, $needles ) {
        $text = function_exists( 'mb_strtolower' ) ? mb_strtolower( (string) $text, 'UTF-8' ) : strtolower( (string) $text );
        foreach ( (array) $needles as $needle ) {
            $needle = (string) $needle;
            if ( '' === $needle ) {
                continue;
            }
            $needle = function_exists( 'mb_strtolower' ) ? mb_strtolower( $needle, 'UTF-8' ) : strtolower( $needle );
            if ( mb_strpos( $text, $needle ) !== false ) {
                return true;
            }
        }
        return false;
    }

    private function score_slot_for_availability_intent( DateTime $slot_start, $intent ) {
        if ( empty( $intent ) || ! is_array( $intent ) ) {
            return 0;
        }

        $score = 0;
        $minutes = ( (int) $slot_start->format( 'G' ) * 60 ) + (int) $slot_start->format( 'i' );

        if ( isset( $intent['day_offset'] ) && $intent['day_offset'] !== null ) {
            $tz          = $slot_start->getTimezone();
            $today       = new DateTime( 'today', $tz );
            $slot_day    = new DateTime( $slot_start->format( 'Y-m-d' ) . ' 00:00:00', $tz );
            $actual_days = (int) $today->diff( $slot_day )->format( '%r%a' );
            if ( $actual_days !== (int) $intent['day_offset'] ) {
                $score += 100 + abs( $actual_days - (int) $intent['day_offset'] );
            }
        }

        if ( isset( $intent['earliest_start_minute'] ) && $intent['earliest_start_minute'] !== null && $minutes < (int) $intent['earliest_start_minute'] ) {
            $score += 25 + ( ( (int) $intent['earliest_start_minute'] - $minutes ) / 60 );
        }

        if ( isset( $intent['latest_start_before_min'] ) && $intent['latest_start_before_min'] !== null && $minutes >= (int) $intent['latest_start_before_min'] ) {
            $score += 25 + ( ( $minutes - (int) $intent['latest_start_before_min'] ) / 60 );
        }

        return $score;
    }

    private function get_next_openings_for_service( $service_id, $days = 14, $limit = 4, $intent = array() ) {
        $service_id = absint( $service_id );
        if ( ! $service_id || ! class_exists( 'YATOON_Business_Hours' ) ) {
            return array();
        }

        $timezone = yatoon_timezone_string();
        try {
            $tz = new DateTimeZone( $timezone );
        } catch ( Exception $e ) {
            $tz = yatoon_timezone();
        }

        $now = new DateTime( 'now', $tz );
        $candidates = array();

        for ( $d = 0; $d < $days; $d++ ) {
            $date = ( clone $now )->modify( '+' . $d . ' days' );
            $date_str = $date->format( 'Y-m-d' );
            $slots = YATOON_Business_Hours::generate_slots( $date_str, $service_id, 0 );
            if ( empty( $slots ) || ! is_array( $slots ) ) {
                continue;
            }

            foreach ( $slots as $slot ) {
                $time = is_array( $slot ) ? ( $slot['time'] ?? '' ) : (string) $slot;
                $display = is_array( $slot ) ? ( $slot['display'] ?? '' ) : '';
                if ( ! $time ) {
                    continue;
                }
                try {
                    $slot_start = new DateTime( $date_str . ' ' . $time, $tz );
                } catch ( Exception $e ) {
                    continue;
                }
                $candidates[] = array(
                    'score'   => $this->score_slot_for_availability_intent( $slot_start, $intent ),
                    'ts'      => $slot_start->getTimestamp(),
                    'opening' => array(
                        'date'  => $date_str,
                        'day'   => $date->format( 'D, M j' ),
                        'time'  => $display ?: $time,
                        'label' => $date->format( 'D, M j' ) . ' at ' . ( $display ?: $time ),
                    ),
                );
            }
        }

        usort( $candidates, function ( $a, $b ) {
            if ( $a['score'] === $b['score'] ) {
                return $a['ts'] <=> $b['ts'];
            }
            return $a['score'] <=> $b['score'];
        } );

        return array_map( function ( $item ) {
            return $item['opening'];
        }, array_slice( $candidates, 0, $limit ) );
    }

    private function get_staff_names_for_service( $service_id, $limit = 6 ) {
        global $wpdb;
        $service_id = absint( $service_id );
        if ( ! $service_id ) {
            return array();
        }

        $staff_table = $wpdb->prefix . 'yatoon_staff';
        $assign_table = $wpdb->prefix . 'yatoon_staff_services';
        $assign_exists = yatoon_db_get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $assign_table ) );

        if ( $assign_exists === $assign_table ) {
            $names = yatoon_db_get_col( $wpdb->prepare(
                "SELECT st.name
                 FROM {$staff_table} st
                 INNER JOIN {$assign_table} ss ON st.id = ss.staff_id
                 WHERE ss.service_id = %d AND st.is_active = 1 AND st.is_bookable = 1
                 ORDER BY st.sort_order ASC, st.name ASC
                 LIMIT %d",
                $service_id,
                absint( $limit )
            ) );
            if ( ! empty( $names ) ) {
                return array_map( 'sanitize_text_field', $names );
            }
        }

        $names = yatoon_db_get_col( $wpdb->prepare(
            "SELECT name FROM {$staff_table}
             WHERE is_active = 1 AND is_bookable = 1
             ORDER BY sort_order ASC, name ASC
             LIMIT %d",
            absint( $limit )
        ) );

        return array_map( 'sanitize_text_field', (array) $names );
    }

    private function match_services_from_text( $text, $services, $availability = array() ) {
        $matches = array();
        $haystack = mb_strtolower( wp_strip_all_tags( (string) $text ) );

        foreach ( (array) $services as $service ) {
            $name = isset( $service['name'] ) ? (string) $service['name'] : '';
            if ( $name === '' ) {
                continue;
            }
            if ( mb_strpos( $haystack, mb_strtolower( $name ) ) === false ) {
                continue;
            }

            $service_id = (int) $service['id'];
            $match = array(
                'id'       => (int) $service['id'],
                'name'     => sanitize_text_field( $service['name'] ),
                'price'    => (float) $service['price'],
                'duration' => (int) $service['duration'],
            );
            if ( isset( $availability[ $service_id ] ) ) {
                $next = $availability[ $service_id ]['next_openings'][0]['label'] ?? '';
                if ( $next ) {
                    $match['next_opening'] = sanitize_text_field( $next );
                }
            }

            $matches[] = $match;

            if ( count( $matches ) >= 3 ) {
                break;
            }
        }

        return $matches;
    }

    /**
     * Thin public wrapper so other classes (e.g. the customer-portal AI
     * reschedule assistant) can reuse the same provider selection, retry,
     * and error handling as the built-in AI features without duplicating it.
     */
    public function quick_generate_text( $prompt, $system_instruction = '' ) {
        return $this->generate_text( $prompt, $system_instruction, 'balanced' );
    }

    private function generate_text( $prompt, $system_instruction = '', $profile = 'balanced' ) {
        if ( ! self::is_text_available() ) {
            return new WP_Error( 'yatoon_ai_unavailable', self::status_message() );
        }

        $routes = $this->get_model_routes();
        $profile = in_array( $profile, array( 'fast', 'balanced', 'quality' ), true ) ? $profile : 'balanced';
        $max_attempts = 'fast' === $profile ? 2 : ( 'quality' === $profile ? 4 : 3 );
        $max_tokens   = 'fast' === $profile ? 360 : ( 'quality' === $profile ? 900 : 600 );
        $timeout_cap  = 'fast' === $profile ? 6 : ( 'quality' === $profile ? 15 : 10 );
        $attempts = 0;
        $skip_providers = array();

        foreach ( $routes as $route ) {
            if ( $attempts >= $max_attempts || isset( $skip_providers[ $route['provider'] ] ) ) {
                continue;
            }
            $cooldown_key = 'yatoon_ai_cd_' . md5( $route['provider'] . ':' . $route['model'] );
            if ( get_transient( $cooldown_key ) ) {
                continue;
            }

            ++$attempts;
            try {
                $builder = self::get_prompt_builder( $prompt );
                if ( $system_instruction && method_exists( $builder, 'using_system_instruction' ) ) {
                    $builder = $builder->using_system_instruction( $system_instruction );
                }
                // Modern Gemini 3.x and reasoning-oriented GPT models reject
                // legacy sampling parameters such as temperature. The system
                // instruction provides the required response consistency without
                // reducing cross-provider compatibility.
                if ( method_exists( $builder, 'using_max_tokens' ) ) {
                    $builder = $builder->using_max_tokens( $max_tokens );
                }
                if ( method_exists( $builder, 'using_provider' ) ) {
                    $builder = $builder->using_provider( $route['provider'] );
                }
                if ( method_exists( $builder, 'using_model_preference' ) ) {
                    $builder = $builder->using_model_preference( $route['model'] );
                }

                $timeout_filter = static function ( $timeout ) use ( $timeout_cap ) {
                    return min( (float) $timeout, (float) $timeout_cap );
                };
                add_filter( 'http_request_timeout', $timeout_filter, 999 );
                try {
                    $result = $builder->generate_text();
                } finally {
                    remove_filter( 'http_request_timeout', $timeout_filter, 999 );
                }
                if ( ! is_wp_error( $result ) ) {
                    return $result;
                }

                $status = $this->ai_error_status( $result );
                yatoon_safe_log( sprintf( 'AI route %s/%s failed (%d): %s', $route['provider'], $route['model'], $status, $result->get_error_message() ) );

                if ( in_array( $status, array( 401, 403 ), true ) ) {
                    // Invalid credentials affect every model for this provider.
                    $skip_providers[ $route['provider'] ] = true;
                } elseif ( 429 === $status ) {
                    set_transient( $cooldown_key, '1', 2 * MINUTE_IN_SECONDS );
                    // Free-tier quotas are commonly provider/project-wide, so
                    // another model at the same provider usually wastes time.
                    $skip_providers[ $route['provider'] ] = true;
                } elseif ( in_array( $status, array( 500, 502, 503, 504 ), true ) ) {
                    // A selected model can be discoverable yet temporarily
                    // overloaded. Cool it down and immediately try the next
                    // configured model/provider instead of exposing the 503.
                    set_transient( $cooldown_key, '1', 5 * MINUTE_IN_SECONDS );
                } elseif ( in_array( $status, array( 400, 404, 422 ), true ) ) {
                    set_transient( $cooldown_key, '1', 30 * MINUTE_IN_SECONDS );
                }
            } catch ( \Throwable $e ) {
                yatoon_safe_log( 'AI generation route failed: ' . $e->getMessage() );
                set_transient( $cooldown_key, '1', 2 * MINUTE_IN_SECONDS );
            }
        }

        // Reproduce the broad, provider-agnostic preference flow used by the
        // original working release. This is deliberately the final attempt:
        // the AI Client can select the first compatibility model actually
        // exposed by any connected provider instead of being locked to a
        // provider whose catalog entry is unavailable or overloaded.
        if ( 'fast' === $profile ) {
            return new WP_Error( 'yatoon_ai_temporarily_unavailable', __( 'AI is busy right now. Please try again in a moment.', 'yatoon-booking-system' ) );
        }

        try {
            $builder = self::get_prompt_builder( $prompt );
            if ( $system_instruction && method_exists( $builder, 'using_system_instruction' ) ) {
                $builder = $builder->using_system_instruction( $system_instruction );
            }
            if ( method_exists( $builder, 'using_max_tokens' ) ) {
                $builder = $builder->using_max_tokens( $max_tokens );
            }
            $compatibility_preferences = $this->get_compatibility_model_preferences();
            if ( method_exists( $builder, 'using_model_preference' ) && ! empty( $compatibility_preferences ) ) {
                $builder = call_user_func_array( array( $builder, 'using_model_preference' ), $compatibility_preferences );
            }
            $timeout_filter = static function ( $timeout ) use ( $timeout_cap ) {
                return min( (float) $timeout, (float) $timeout_cap );
            };
            add_filter( 'http_request_timeout', $timeout_filter, 999 );
            try {
                $result = $builder->generate_text();
            } finally {
                remove_filter( 'http_request_timeout', $timeout_filter, 999 );
            }
            if ( ! is_wp_error( $result ) ) {
                return $result;
            }
            yatoon_safe_log( 'AI compatibility route failed: ' . $result->get_error_message() );
        } catch ( \Throwable $e ) {
            yatoon_safe_log( 'AI compatibility route failed: ' . $e->getMessage() );
        }

        return new WP_Error( 'yatoon_ai_temporarily_unavailable', __( 'AI is busy right now. Please try again in a moment.', 'yatoon-booking-system' ) );
    }

    private function ai_error_status( $error ) {
        $data = is_wp_error( $error ) ? $error->get_error_data() : null;
        return is_array( $data ) && isset( $data['status'] ) ? absint( $data['status'] ) : 500;
    }

    private function get_model_routes() {
        $providers = get_option( 'yatoon_ai_provider_order', array( 'google', 'openai', 'anthropic' ) );
        if ( is_string( $providers ) ) {
            $decoded = json_decode( $providers, true );
            $providers = is_array( $decoded ) ? $decoded : explode( ',', $providers );
        }

        $providers = array_values( array_unique( array_filter( array_map( 'sanitize_key', (array) $providers ) ) ) );
        $allowed_providers = array( 'google', 'openai', 'anthropic' );
        $providers = array_values( array_intersect( $providers, $allowed_providers ) );
        foreach ( $allowed_providers as $provider ) {
            if ( ! in_array( $provider, $providers, true ) ) {
                $providers[] = $provider;
            }
        }

        $defaults = array(
            'google'    => 'gemini-3.7-flash',
            'openai'    => 'gpt-5.6-luna',
            'anthropic' => 'claude-haiku-4-5',
        );
        $supported_models = array(
            'google'    => array( 'gemini-3.7-flash', 'gemini-3.6-flash', 'gemini-3.5-flash', 'gemini-3.5-flash-lite', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-flash', 'gemini-1.5-flash-8b' ),
            'openai'    => array( 'gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-5-mini', 'gpt-5-nano', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o-mini' ),
            'anthropic' => array( 'claude-haiku-4-5', 'claude-sonnet-5', 'claude-opus-5', 'claude-fable-5', 'claude-3-5-haiku-20241022', 'claude-3-haiku-20240307', 'claude-3-5-sonnet-20241022', 'claude-3-7-sonnet-20250219' ),
        );
        // Compatibility models are intentionally attempted immediately after
        // the owner's selected model. Some Connector/provider combinations
        // expose the newest catalog entries before generation is reliable for
        // that account or region, while these older stable IDs still work.
        $fallback_models = array(
            'google'    => array( 'gemini-2.0-flash', 'gemini-1.5-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-flash-8b', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-3.6-flash', 'gemini-3.5-flash-lite', 'gemini-3.5-flash', 'gemini-3.7-flash' ),
            'openai'    => array( 'gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-5.6-sol' ),
            'anthropic' => array( 'claude-3-5-haiku-20241022', 'claude-3-haiku-20240307', 'claude-3-5-sonnet-20241022', 'claude-3-7-sonnet-20250219', 'claude-haiku-4-5', 'claude-sonnet-5', 'claude-opus-5', 'claude-fable-5' ),
        );

        $routes = array();
        foreach ( $providers as $provider ) {
            $model = sanitize_text_field( get_option( 'yatoon_ai_model_' . $provider, $defaults[ $provider ] ) );
            if ( ! in_array( $model, $supported_models[ $provider ], true ) ) {
                $model = $defaults[ $provider ];
            }
            foreach ( array_values( array_unique( array_merge( array( $model ), $fallback_models[ $provider ] ) ) ) as $candidate ) {
                if ( $candidate !== '' ) {
                    $routes[] = array( 'provider' => $provider, 'model' => $candidate );
                }
            }
        }

        return $routes;
    }

    private function get_compatibility_model_preferences() {
        return array(
            'gemini-2.0-flash',
            'gpt-4o-mini',
            'claude-3-5-haiku-20241022',
            'gemini-1.5-flash',
            'gpt-4.1-mini',
            'claude-3-haiku-20240307',
        );
    }

    private function get_model_preferences() {
        return array_values( array_unique( array_map( function ( $route ) {
            return $route['model'];
        }, $this->get_model_routes() ) ) );
    }

    private function trim_ai_text( $text ) {
        $text = wp_strip_all_tags( (string) $text );
        $text = str_replace( array( '**', '__', '*', '###', '##', '#' ), '', $text );
        $text = trim( preg_replace( "/[ \t]+\n/", "\n", $text ) );
        return function_exists( 'mb_substr' ) ? mb_substr( $text, 0, 2000 ) : substr( $text, 0, 2000 );
    }

    private function get_client_ip() {
        $ip = class_exists( 'YATOON_Request_Guard' ) ? YATOON_Request_Guard::client_ip() : '';
        return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '';
    }

    public function screen_ai_input( $text ) {
        $text = trim( wp_strip_all_tags( (string) $text ) );
        $lower = function_exists( 'mb_strtolower' ) ? mb_strtolower( $text ) : strtolower( $text );
        $blocked_terms = array( 'credit card', 'debit card', 'card number', 'cvv', 'password', 'social security', 'account number', '信用卡', '银行卡', '密码', '账号' );
        foreach ( $blocked_terms as $term ) {
            if ( false !== strpos( $lower, $term ) ) {
                return array( 'safe_text' => '', 'redacted' => false, 'blocked' => true );
            }
        }
        if ( preg_match( '/(?:\d[ -]*?){13,19}/', $lower ) ) {
            return array( 'safe_text' => '', 'redacted' => false, 'blocked' => true );
        }

        $health_terms = array( 'allerg', 'pregnan', 'diagnos', 'medication', 'medical condition', 'health condition', '过敏', '怀孕', '疾病', '诊断', '药物' );
        $sentences = preg_split( '/(?<=[.!?。！？\n])\s*/u', $text, -1, PREG_SPLIT_NO_EMPTY );
        $safe      = array();
        $redacted  = false;
        foreach ( (array) $sentences as $sentence ) {
            $sentence_lower = function_exists( 'mb_strtolower' ) ? mb_strtolower( $sentence ) : strtolower( $sentence );
            $sensitive = false;
            foreach ( $health_terms as $term ) {
                if ( false !== strpos( $sentence_lower, $term ) ) {
                    $sensitive = true;
                    break;
                }
            }
            if ( $sensitive ) {
                $redacted = true;
            } else {
                $safe[] = $sentence;
            }
        }
        return array( 'safe_text' => trim( implode( ' ', $safe ) ), 'redacted' => $redacted, 'blocked' => false );
    }

    private function enforce_rate_limit( $scope, $limit = 5, $minutes = 10 ) {
        YATOON_Request_Guard::enforce( $scope, $limit, $minutes );
    }

    public function enforce_global_ai_cap() {
        $limit = max( 0, (int) apply_filters( 'yatoon_ai_provider_hourly_cap', 120 ) );
        if ( $limit > 0 && YATOON_Request_Guard::consume_counter( 'yatoon_ai_provider_hourly', HOUR_IN_SECONDS ) > $limit ) {
            wp_send_json_error( array(
                'message' => __( 'AI is temporarily at capacity for this site. Please try again later or contact the salon directly.', 'yatoon-booking-system' ),
            ) );
        }
    }

    private function client_hash() {
        $ip = $this->get_client_ip();
        return '' === $ip ? '' : hash_hmac( 'sha256', $ip, wp_salt( 'auth' ) );
    }

    public function consume_consent_receipt( $receipt ) {
        if ( '' === (string) $receipt ) {
            return false;
        }
        $key    = 'yatoon_ai_consent_' . hash( 'sha256', (string) $receipt );
        $stored = get_transient( $key );
        delete_transient( $key );
        if ( ! is_array( $stored ) ) {
            return false;
        }
        $expected_text = hash( 'sha256', wp_strip_all_tags( self::consent_text() ) );
        if ( ! hash_equals( $expected_text, (string) ( $stored['text_hash'] ?? '' ) ) ) {
            return false;
        }
        $stored_client = (string) ( $stored['client_hash'] ?? '' );
        $current_client = $this->client_hash();
        if ( '' !== $stored_client && ! hash_equals( $stored_client, $current_client ) ) {
            return false;
        }
        if ( class_exists( 'YATOON_Activity_Log' ) ) {
        YATOON_Activity_Log::record( 'ai_consent_accepted', __( 'Third-party AI consent accepted.', 'yatoon-booking-system' ), array(
                'consented_at' => (string) ( $stored['consented_at'] ?? '' ),
                'text_hash'    => $expected_text,
                'receipt_id'   => substr( hash( 'sha256', (string) $receipt ), 0, 16 ),
            ) );
        }
        return true;
    }
}
