<?php
/**
 * Plugin Name: TryOnCloud - AI Virtual TryOn
 * Plugin URI: https://www.tryoncloud.com/integrations/woocommerce
 * Description: Add AI-powered virtual try-on to your WooCommerce store. Customers upload a photo and see how your clothing looks on them in seconds. No app required.
 * Version: 1.5.0
 * Author: TryOnCloud
 * Author URI: https://www.tryoncloud.com
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: tryoncloud-ai-virtual-tryon
 * Domain Path: /languages
 * Requires at least: 5.8
 * Requires PHP: 7.4
 * WC requires at least: 5.0
 */

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

define( 'AIVT_VERSION', '1.5.0' );
define( 'AIVT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'AIVT_PLUGIN_URL', plugin_dir_url( __FILE__ ) );

/**
 * Translations are loaded automatically by WordPress.org since WP 4.6, so no
 * load_plugin_textdomain() call is required for the directory-hosted plugin.
 */

/**
 * Initialize the plugin ONLY after all plugins are loaded.
 * This ensures WooCommerce is available before we use any WC functions.
 */
if ( ! function_exists( 'aivt_init' ) ) {
    function aivt_init() {
        // Bail early if WooCommerce is not active
        if ( ! class_exists( 'WooCommerce' ) ) {
            add_action( 'admin_notices', 'aivt_wc_missing_notice' );
            return;
        }

        // Load admin settings (only in admin context)
        if ( is_admin() ) {
            require_once AIVT_PLUGIN_DIR . 'admin/settings.php';
        }

        // Frontend hooks -- only on non-admin pages
        if ( ! is_admin() ) {
            add_action( 'wp_enqueue_scripts', 'aivt_enqueue_scripts' );

            // Button position: whitelisted to 'before' or 'after' only — fixes issue #6
            $position = get_option( 'aivt_button_position', 'after' );
            $position = in_array( $position, array( 'before', 'after' ), true ) ? $position : 'after';
            $priority = ( $position === 'before' ) ? 25 : 35;
            add_action( 'woocommerce_single_product_summary', 'aivt_add_tryon_button', $priority );

            add_action( 'wp_footer', 'aivt_render_modal' );
        }

        // AJAX proxy handlers — fix #1: API key never sent to browser
        // Authenticated users (logged-in)
        add_action( 'wp_ajax_aivt_tryon', 'aivt_ajax_tryon_proxy' );
        // Non-authenticated users (shop visitors)
        add_action( 'wp_ajax_nopriv_aivt_tryon', 'aivt_ajax_tryon_proxy' );

        // Admin-only test-connection proxy — fix #2: key never in admin DOM
        add_action( 'wp_ajax_aivt_test_connection', 'aivt_ajax_test_connection' );

        // Admin-only product search for settings page
        add_action( 'wp_ajax_aivt_search_products', 'aivt_ajax_search_products' );

        // Limit check — called by frontend before submitting try-on
        add_action( 'wp_ajax_aivt_check_limit', 'aivt_ajax_check_limit' );
        add_action( 'wp_ajax_nopriv_aivt_check_limit', 'aivt_ajax_check_limit' );
    }
    add_action( 'plugins_loaded', 'aivt_init', 20 );
}

/**
 * Admin notice when WooCommerce is not active.
 * Static string — safe, no dynamic values echoed.
 */
if ( ! function_exists( 'aivt_wc_missing_notice' ) ) {
    function aivt_wc_missing_notice() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        // Static string — safe
        echo '<div class="error"><p><strong>' . esc_html__( 'TryOnCloud - AI Virtual TryOn', 'tryoncloud-ai-virtual-tryon' ) . '</strong> ' . esc_html__( 'requires WooCommerce to be installed and active.', 'tryoncloud-ai-virtual-tryon' ) . '</p></div>';
    }
}

/**
 * Safely check if the current product is in an enabled category.
 * Returns true if no categories are configured (show on all), or if the product matches.
 */
if ( ! function_exists( 'aivt_is_product_enabled' ) ) {
    function aivt_is_product_enabled() {
        // Safety: only run on single product pages
        if ( ! function_exists( 'is_product' ) || ! is_product() ) {
            return false;
        }

        $enabled_cats     = get_option( 'aivt_enabled_categories', array() );
        $enabled_products = get_option( 'aivt_enabled_products', array() );

        $has_cat_filter  = ! empty( $enabled_cats ) && is_array( $enabled_cats );
        $has_prod_filter = ! empty( $enabled_products ) && is_array( $enabled_products );

        // If no filters configured at all, show on NO products (require explicit setup)
        if ( ! $has_cat_filter && ! $has_prod_filter ) {
            return false;
        }

        // Get the global product object safely
        global $product;
        if ( ! is_object( $product ) || ! method_exists( $product, 'get_id' ) ) {
            global $post;
            if ( isset( $post->ID ) && function_exists( 'wc_get_product' ) ) {
                $product = wc_get_product( $post->ID );
            }
            if ( ! is_object( $product ) ) {
                return true; // Fail open: show button if we can't determine product
            }
        }

        $current_id = (string) $product->get_id();

        // Check individual product match first (OR logic)
        if ( $has_prod_filter && in_array( $current_id, array_map( 'strval', $enabled_products ), true ) ) {
            return true;
        }

        // Check category match
        if ( $has_cat_filter && method_exists( $product, 'get_category_ids' ) ) {
            $product_cat_ids = $product->get_category_ids();
            foreach ( $product_cat_ids as $cat_id ) {
                if ( in_array( (string) $cat_id, $enabled_cats, true ) ) {
                    return true;
                }
            }
        }

        return false;
    }
}

/**
 * Enqueue frontend scripts and styles ONLY on single product pages.
 * Fix #1: apiKey and apiUrl are NO LONGER passed to JS.
 * Fix #8: debug flag removed from localized config entirely.
 */
if ( ! function_exists( 'aivt_enqueue_scripts' ) ) {
    function aivt_enqueue_scripts() {
        // Guard: only run on single product pages
        if ( ! function_exists( 'is_product' ) || ! is_product() ) {
            return;
        }

        $api_url = get_option( 'aivt_api_url', '' );
        $api_key = get_option( 'aivt_api_key', '' );

        // Don't load assets if not configured
        if ( empty( $api_url ) || empty( $api_key ) ) {
            return;
        }

        // Check category filter
        if ( ! aivt_is_product_enabled() ) {
            return;
        }

        wp_enqueue_style(
            'aivt-style',
            AIVT_PLUGIN_URL . 'assets/css/style.css',
            array(),
            AIVT_VERSION
        );

        wp_enqueue_script(
            'aivt-tryon',
            AIVT_PLUGIN_URL . 'assets/js/tryon.js',
            array(),
            AIVT_VERSION,
            true
        );

        // Modal interaction logic (was inline <script> in modal.php — now enqueued)
        wp_enqueue_script(
            'aivt-modal',
            AIVT_PLUGIN_URL . 'assets/js/modal.js',
            array( 'aivt-tryon' ),
            AIVT_VERSION,
            true
        );

        // HEIC converter (heic2any, MIT licensed) is bundled locally in the plugin.
        // tryon.js lazy-loads it from aivtConfig.heicUrl only when a HEIC file is
        // uploaded — no remote CDN, and the 1.3MB lib never loads unless needed.

        // Build product image URL safely
        $product_image_url = '';
        $product_id        = '';

        global $product;
        if ( is_object( $product ) && method_exists( $product, 'get_image_id' ) ) {
            $image_id = $product->get_image_id();
            if ( $image_id ) {
                $product_image_url = wp_get_attachment_image_url( $image_id, 'full' );
            }
            $product_id = (string) $product->get_id();
        } else {
            global $post;
            if ( isset( $post->ID ) && function_exists( 'wc_get_product' ) ) {
                $p = wc_get_product( $post->ID );
                if ( $p ) {
                    $image_id = $p->get_image_id();
                    if ( $image_id ) {
                        $product_image_url = wp_get_attachment_image_url( $image_id, 'full' );
                    }
                    $product_id = (string) $p->get_id();
                }
            }
        }

        $theme_color  = sanitize_hex_color( get_option( 'aivt_theme_color', '#1a1a1a' ) );
        $btn_radius   = (int) get_option( 'aivt_btn_border_radius', 12 );
        $btn_pad_v    = (int) get_option( 'aivt_btn_padding_v', 12 );
        $btn_pad_h    = (int) get_option( 'aivt_btn_padding_h', 24 );
        $btn_font_sz  = (int) get_option( 'aivt_btn_font_size', 14 );
        $btn_font_wt  = sanitize_text_field( get_option( 'aivt_btn_font_weight', '600' ) );
        $btn_font_fam = sanitize_text_field( get_option( 'aivt_btn_font_family', 'theme' ) );
        $font_map     = array(
            'theme'  => 'inherit',
            'system' => "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
            'serif'  => "Georgia, 'Times New Roman', serif",
            'mono'   => "'Courier New', Courier, monospace",
        );
        $font_family  = isset( $font_map[ $btn_font_fam ] ) ? $font_map[ $btn_font_fam ] : 'inherit';

        // Inject CSS custom properties into <head> via inline style (never browser-cached)
        $custom_css = sprintf(
            '.aivt-tryon-button {
                --aivt-btn-bg: %s;
                --aivt-btn-radius: %dpx;
                --aivt-btn-pad-v: %dpx;
                --aivt-btn-pad-h: %dpx;
                --aivt-btn-font-sz: %dpx;
                --aivt-btn-font-wt: %s;
                --aivt-btn-font-fam: %s;
            }',
            esc_attr( $theme_color ),
            $btn_radius,
            $btn_pad_v,
            $btn_pad_h,
            $btn_font_sz,
            esc_attr( $btn_font_wt ),
            esc_attr( $font_family )
        );
        wp_add_inline_style( 'aivt-style', $custom_css );

        $terms_url   = get_option( 'aivt_terms_url', '' );
        $privacy_url = get_option( 'aivt_privacy_url', '' );

        $max_tryons    = (int) get_option( 'aivt_max_tryons_per_user', 0 );
        $limit_period  = get_option( 'aivt_limit_period', 'daily' );

        wp_localize_script( 'aivt-tryon', 'aivtConfig', array(
            // Fix #1: No apiUrl or apiKey — all requests go through the WP AJAX proxy
            'ajaxUrl'         => admin_url( 'admin-ajax.php' ),
            'nonce'           => wp_create_nonce( 'aivt_tryon' ), // fix #3
            'productImageUrl' => esc_url( $product_image_url ),
            'productId'       => sanitize_text_field( $product_id ),
            'themeColor'      => sanitize_hex_color( $theme_color ),
            // Fix #8: debug flag removed — console logging only via ?aivt_debug=1 URL param
            'termsUrl'        => ! empty( $terms_url ) ? esc_url( $terms_url ) : '',
            'privacyUrl'      => ! empty( $privacy_url ) ? esc_url( $privacy_url ) : '',
            'maxTryons'       => $max_tryons,
            'limitPeriod'     => sanitize_text_field( $limit_period ),
            // Local HEIC converter URL (bundled in the plugin — no remote CDN)
            'heicUrl'         => esc_url( AIVT_PLUGIN_URL . 'assets/js/heic2any.min.js' ),
        ) );
    }
}

/**
 * AJAX proxy handler — fix #1 + #3 + #4
 * Browser sends: nonce + image file + product image URL
 * PHP verifies nonce, validates MIME, calls SaaS with the real API key server-side.
 * API key never touches the browser.
 */
if ( ! function_exists( 'aivt_ajax_tryon_proxy' ) ) {
    function aivt_ajax_tryon_proxy() {
        // Fix #3: Verify nonce. On failure return friendly message.
        if ( ! check_ajax_referer( 'aivt_tryon', 'nonce', false ) ) {
            wp_send_json_error( array(
                'message' => __( 'Session expired. Please refresh the page and try again.', 'tryoncloud-ai-virtual-tryon' ),
                'code'    => 'NONCE_EXPIRED',
            ), 403 );
            return;
        }

        // Hard server-side limit enforcement — checks ALL fingerprint keys
        $max = (int) get_option( 'aivt_max_tryons_per_user', 0 );
        if ( $max > 0 ) {
            $limit_keys = aivt_limit_keys();
            $max_used   = 0;
            foreach ( $limit_keys as $lk ) {
                $u = (int) get_transient( $lk );
                if ( $u > $max_used ) $max_used = $u;
            }
            if ( $max_used >= $max ) {
                $period = get_option( 'aivt_limit_period', 'daily' );
                $when   = array(
                    'daily'   => __( 'tomorrow', 'tryoncloud-ai-virtual-tryon' ),
                    'weekly'  => __( 'next week', 'tryoncloud-ai-virtual-tryon' ),
                    'monthly' => __( 'next month', 'tryoncloud-ai-virtual-tryon' ),
                );
                $reset  = isset( $when[ $period ] ) ? $when[ $period ] : __( 'later', 'tryoncloud-ai-virtual-tryon' );
                wp_send_json_error( array(
                    /* translators: %s: time period (e.g. "tomorrow", "next week") */
                    'message' => sprintf( __( 'You have reached your try-on limit for this period. Please try again %s.', 'tryoncloud-ai-virtual-tryon' ), $reset ),
                    'code'    => 'USER_LIMIT_REACHED',
                ), 429 );
                return;
            }
        }

        $api_url = get_option( 'aivt_api_url', '' );
        $api_key = get_option( 'aivt_api_key', '' );

        if ( empty( $api_url ) || empty( $api_key ) ) {
            wp_send_json_error( array( 'message' => __( 'Virtual try-on is not configured correctly.', 'tryoncloud-ai-virtual-tryon' ) ), 500 );
            return;
        }

        // Fix #4: Server-side MIME validation before forwarding
        if ( empty( $_FILES['user_image'] ) || $_FILES['user_image']['error'] !== UPLOAD_ERR_OK ) {
            wp_send_json_error( array( 'message' => __( 'No image received. Please try again.', 'tryoncloud-ai-virtual-tryon' ) ), 400 );
            return;
        }

        $allowed_mime_types = array( 'image/jpeg', 'image/png', 'image/webp' );

        // Detect MIME — use finfo if available, fall back to mime_content_type, then WP helper
        if ( class_exists( 'finfo' ) ) {
            $finfo         = new finfo( FILEINFO_MIME_TYPE );
            $detected_mime = $finfo->file( $_FILES['user_image']['tmp_name'] );
        } elseif ( function_exists( 'mime_content_type' ) ) {
            $detected_mime = mime_content_type( $_FILES['user_image']['tmp_name'] );
        } else {
            // Last resort: trust the browser-supplied type (lower security but won't crash)
            $detected_mime = $_FILES['user_image']['type'];
        }

        // Normalise: some servers return "image/jpg" instead of "image/jpeg"
        if ( $detected_mime === 'image/jpg' ) {
            $detected_mime = 'image/jpeg';
        }

        if ( ! in_array( $detected_mime, $allowed_mime_types, true ) ) {
            wp_send_json_error( array( 'message' => __( 'Please upload a JPG, PNG or WebP image.', 'tryoncloud-ai-virtual-tryon' ) ), 400 );
            return;
        }

        $product_image_url = isset( $_POST['product_image_url'] ) ? esc_url_raw( $_POST['product_image_url'] ) : '';
        if ( empty( $product_image_url ) ) {
            wp_send_json_error( array( 'message' => __( 'Product image not found. Please refresh the page.', 'tryoncloud-ai-virtual-tryon' ) ), 400 );
            return;
        }

        // Forward to SaaS using WordPress HTTP API (cURL under the hood)
        $boundary  = wp_generate_password( 24, false );
        $body      = '';
        $file_data = file_get_contents( $_FILES['user_image']['tmp_name'] );

        if ( $file_data === false ) {
            wp_send_json_error( array( 'message' => __( 'Something went wrong. Please try again.', 'tryoncloud-ai-virtual-tryon' ) ), 500 );
            return;
        }

        // Build multipart body
        $body .= '--' . $boundary . "\r\n";
        $body .= 'Content-Disposition: form-data; name="user_image"; filename="photo.jpg"' . "\r\n";
        $body .= 'Content-Type: ' . esc_attr( $detected_mime ) . "\r\n\r\n";
        $body .= $file_data . "\r\n";

        $body .= '--' . $boundary . "\r\n";
        $body .= 'Content-Disposition: form-data; name="product_image_url"' . "\r\n\r\n";
        $body .= $product_image_url . "\r\n";

        $body .= '--' . $boundary . '--' . "\r\n";

        // Sign the request (HMAC-SHA256) so the SaaS can verify it originated
        // from a holder of the API key and reject replays. Scheme must match the
        // server: HMAC(apiKey, "apiKey:timestamp:multipart").
        $aivt_timestamp = (string) time();
        $aivt_payload   = $api_key . ':' . $aivt_timestamp . ':multipart';
        $aivt_signature = hash_hmac( 'sha256', $aivt_payload, $api_key );

        $saas_url  = trailingslashit( $api_url ) . 'api/tryon';
        $response  = wp_remote_post( $saas_url, array(
            'timeout' => 120,
            'headers' => array(
                'X-API-KEY'           => $api_key,
                'Content-Type'        => 'multipart/form-data; boundary=' . $boundary,
                'X-TryOnCloud-Source' => 'wordpress',
                'X-Signature'         => $aivt_signature,
                'X-Timestamp'         => $aivt_timestamp,
            ),
            'body'    => $body,
        ) );

        if ( is_wp_error( $response ) ) {
            wp_send_json_error( array( 'message' => __( 'Connection failed. Please check your internet and try again.', 'tryoncloud-ai-virtual-tryon' ) ), 502 );
            return;
        }

        $status_code   = wp_remote_retrieve_response_code( $response );
        $response_body = wp_remote_retrieve_body( $response );
        $content_type  = wp_remote_retrieve_header( $response, 'content-type' );

        // ── Image response (binary PNG/JPEG/WebP returned directly) ──────────
        // The SaaS returns a raw image buffer, not JSON. Convert to base64 data URL.
        if ( $status_code >= 200 && $status_code < 300 && strpos( $content_type, 'image/' ) === 0 ) {
            // Increment ALL fingerprint keys on success
            if ( $max > 0 ) {
                $ttl = aivt_limit_ttl();
                foreach ( $limit_keys as $lk ) {
                    $u = (int) get_transient( $lk );
                    set_transient( $lk, $u + 1, $ttl );
                }
            }
            $mime     = strtok( $content_type, ';' ); // strip charset/boundary suffixes
            $mime     = trim( $mime );
            $b64      = base64_encode( $response_body );
            wp_send_json_success( array(
                'resultUrl' => 'data:' . $mime . ';base64,' . $b64,
            ), 200 );
            return;
        }

        // ── JSON response ────────────────────────────────────────────────────
        $decoded = json_decode( $response_body, true );
        if ( json_last_error() !== JSON_ERROR_NONE ) {
            wp_send_json_error( array( 'message' => 'Something went wrong. Please try again.' ), 500 );
            return;
        }

        if ( $status_code >= 200 && $status_code < 300 ) {
            // Increment ALL fingerprint keys on JSON success path too
            if ( $max > 0 ) {
                $ttl = aivt_limit_ttl();
                foreach ( $limit_keys as $lk ) {
                    $u = (int) get_transient( $lk );
                    set_transient( $lk, $u + 1, $ttl );
                }
            }
            wp_send_json_success( $decoded, $status_code );
        } else {
            // Map known SaaS error codes to user-friendly messages — never expose raw errors
            // Always respond 200 to WordPress/JS layer so response body is always readable;
            // HTTP 4xx/5xx from the SaaS API causes jQuery/fetch to swallow the body on some WP setups
            $code    = isset( $decoded['code'] ) ? $decoded['code'] : '';
            $message = aivt_friendly_error_message( $code, $status_code );
            wp_send_json_error( array( 'message' => $message, 'code' => $code ), 200 );
        }
    }
}

/**
 * Build a stable transient key for per-visitor try-on rate limiting.
 *
 * Combines WP user ID (logged-in) or a hash of IP + User-Agent (guest).
 * Using both IP and UA together means a VPN switch alone won't reset the counter
 * unless the user also changes browser/device — dramatically harder to abuse.
 * The period suffix ensures keys expire naturally without cron cleanup.
 */
/**
 * Returns the period bucket string (used in all transient keys).
 */
if ( ! function_exists( 'aivt_limit_bucket' ) ) {
    function aivt_limit_bucket() {
        $period = get_option( 'aivt_limit_period', 'daily' );
        switch ( $period ) {
            case 'weekly':  return gmdate( 'Y-W' );
            case 'monthly': return gmdate( 'Y-m' );
            default:        return gmdate( 'Y-m-d' );
        }
    }
}

/**
 * Returns the visitor's IP address.
 */
if ( ! function_exists( 'aivt_get_ip' ) ) {
    function aivt_get_ip() {
        $ip = '';
        if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
            $parts = explode( ',', wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
            $ip    = trim( $parts[0] );
        }
        if ( empty( $ip ) && ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
            $ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
        }
        return $ip;
    }
}

/**
 * Returns ALL transient keys that apply to this visitor.
 *
 * Strategy: TWO independent keys for guests —
 *   1. device_id key  — tracks the browser/device across network changes
 *   2. IP key         — tracks the network (catches incognito on same connection)
 *
 * Either key being exhausted blocks the request.
 * Both keys are incremented on success so every path is covered:
 *   - Normal browser:   device_id key exhausted → blocked even on VPN
 *   - Incognito:        IP key exhausted → blocked (same wifi = same IP)
 *   - Incognito + VPN:  device_id key is sess_ random but IP key still tracks the VPN exit IP
 *
 * Logged-in users get a single WP user_id key (fully reliable).
 */
if ( ! function_exists( 'aivt_limit_keys' ) ) {
    function aivt_limit_keys() {
        $bucket  = aivt_limit_bucket();
        $user_id = get_current_user_id();

        if ( $user_id > 0 ) {
            // Logged-in: one reliable key
            return array( 'aivt_lim_' . $bucket . '_u' . $user_id );
        }

        $keys = array();

        // Key 1: device_id based (network-independent, persistent across wifi/mobile switches)
        $device_id = '';
        if ( isset( $_POST['device_id'] ) ) {
            $raw = sanitize_text_field( wp_unslash( $_POST['device_id'] ) );
            // Accept hw_ (canvas fingerprint), dev_ (localStorage UUID), sess_ (session fallback)
            if ( preg_match( '/^(hw|dev|sess)_[a-z0-9]{8,}$/i', $raw ) ) {
                $device_id = $raw;
            }
        }
        $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : '';
        if ( ! empty( $device_id ) ) {
            $keys[] = 'aivt_lim_' . $bucket . '_d' . substr( hash( 'sha256', $device_id . '|' . $ua ), 0, 16 );
        }

        // Key 2: IP based (catches incognito on same network connection)
        $ip = aivt_get_ip();
        if ( ! empty( $ip ) ) {
            $keys[] = 'aivt_lim_' . $bucket . '_i' . substr( hash( 'sha256', $ip ), 0, 16 );
        }

        // Fallback: if somehow both are empty, use UA only
        if ( empty( $keys ) ) {
            $keys[] = 'aivt_lim_' . $bucket . '_g' . substr( hash( 'sha256', $ua ), 0, 16 );
        }

        return $keys;
    }
}

// Keep aivt_limit_key() as a single-key alias for backward compat (returns first key only)
if ( ! function_exists( 'aivt_limit_key' ) ) {
    function aivt_limit_key() {
        $keys = aivt_limit_keys();
        return $keys[0];
    }
}

/**
 * Return seconds until the current period expires (used as transient TTL).
 */
if ( ! function_exists( 'aivt_limit_ttl' ) ) {
    function aivt_limit_ttl() {
        $period = get_option( 'aivt_limit_period', 'daily' );
        $now    = time();
        switch ( $period ) {
            case 'weekly':
                // Seconds until next Monday 00:00 UTC
                $next = strtotime( 'next Monday midnight', $now );
                break;
            case 'monthly':
                // Seconds until 1st of next month 00:00 UTC
                $next = strtotime( 'first day of next month midnight', $now );
                break;
            default: // daily
                $next = strtotime( 'tomorrow midnight', $now );
        }
        return max( 60, $next - $now );
    }
}

/**
 * AJAX: check current visitor's remaining try-ons for this period.
 * Returns { allowed: bool, remaining: int|null, limit: int }
 * Called by frontend before submitting the form (soft pre-check).
 * The hard enforcement happens inside aivt_ajax_tryon_proxy.
 */
if ( ! function_exists( 'aivt_ajax_check_limit' ) ) {
    function aivt_ajax_check_limit() {
        if ( ! check_ajax_referer( 'aivt_tryon', 'nonce', false ) ) {
            wp_send_json_error( array( 'message' => __( 'Session expired. Please refresh the page.', 'tryoncloud-ai-virtual-tryon' ) ), 403 );
            return;
        }

        $max = (int) get_option( 'aivt_max_tryons_per_user', 0 );
        if ( $max <= 0 ) {
            wp_send_json_success( array( 'allowed' => true, 'remaining' => null, 'limit' => 0 ) );
            return;
        }

        // Check ALL keys — use the highest usage count (most restrictive)
        $keys     = aivt_limit_keys();
        $max_used = 0;
        foreach ( $keys as $key ) {
            $used = (int) get_transient( $key );
            if ( $used > $max_used ) {
                $max_used = $used;
            }
        }
        wp_send_json_success( array(
            'allowed'   => $max_used < $max,
            'remaining' => max( 0, $max - $max_used ),
            'limit'     => $max,
        ) );
    }
}

/**
 * Map SaaS error codes to user-friendly messages.
 */
if ( ! function_exists( 'aivt_friendly_error_message' ) ) {
    function aivt_friendly_error_message( $code, $status ) {
        $map = array(
            'USER_LIMIT_REACHED'     => __( 'You have reached your try-on limit for this period.', 'tryoncloud-ai-virtual-tryon' ),
            'MONTHLY_QUOTA_EXCEEDED' => __( 'Monthly limit reached. Please try later.', 'tryoncloud-ai-virtual-tryon' ),
            'DAILY_LIMIT'            => __( 'Daily limit reached. Please try tomorrow.', 'tryoncloud-ai-virtual-tryon' ),
            'RATE_LIMIT_KEY'         => __( 'Too many requests. Please wait a moment.', 'tryoncloud-ai-virtual-tryon' ),
            'RATE_LIMIT_IP'          => __( 'Too many requests. Please wait a moment.', 'tryoncloud-ai-virtual-tryon' ),
            'INVALID_USER_IMAGE'     => __( 'Please try with a clearer photo showing your face and upper body.', 'tryoncloud-ai-virtual-tryon' ),
            'GENERATION_FAILED'      => __( 'Please try with a clearer photo showing your face and upper body.', 'tryoncloud-ai-virtual-tryon' ),
            'CONTENT_VIOLATION'      => __( 'Please upload a photo of yourself wearing clothes. Explicit images are not allowed.', 'tryoncloud-ai-virtual-tryon' ),
            'CONTENT_FILTERED'       => __( 'Please upload a photo of yourself wearing clothes. Explicit images are not allowed.', 'tryoncloud-ai-virtual-tryon' ),
            'AI_SERVICE_ERROR'       => __( 'AI service is temporarily busy. Please try again in a moment.', 'tryoncloud-ai-virtual-tryon' ),
            'SERVICE_PAUSED'         => __( 'Service temporarily under maintenance.', 'tryoncloud-ai-virtual-tryon' ),
            'UPLOAD_EXPIRED'         => __( 'Upload session expired. Please try again.', 'tryoncloud-ai-virtual-tryon' ),
        );
        if ( isset( $map[ $code ] ) ) {
            return $map[ $code ];
        }
        if ( $status >= 500 ) {
            return __( 'Service temporarily unavailable. Please try again.', 'tryoncloud-ai-virtual-tryon' );
        }
        return __( 'Something went wrong. Please try again.', 'tryoncloud-ai-virtual-tryon' );
    }
}

/**
 * Admin-only AJAX handler for test-connection — fix #2
 * Key is read server-side, never echoed into the DOM.
 */
if ( ! function_exists( 'aivt_ajax_test_connection' ) ) {
    function aivt_ajax_test_connection() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => 'Unauthorized.' ), 403 );
            return;
        }

        check_ajax_referer( 'aivt_test_connection', 'nonce' );

        $api_url = get_option( 'aivt_api_url', '' );
        $api_key = get_option( 'aivt_api_key', '' );

        if ( empty( $api_url ) || empty( $api_key ) ) {
            wp_send_json_error( array( 'message' => __( 'Please save your API URL and API Key first.', 'tryoncloud-ai-virtual-tryon' ) ) );
            return;
        }

        $health_url = trailingslashit( $api_url ) . 'api/health';
        $response   = wp_remote_get( $health_url, array(
            'timeout' => 15,
            'headers' => array( 'X-API-KEY' => $api_key ),
        ) );

        if ( is_wp_error( $response ) ) {
            wp_send_json_error( array( 'message' => __( 'Connection failed. Check the API URL.', 'tryoncloud-ai-virtual-tryon' ) ) );
            return;
        }

        $status = wp_remote_retrieve_response_code( $response );
        $body   = json_decode( wp_remote_retrieve_body( $response ), true );

        if ( $status === 200 && isset( $body['status'] ) && $body['status'] === 'ok' ) {
            wp_send_json_success( array(
                'message' => __( 'Connection successful.', 'tryoncloud-ai-virtual-tryon' ),
                'plan'    => isset( $body['plan'] ) ? sanitize_text_field( $body['plan'] ) : __( 'Unknown', 'tryoncloud-ai-virtual-tryon' ),
                'usage'   => isset( $body['usageCount'] ) ? intval( $body['usageCount'] ) : 0,
                'limit'   => isset( $body['monthlyLimit'] ) ? intval( $body['monthlyLimit'] ) : 0,
            ) );
        } elseif ( $status === 401 ) {
            wp_send_json_error( array( 'message' => __( 'Invalid API key.', 'tryoncloud-ai-virtual-tryon' ) ) );
        } else {
            /* translators: %d: HTTP status code */
            wp_send_json_error( array( 'message' => sprintf( __( 'Unexpected response (HTTP %d).', 'tryoncloud-ai-virtual-tryon' ), intval( $status ) ) ) );
        }
    }
}

/**
 * Admin-only AJAX: search WooCommerce products by title.
 * Used by the Individual Products selector in settings.
 */
if ( ! function_exists( 'aivt_ajax_search_products' ) ) {
    function aivt_ajax_search_products() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => 'Unauthorized.' ), 403 );
            return;
        }

        check_ajax_referer( 'aivt_search_products', 'nonce' );

        $search = isset( $_POST['q'] ) ? sanitize_text_field( wp_unslash( $_POST['q'] ) ) : '';
        if ( strlen( $search ) < 2 ) {
            wp_send_json_success( array() );
            return;
        }

        // Direct title LIKE search — more reliable than get_posts 's' param
        global $wpdb;
        $like        = '%' . $wpdb->esc_like( $search ) . '%';
        $product_ids = $wpdb->get_col( $wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts}
             WHERE post_type = 'product'
               AND post_status = 'publish'
               AND post_title LIKE %s
             ORDER BY post_title ASC
             LIMIT 15",
            $like
        ) );

        $results = array();
        foreach ( $product_ids as $id ) {
            if ( ! function_exists( 'wc_get_product' ) ) {
                break;
            }
            $p = wc_get_product( $id );
            if ( ! $p ) {
                continue;
            }
            $thumb = get_the_post_thumbnail_url( $id, array( 48, 48 ) );
            // Use wc_price on the plain price number to avoid sale-price HTML verbosity
            $price_raw = function_exists( 'wc_price' ) ? wc_price( (float) $p->get_price() ) : '';
            $results[] = array(
                'id'        => $id,
                'title'     => $p->get_name(),
                'thumbnail' => $thumb ? $thumb : '',
                'price'     => html_entity_decode( wp_strip_all_tags( $price_raw ), ENT_HTML5, 'UTF-8' ),
            );
        }

        wp_send_json_success( $results );
    }
}

/**
 * Add the Try On button to the product page.
 */
if ( ! function_exists( 'aivt_add_tryon_button' ) ) {
    function aivt_add_tryon_button() {
        $api_url = get_option( 'aivt_api_url', '' );
        $api_key = get_option( 'aivt_api_key', '' );

        if ( empty( $api_url ) || empty( $api_key ) ) {
            return;
        }

        if ( ! aivt_is_product_enabled() ) {
            return;
        }

        $button_text = get_option( 'aivt_button_text', 'Try On' );

        echo '<button type="button" id="aivt-tryon-btn" class="aivt-tryon-button">';
        echo '<span class="aivt-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2z"/><path d="M19 15l.75 2.25L22 18l-2.25.75L19 21l-.75-2.25L16 18l2.25-.75L19 15z"/></svg></span>';
        echo '<span>' . esc_html( $button_text ) . '</span>';
        echo '</button>';
    }
}

/**
 * Render the modal HTML in the footer.
 */
if ( ! function_exists( 'aivt_render_modal' ) ) {
    function aivt_render_modal() {
        if ( ! function_exists( 'is_product' ) || ! is_product() ) {
            return;
        }

        if ( ! aivt_is_product_enabled() ) {
            return;
        }

        $api_url = get_option( 'aivt_api_url', '' );
        $api_key = get_option( 'aivt_api_key', '' );

        if ( empty( $api_url ) || empty( $api_key ) ) {
            return;
        }

        $modal_file = AIVT_PLUGIN_DIR . 'public/modal.php';
        if ( file_exists( $modal_file ) ) {
            require_once $modal_file;
        }
    }
}

/**
 * Add Settings link on the Plugins list page.
 */
if ( ! function_exists( 'aivt_plugin_action_links' ) ) {
    function aivt_plugin_action_links( $links ) {
        $settings_link = '<a href="' . admin_url( 'admin.php?page=aivt-settings' ) . '">' . __( 'Settings', 'tryoncloud-ai-virtual-tryon' ) . '</a>';
        array_unshift( $links, $settings_link );
        return $links;
    }
    add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'aivt_plugin_action_links' );
}

/**
 * Plugin activation hook.
 */
if ( ! function_exists( 'aivt_activate' ) ) {
    function aivt_activate() {
        add_option( 'aivt_api_url', '' );
        add_option( 'aivt_api_key', '' );
        add_option( 'aivt_button_text', 'Try On' );
        add_option( 'aivt_button_position', 'after' );
        add_option( 'aivt_theme_color', '#1a1a1a' );
        add_option( 'aivt_btn_border_radius', 8 );
        add_option( 'aivt_btn_padding_v', 11 );
        add_option( 'aivt_btn_padding_h', 22 );
        add_option( 'aivt_btn_font_size', 16 );
        add_option( 'aivt_btn_font_weight', '600' );
        add_option( 'aivt_btn_font_family', 'theme' );
        add_option( 'aivt_enabled_categories', array() );
        add_option( 'aivt_enabled_products', array() );
        add_option( 'aivt_max_tryons_per_user', 0 );
        add_option( 'aivt_limit_period', 'daily' );
    }
    register_activation_hook( __FILE__, 'aivt_activate' );
}

// Uninstall cleanup is handled by uninstall.php (WordPress-recommended approach).
// That file runs when the plugin is deleted from the Plugins page.
