<?php
/**
 * Search helper functions
 *
 * Utility functions for price extraction, filtering, sorting, and filter option extraction.
 */

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

/**
 * Extract numeric price from display amount string.
 *
 * Removes currency symbols and non-numeric characters, returning a float.
 *
 * @param string $price_string Price string (e.g., "$19.99" or "USD 19.99").
 * @return float Numeric price value, or 0 if empty/invalid.
 */
function psfa_extract_price($price_string) {
    if (empty($price_string)) {
        return 0;
    }
    // Extract FIRST price only (handles "$22.99 ($11.50 / count)" format)
    // Stop at opening parenthesis or slash to avoid per-unit prices
    $price_string = preg_replace('/\s*[\(\/].*$/', '', $price_string);
    $cleaned = preg_replace('/[^0-9.]/', '', $price_string);
    return (float) $cleaned;
}

/**
 * Get price display string from item (supports OffersV2 and legacy Offers).
 *
 * @param array $item Product item from API.
 * @return string Price display string or empty string.
 */
function psfa_get_item_price_display($item) {
    // OffersV2 path (new)
    if (isset($item['OffersV2']['Listings'][0]['Price']['Money']['DisplayAmount'])) {
        return $item['OffersV2']['Listings'][0]['Price']['Money']['DisplayAmount'];
    }
    // Legacy Offers path
    if (isset($item['Offers']['Listings'][0]['Price']['DisplayAmount'])) {
        return $item['Offers']['Listings'][0]['Price']['DisplayAmount'];
    }
    if (isset($item['Offers']['Listings'][0]['Price']['Amount'])) {
        $amount = $item['Offers']['Listings'][0]['Price']['Amount'] ?? 0;
        $currency = $item['Offers']['Listings'][0]['Price']['Currency'] ?? 'USD';
        return $currency . number_format($amount / 100, 2);
    }
    if (isset($item['Offers']['Summaries'][0]['LowestPrice']['DisplayAmount'])) {
        return $item['Offers']['Summaries'][0]['LowestPrice']['DisplayAmount'];
    }
    return '';
}

/**
 * Get saving basis (original price) display string from item.
 *
 * @param array $item Product item from API.
 * @return string Original price display string or empty string.
 */
function psfa_get_item_saving_basis($item) {
    // OffersV2 path (new)
    if (isset($item['OffersV2']['Listings'][0]['Price']['SavingBasis']['Money']['DisplayAmount'])) {
        return $item['OffersV2']['Listings'][0]['Price']['SavingBasis']['Money']['DisplayAmount'];
    }
    // Legacy Offers path
    if (isset($item['Offers']['Listings'][0]['SavingBasis']['DisplayAmount'])) {
        return $item['Offers']['Listings'][0]['SavingBasis']['DisplayAmount'];
    }
    return '';
}

/**
 * Get condition value from item.
 *
 * @param array $item Product item from API.
 * @return string Condition value or empty string.
 */
function psfa_get_item_condition($item) {
    // OffersV2 path (new)
    if (isset($item['OffersV2']['Listings'][0]['Condition']['Value'])) {
        return $item['OffersV2']['Listings'][0]['Condition']['Value'];
    }
    // Legacy Offers path
    return $item['Offers']['Listings'][0]['Condition']['Value'] ?? '';
}

/**
 * Get availability type from item.
 *
 * @param array $item Product item from API.
 * @return string Availability type or 'Now' as default.
 */
function psfa_get_item_availability_type($item) {
    // OffersV2 path (new)
    if (isset($item['OffersV2']['Listings'][0]['Availability']['Type'])) {
        return $item['OffersV2']['Listings'][0]['Availability']['Type'];
    }
    // Legacy Offers path
    return $item['Offers']['Listings'][0]['Availability']['Type'] ?? 'Now';
}

/**
 * Get availability message from item.
 *
 * @param array $item Product item from API.
 * @return string Availability message or empty string.
 */
function psfa_get_item_availability_message($item) {
    // OffersV2 path (new)
    if (isset($item['OffersV2']['Listings'][0]['Availability']['Message'])) {
        return $item['OffersV2']['Listings'][0]['Availability']['Message'];
    }
    // Legacy Offers path
    return $item['Offers']['Listings'][0]['Availability']['Message'] ?? '';
}

/**
 * Get merchant name from item.
 *
 * @param array $item Product item from API.
 * @return string Merchant name or empty string.
 */
function psfa_get_item_merchant($item) {
    // OffersV2 path (new)
    if (isset($item['OffersV2']['Listings'][0]['MerchantInfo']['Name'])) {
        return $item['OffersV2']['Listings'][0]['MerchantInfo']['Name'];
    }
    // Legacy Offers path
    return $item['Offers']['Listings'][0]['MerchantInfo']['Name'] ?? '';
}

/**
 * Sort items client-side when server-side sorting is unavailable.
 *
 * Used when:
 * - Price sorting is active (API doesn't honor price sorts reliably).
 * - Price filtering + price sorting are active (to avoid API sorting before filtering).
 *
 * @param array $items Array of product items from PA-API.
 * @param string $sort Sort option: 'Price:LowToHigh' or 'Price:HighToLow'.
 * @return array Sorted items array.
 */
function psfa_sort_items($items, $sort) {
    if (empty($sort) || !is_array($items)) {
        return $items;
    }
    
    // Only handle price sorting client-side (other sorts are less critical)
    if ($sort === 'Price:LowToHigh' || $sort === 'Price:HighToLow') {
        usort($items, function ($a, $b) use ($sort) {
            $priceA = psfa_get_item_price_display($a);
            $priceB = psfa_get_item_price_display($b);
            
            // Extract numeric price
            $toNumber = function ($price) {
                if (!is_string($price) || $price === '') {
                    return null;
                }
                // Extract FIRST price only (handles "$22.99 ($11.50 / count)" format)
                // Stop at opening parenthesis or slash to avoid per-unit prices
                $price = preg_replace('/\s*[\(\/].*$/', '', $price);
                $clean = preg_replace('/[^0-9.]/', '', $price);
                if ($clean === '' || !is_numeric($clean)) {
                    return null;
                }
                return (float) $clean;
            };
            
            $numA = $toNumber($priceA);
            $numB = $toNumber($priceB);
            
            // Handle missing prices
            if ($numA === null && $numB === null) {
                return 0;
            }
            if ($numA === null) {
                return 1; // A goes after B
            }
            if ($numB === null) {
                return -1; // B goes after A
            }
            
            if ($sort === 'Price:LowToHigh') {
                return $numA <=> $numB;
            }
            if ($sort === 'Price:HighToLow') {
                return $numB <=> $numA;
            }
            
            return 0;
        });
    }
    
    return $items;
}

/**
 * Filter products by price range (client-side).
 *
 * PA-API v5 SearchItems does not support MinPrice/MaxPrice parameters,
 * so price filtering is performed client-side after receiving results.
 *
 * @param array $items Array of product items from PA-API.
 * @param float $min_price Minimum price (0 to disable).
 * @param float $max_price Maximum price (0 to disable).
 * @return array Filtered items array.
 */
function psfa_filter_products_by_price($items, $min_price, $max_price) {
    // Normalize inputs to floats to avoid integer-only behaviour and
    // potential comparison quirks.
    $min_price = (float) $min_price;
    $max_price = (float) $max_price;

    // If both are zero, no price filter at all.
    if ($min_price == 0.0 && $max_price == 0.0) {
        return $items;
    }

    // If the user accidentally provided min > max, swap them so we still
    // get a sensible inclusive range.
    if ($min_price > 0 && $max_price > 0 && $min_price > $max_price) {
        $tmp       = $min_price;
        $min_price = $max_price;
        $max_price = $tmp;
    }

    $filtered = [];
    foreach ($items as $item) {
        $price_str = psfa_get_item_price_display($item);
        
        // Skip items with no price information
        if (empty($price_str) || trim($price_str) === '') {
            // If minimum price is set, exclude items without prices
            // If only max price is set, we might want to include unpriced items, but for now exclude them
            if ($min_price > 0.0 || $max_price > 0.0) {
                continue;
            }
            // If no price filter, include the item
            $filtered[] = $item;
            continue;
        }
        
        $price = (float) psfa_extract_price($price_str);

        // Ignore items where price extraction failed (resulted in 0) when a minimum is set
        if ($price == 0.0 && $min_price > 0.0) {
            continue;
        }
        
        // If price is 0 and we have a max filter, exclude it (can't determine if it's in range)
        if ($price == 0.0 && $max_price > 0.0) {
            continue;
        }
        
        // Inclusive range checks. Use a small epsilon on the upper bound to
        // avoid float precision edge cases.
        $epsilon  = 0.00001;
        $pass_min = ($min_price == 0.0 || $price >= $min_price);
        $pass_max = ($max_price == 0.0 || $price <= ($max_price + $epsilon));
        
        if ($pass_min && $pass_max) {
            $filtered[] = $item;
        }
    }
    return $filtered;
}

/**
 * Calculate price range from items.
 *
 * Extracts all prices from items and returns the min/max range.
 * Used for building filter options and displaying price ranges.
 *
 * @param array $items Product items from PA-API.
 * @return array{min_price: float, max_price: float} Price range.
 */
function psfa_calculate_price_range($items) {
    $prices = array();
    
    foreach ($items as $item) {
        $price_str = psfa_get_item_price_display($item);
        if (!empty($price_str)) {
            $price = psfa_extract_price($price_str);
            if ($price > 0) {
                $prices[] = $price;
            }
        }
    }
    
    return array(
        'min_price' => !empty($prices) ? min($prices) : 0,
        'max_price' => !empty($prices) ? max($prices) : 0,
    );
}

/**
 * Determine effective sort (skip server-side for certain conditions)
 *
 * Skip server-side sorting for price sorts because:
 * - OAuth/Creators API doesn't honor sortBy reliably
 * - Price filter + price sort needs to sort after filtering
 * - Client-side sorting is more reliable for price ordering
 *
 * @param string $sort
 * @param float $min_price
 * @param float $max_price
 * @return string
 */
function psfa_get_effective_sort($sort, $min_price, $max_price) {
    if (empty($sort)) {
        return '';
    }
    
    $price_sorts = ['Price:LowToHigh', 'Price:HighToLow'];
    $is_price_sort = in_array($sort, $price_sorts, true);
    
    // Always skip price sorts server-side - API doesn't honor them reliably
    // Client-side sorting handles price ordering after filtering
    if ($is_price_sort) {
        return '';
    }
    
    return $sort;
}

/**
 * Check if client-side sorting is needed
 *
 * @param string $brand
 * @param string $sort
 * @param float $min_price
 * @param float $max_price
 * @param string $has_deal
 * @param int $min_saving_percent
 * @return bool
 */
function psfa_needs_client_sort($brand, $sort, $min_price, $max_price, $has_deal = '', $min_saving_percent = 0) {
    if (empty($sort)) {
        return false;
    }
    
    $price_sorts = ['Price:LowToHigh', 'Price:HighToLow'];
    $is_price_sort = in_array($sort, $price_sorts, true);
    
    // Always need client-side sorting for price sorts - API doesn't honor them reliably
    if ($is_price_sort) {
        return true;
    }
    
    // Brand + price sort needs client-side sorting (API may not honor price sorts reliably)
    // Non-price sorts (NewestArrivals, AvgCustomerReviews, Featured) are sent to the API
    // via SortBy even when Brand is set - API may ignore it but cache keys will differ.
    if (!empty($brand) && $is_price_sort) {
        return true;
    }
    
    // Has Deal filter requires client-side sort (V2 client-side filter)
    if (!empty($has_deal)) {
        return true;
    }
    
    return false;
}

/**
 * Check if OAuth auth type is being used
 *
 * @return bool True if OAuth/Creators API is active
 */
function psfa_is_oauth_auth_type() {
    if (!class_exists('PSFA_API_Factory')) {
        return false;
    }
    return PSFA_API_Factory::get_auth_type() === 'oauth';
}

function psfa_extract_filter_options($items, $search_refinements = null) {
    $brands       = [];
    $conditions   = [];
    $price_ranges = [];

    // Prefer dynamic SearchRefinements when available (live mode only).
    if ($search_refinements !== null && is_array($search_refinements)) {
        // Log keys present in SearchRefinements so we can verify in live mode
        // that we're not using the fallback. Only log in debug mode.
        if (defined('WP_DEBUG') && WP_DEBUG && function_exists('error_log')) {
            $refinement_keys = array_keys($search_refinements);
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only runs when WP_DEBUG is enabled
            error_log('PSFA SearchRefinements active: ' . json_encode($refinement_keys));
        }

        // Handle both keyed (e.g. ['Brand' => [...]] ) and indexed formats.
        // We iterate over all refinement groups and pick out Brand / Condition.
        foreach ($search_refinements as $key => $refinement) {
            $is_brand_group     = false;
            $is_condition_group = false;

            if ($key === 'Brand' || $key === 'Brands') {
                $is_brand_group = true;
            } elseif (isset($refinement['DisplayName']) && in_array($refinement['DisplayName'], array( 'Brand', 'Brands' ), true)) {
                $is_brand_group = true;
            }

            if ($key === 'Condition') {
                $is_condition_group = true;
            } elseif (isset($refinement['DisplayName']) && $refinement['DisplayName'] === 'Condition') {
                $is_condition_group = true;
            }

            if ( ! $is_brand_group && ! $is_condition_group ) {
                continue;
            }

            $bins = $refinement['Bins'] ?? array();
            if ( ! is_array($bins) ) {
                continue;
            }

            foreach ($bins as $bin) {
                if ( ! is_array($bin) ) {
                    continue;
                }
                $label = $bin['Label'] ?? ( $bin['DisplayName'] ?? ( $bin['Value'] ?? '' ) );
                $label = is_string($label) ? trim($label) : '';

                if ($label === '') {
                    continue;
                }

                if ($is_brand_group && ! in_array($label, $brands, true)) {
                    $brands[] = $label;
                }

                if ($is_condition_group && ! in_array($label, $conditions, true)) {
                    $conditions[] = $label;
                }
            }
        }
    }

    // Fallback to extracting brands directly from items when refinements are
    // not available or did not yield any brands (mock mode, older responses, etc.).
    if (empty($brands) && is_array($items)) {
        foreach ($items as $item) {
            $brand = $item['ItemInfo']['ByLineInfo']['Brand']['DisplayValue'] ?? '';
            if ($brand !== '' && ! in_array($brand, $brands, true)) {
                $brands[] = $brand;
            }
        }
    }

    // Extract prices from items for min/max price suggestions.
    if (is_array($items)) {
        foreach ($items as $item) {
            $price_str = psfa_get_item_price_display($item);
            if (! empty($price_str)) {
                $price = psfa_extract_price($price_str);
                if ($price > 0) {
                    $price_ranges[] = $price;
                }
            }
        }
    }

    sort($brands);
    sort($conditions);

    return array(
        'brands'       => $brands,
        'conditions'   => $conditions,
        'price_ranges' => array(),
        'min_price'    => ! empty($price_ranges) ? min($price_ranges) : 0,
        'max_price'    => ! empty($price_ranges) ? max($price_ranges) : 0,
    );
}

