<?php

namespace PortalPeople\shippingcosts\services;

use Yii;
use PortalPeople\shippingcosts\models\Shippingcosts;
use backend\models\Accounts;
use backend\models\Cartsbe;
use yii\db\Expression;

/**
 * ShippingCostCalculatorService
 *
 * Handles core shipping cost calculation logic extracted from Shippingcosts model.
 * Manages pallet calculations, haulier lookups, and cost determination.
 */
class ShippingCostCalculatorService
{
    /**
     * Calculate shipping costs for a cart
     *
     * @param string $cartId Cart ID to calculate costs for
     * @param string|null $method If null, returns summary; otherwise returns detailed data
     * @return array|null Shipping costs broken down by haulier
     */
    public static function calculateCartCosts($cartId, $method = null): ?array
    {
        $cart = Cartsbe::findOne(['id' => $cartId]);
        if (!$cart) {
            return null;
        }

        $fullpostcode = $cart->deliveryaddressl ? $cart->deliveryaddressl->postcode : '';
        $area = substr($fullpostcode, -3, 3);
        $postcode = str_replace([$area, ' '], ['', ''], $fullpostcode);

        $out = [];

        // Calculate pallet dimensions from cart items
        $palletConfig = self::calculatePalletConfig($cart);

        // Process each delivery cart line
        foreach ($cart->deliveryCartlines as $cartline) {
            $out = self::processCartLine(
                $cartline,
                $cart,
                $postcode,
                $fullpostcode,
                $palletConfig,
                $out
            );
        }

        // Calculate final totals and discounts
        return self::finalizeCostsOutput($out, $method);
    }

    /**
     * Calculate pallet configuration details from cart items
     *
     * @param Cartsbe $cart Cart to analyze
     * @return array Configuration with pallet size, count, items per pallet, and sleepers
     */
    private static function calculatePalletConfig($cart): array
    {
        $config = [
            'pallet_size' => 0.0,
            'pallet_count' => 0.0,
            'items_per_pallet' => 0.0,
            'sleepers' => 0,
        ];

        foreach ($cart->cartlines as $cartline) {
            if ($cartline->product->ext_data->number_on_pallet > 1) {
                $qty = $cartline->qty * $cartline->product->ext_data->erp_qty;

                $config['pallet_size'] = max(
                    $config['pallet_size'],
                    $cartline->product->ext_data->size_of_pallet
                );

                $config['pallet_count'] = max(
                    $config['pallet_count'],
                    $cartline->product->ext_data->number_of_pallets
                );

                $config['items_per_pallet'] = max(
                    $config['items_per_pallet'],
                    $cartline->product->ext_data->number_on_pallet
                );
            }
        }

        // Calculate sleepers if multi-pallet config
        if ($config['pallet_count'] > 1) {
            foreach ($cart->cartlines as $cartline) {
                if (
                    $cartline->product->ext_data->number_on_pallet > 1 &&
                    $cartline->product->ext_data->pallet_split == 1
                ) {
                    $qty = $cartline->qty * $cartline->product->ext_data->erp_qty;

                    if ($cartline->product->ext_data->number_of_pallets > 1) {
                        $config['sleepers'] += $qty;
                    } else {
                        $config['sleepers'] += $qty / 2;
                    }
                }
            }
        }

        return $config;
    }

    /**
     * Process a single cart line and lookup corresponding costs
     *
     * @param mixed $cartline Cart line item
     * @param mixed $cart Full cart object
     * @param string $postcode Postcode (without area)
     * @param string $fullpostcode Full postcode
     * @param array $palletConfig Pallet configuration
     * @param array $out Accumulator for output
     * @return array Updated output accumulator
     */
    private static function processCartLine($cartline, $cart, $postcode, $fullpostcode, $palletConfig, $out): array
    {
        // Skip non-shippable items
        if (
            $cartline->product->ext_data->number_on_pallet <= 0.00 ||
            in_array($cartline->product_name, ['Delivery', 'Collection'])
        ) {
            return $out;
        }

        $qty = $cartline->qty * $cartline->product->ext_data->erp_qty;
        $haulier = $cartline->product->ext_data->haulierid;

        // Determine pallet configuration and sizing
        $palletInfo = self::calculatePalletInfo($qty, $cartline, $palletConfig);

        // Look up shipping costs for this configuration
        $out = self::findAndAddCosts(
            $out,
            $palletInfo,
            $haulier,
            $postcode,
            $fullpostcode,
            $cart,
            $cartline,
            $qty
        );

        return $out;
    }

    /**
     * Calculate pallet information for a cart line
     *
     * @param float $qty Quantity
     * @param mixed $cartline Cart line object
     * @param array $palletConfig Pallet configuration
     * @return array Pallet info with size and count
     */
    private static function calculatePalletInfo($qty, $cartline, $palletConfig): array
    {
        $pallets = 0;
        $size = 0;

        if (
            $cartline->product->ext_data->size_of_pallet == 1.00 ||
            $cartline->product->ext_data->size_of_pallet == 0
        ) {
            $pallets = $cartline->product->ext_data->number_on_pallet > 0 ?
                ceil($qty / $cartline->product->ext_data->number_on_pallet) : $qty;

            if ($cartline->product->ext_data->number_of_pallets > 1) {
                $pallets = $pallets * $cartline->product->ext_data->number_of_pallets;
            }

            $size = (float) $pallets;
        } elseif ($palletConfig['sleepers'] > 0) {
            $pallets = $palletConfig['items_per_pallet'] > 0 ?
                ceil($palletConfig['sleepers'] / $palletConfig['items_per_pallet']) :
                $palletConfig['sleepers'];

            if ($palletConfig['pallet_count'] > 1) {
                $pallets = $pallets * $palletConfig['pallet_count'];
            }

            $size = $palletConfig['pallet_size'];
        } else {
            $pallets = $cartline->product->ext_data->number_on_pallet > 0 ?
                ceil($qty / $cartline->product->ext_data->number_on_pallet) : $qty;

            if ($cartline->product->ext_data->number_of_pallets > 1) {
                $pallets = $pallets * $cartline->product->ext_data->number_of_pallets;
            }

            $size = $cartline->product->ext_data->size_of_pallet;
        }

        return [
            'size' => $size,
            'count' => (float) number_format($pallets, 2),
        ];
    }

    /**
     * Find and add costs for a pallet configuration
     *
     * @param array $out Output accumulator
     * @param array $palletInfo Pallet sizing info
     * @param mixed $haulier Haulier ID or array of IDs
     * @param string $postcode Postcode
     * @param string $fullpostcode Full postcode
     * @param mixed $cart Cart object
     * @param mixed $cartline Cart line object
     * @param float $qty Quantity
     * @return array Updated accumulator
     */
    private static function findAndAddCosts(
        $out,
        $palletInfo,
        $haulier,
        $postcode,
        $fullpostcode,
        $cart,
        $cartline,
        $qty
    ): array {
        $palletSize = $palletInfo['size'];
        $palletCount = $palletInfo['count'];

        // Build JSON search expressions
        $searchPath = "JSON_SEARCH(prices, 'one', '$palletSize', NULL, '$[*].qty')";
        $priceExpr = new Expression(
            "json_value(prices, JSON_UNQUOTE(REPLACE($searchPath, '.qty', '.price'))) as pricea"
        );
        $qtyExpr = new Expression(
            "json_value(prices, JSON_UNQUOTE(REPLACE($searchPath, '.qty', '.qty'))) as priceqty"
        );

        // Try exact postcode match first
        $costs = self::queryCosts($haulier, $postcode, $cart, $priceExpr, $qtyExpr);

        if (!$costs || !$costs['pricea']) {
            // Fall back to generic/wildcard costs
            $costs = self::queryCosts($haulier, '*', $cart, $priceExpr, $qtyExpr);
        }

        if ($costs) {
            $haulierId = is_array($haulier) ? $haulier[0] : $haulier;
            $calculatedCost = self::calculateCost($costs, $palletCount, $qty);

            if (!isset($out[$haulierId])) {
                $out[$haulierId] = [
                    'data' => [],
                    'pallets' => 0,
                ];
            }

            $out[$haulierId]['data'][] = [
                'productid' => $cartline->productid,
                'product' => $cartline->product_name,
                'split' => $cartline->product->ext_data->pallet_split,
                'fullpostcode' => $fullpostcode,
                'postcode' => $postcode,
                'shipment_type' => $cart->shipment->delivery,
                'qty' => $qty,
                'weight' => $qty * $cartline->product->ext_data->palletweight,
                'pallets' => $palletCount,
                'pallet_size' => $palletSize,
                'pallet_qty' => $cartline->product->ext_data->number_on_pallet,
                'cost-qty' => $costs['priceqty'] ?? null,
                'single-cost' => $calculatedCost,
            ];

            $out[$haulierId]['pallets'] += (float) $palletCount;
        }

        return $out;
    }

    /**
     * Query database for shipping costs
     *
     * @param mixed $haulier Haulier ID(s)
     * @param string $postcode Postcode to match
     * @param mixed $cart Cart object
     * @param Expression $priceExpr Price expression
     * @param Expression $qtyExpr Qty expression
     * @return array|null Cost record or null
     */
    private static function queryCosts($haulier, $postcode, $cart, $priceExpr, $qtyExpr): ?array
    {
        $query = Shippingcosts::find()
            ->select('shipping_costs.*')
            ->addSelect([$priceExpr, $qtyExpr])
            ->where(['in', 'haulierid', $haulier])
            ->andWhere(['status' => 'Active']);

        if ($postcode !== '*') {
            $query->andWhere(['like', 'postcodes', ",$postcode,"]);
        } else {
            $query->andWhere(['=', 'postcodes', '*']);
        }

        // Filter by order type
        $deliveryType = stripos($cart->shipment->delivery, 'Economy') !== false ||
            $cart->shipment->delivery == 'Standard (2-3 Working Days) - FREE' ? 'Eco' : 'Next Day';
        $query->andWhere(['like', 'type', $deliveryType]);

        return $query->asArray()->one();
    }

    /**
     * Calculate final cost based on quantity and pricing
     *
     * @param array $costs Cost record
     * @param float $palletCount Pallet count
     * @param float $qty Quantity
     * @return float Calculated cost
     */
    private static function calculateCost($costs, $palletCount, $qty): float
    {
        $basePrice = $costs['pricea'] ?? 0;
        $pricedQty = $costs['priceqty'] ?? 1;

        if (!$basePrice) {
            return 0.0;
        }

        $cost = ($pricedQty == $palletCount) ? $basePrice : ($basePrice * $palletCount);

        // Apply surcharge for small shipments on specific haulier
        if ($qty < 35 && ($costs['haulierid'] ?? null) == 38910 && $palletCount <= 1) {
            $cost += 5.00;
        }

        return (float) number_format($cost, 2);
    }

    /**
     * Finalize costs output with totals and discounts
     *
     * @param array $out Costs by haulier
     * @param string|null $method Return format (null for summary, 'detailed' for full data)
     * @return array Final output
     */
    private static function finalizeCostsOutput($out, $method = null): array
    {
        $output = [];

        foreach ($out as $haulierId => $data) {
            $totalCost = 0;
            foreach ($data['data'] as $lineData) {
                $totalCost += $lineData['single-cost'];
            }

            // Apply volume discount for specific haulier
            $discount = ($haulierId == 38051 && $data['pallets'] > 4) ? ($totalCost * 0.10) : 0;
            $finalCost = $totalCost - $discount;

            $haulierRecord = Accounts::findOne(['intid' => $haulierId]);
            $haulierName = $haulierRecord ? $haulierRecord->name : 'Unknown';

            $output[$haulierId] = [
                'haulierid' => $haulierId,
                'haulier' => $haulierName,
                'pallets' => $data['pallets'],
                'cost' => $finalCost,
            ];

            // Include detailed data if requested
            if ($method !== null) {
                $output[$haulierId]['data'] = $data['data'];
                $output[$haulierId]['discount'] = $discount;
                $output[$haulierId]['subtotal'] = $totalCost;
            }
        }

        return $output;
    }
}
