<?php

namespace App\Services\OrderAllocation;

use App\Exceptions\InsufficientStockException;
use App\Exceptions\StockException;
use App\Models\AllocationRule;
use App\Models\Company;
use App\Models\Location;
use App\Models\Order;
use App\Models\OrderAllocation;
use App\Models\OrderItem;
use App\Services\LocationInventoryService;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * Online order allocation heuristics / feature flags -- not a ranked priority engine.
 * Rules (split, warehouse fallback, zones, etc.) are toggles scored simply by stock + distance/priority.
 */
class OrderAllocationService
{
    public function __construct(protected LocationInventoryService $inventory)
    {
    }

    /**
     * Allocate and reserve inventory for an order (or cart lines before items exist).
     *
     * @param  array<int, array{product_id:int, qty:int, order_item_id?:int|null}>  $lines
     * @return array{location_id:int, rule:string, tokens:array<int,string>}
     */
    public function allocateAndReserve(
        Order $order,
        array $lines,
        string $fulfillmentType = 'home_delivery',
        ?string $pincode = null,
        ?float $lat = null,
        ?float $lng = null,
        bool $allowSplit = false,
    ): array {
        return DB::transaction(function () use ($order, $lines, $fulfillmentType, $pincode, $lat, $lng, $allowSplit) {
            $candidates = $this->resolveCandidates($order, $fulfillmentType, $pincode);

            if ($candidates->isEmpty()) {
                // Parity fallback: default store
                $default = Location::defaultStore();
                if (!$default) {
                    throw new StockException('No fulfillment location available.');
                }
                $candidates = collect([$default]);
            }

            $scored = $this->scoreCandidates($candidates, $lines, $lat, $lng);
            $best = $scored->first();

            if (!$best || !$this->canFulfillFully($best['location'], $lines)) {
                // Warehouse fallback (allocation-only; warehouses are not online catalog locations)
                if ($this->warehouseFallbackEnabled()) {
                    $warehouse = Location::centralWarehouse();
                    if ($warehouse && $this->canFulfillFully($warehouse, $lines)) {
                        return $this->commitAllocation(
                            $order,
                            $warehouse,
                            $lines,
                            AllocationRule::TYPE_WAREHOUSE_FALLBACK,
                            $fulfillmentType
                        );
                    }
                }

                if ($allowSplit && $this->splitEnabled()) {
                    return $this->allocateSplit($order, $lines, $scored, $fulfillmentType);
                }

                $productId = $lines[0]['product_id'] ?? 0;
                $product = \App\Models\Product::find($productId);
                $message = 'Some items in this order cannot be fulfilled from available store stock';
                if ($allowSplit && $this->splitEnabled()) {
                    $message .= ' (even when shipping from multiple stores)';
                } elseif ($this->splitEnabled() === false) {
                    $message .= '. No single store has every item — enable Split Orders or consolidate stock';
                }
                throw new InsufficientStockException(
                    $product?->name ?? 'Product',
                    (int) ($lines[0]['qty'] ?? 1),
                    $best ? $this->inventory->getAvailableStock((int) $lines[0]['product_id'], $best['location']->id) : 0,
                    $message
                );
            }

            $rule = $best['rule'] ?? AllocationRule::TYPE_PRIORITY_STORE;

            return $this->commitAllocation($order, $best['location'], $lines, $rule, $fulfillmentType);
        });
    }

    /**
     * Parity mode: always allocate to default store (or preferred/pickup store).
     */
    public function allocateParity(Order $order, array $lines, string $fulfillmentType = 'home_delivery', ?int $storeId = null): array
    {
        $location = null;
        if ($storeId) {
            $location = Location::query()->active()->find($storeId);
        }
        if (!$location && $order->preferred_store_id) {
            $location = Location::query()->active()->find($order->preferred_store_id);
        }
        $location = $location ?? Location::defaultStore();
        if (!$location) {
            app(\App\Services\LocationService::class)->ensureCompanyAndDefaults();
            $location = Location::defaultStore();
        }
        if (!$location) {
            throw new StockException('Default store not configured.');
        }

        return $this->commitAllocation($order, $location, $lines, 'parity_default_store', $fulfillmentType);
    }

    protected function commitAllocation(Order $order, Location $location, array $lines, string $rule, string $fulfillmentType): array
    {
        $tokens = [];
        foreach ($lines as $line) {
            $token = $this->inventory->reserve(
                (int) $line['product_id'],
                (int) $line['qty'],
                (int) $location->id,
                null,
                (int) $order->id
            );
            $tokens[] = $token;
        }

        $allocation = OrderAllocation::create([
            'order_id' => $order->id,
            'location_id' => $location->id,
            'status' => 'allocated',
            'rule_matched' => $rule,
        ]);

        foreach ($lines as $line) {
            $allocation->items()->create([
                'order_item_id' => $line['order_item_id'] ?? null,
                'product_id' => $line['product_id'],
                'quantity' => $line['qty'],
            ]);
        }

        $order->update([
            'fulfillment_location_id' => $location->id,
            'allocation_status' => 'allocated',
            'fulfillment_type' => $fulfillmentType,
            'is_split_parent' => false,
        ]);

        // Stamp order items if they already exist
        foreach ($lines as $line) {
            if (!empty($line['order_item_id'])) {
                OrderItem::whereKey($line['order_item_id'])->update([
                    'allocated_location_id' => $location->id,
                    'reserved_qty' => $line['qty'],
                ]);
            } else {
                OrderItem::where('order_id', $order->id)
                    ->where('product_id', $line['product_id'])
                    ->update([
                        'allocated_location_id' => $location->id,
                        'reserved_qty' => $line['qty'],
                    ]);
            }
        }

        Log::info('Order allocated', [
            'order_id' => $order->id,
            'location_id' => $location->id,
            'rule' => $rule,
        ]);

        return [
            'location_id' => $location->id,
            'rule' => $rule,
            'tokens' => $tokens,
        ];
    }

    protected function resolveCandidates(Order $order, string $fulfillmentType, ?string $pincode): Collection
    {
        if ($fulfillmentType === 'pickup' && $order->preferred_store_id) {
            $store = Location::query()->stores()->allowsPickup()->whereKey($order->preferred_store_id)->first();
            return $store ? collect([$store]) : collect();
        }

        if ($order->preferred_store_id) {
            $preferred = Location::query()->fulfillsOnline()->whereKey($order->preferred_store_id)->first();
            if ($preferred) {
                return collect([$preferred]);
            }
        }

        return Location::query()->fulfillsOnline()->orderBy('priority')->get();
    }

    /**
     * @param  Collection<int, Location>  $candidates
     * @return Collection<int, array{location:Location, score:float, rule:string}>
     */
    protected function scoreCandidates(Collection $candidates, array $lines, ?float $lat, ?float $lng): Collection
    {
        return $candidates->map(function (Location $location) use ($lines, $lat, $lng) {
            $stockScore = 0;
            foreach ($lines as $line) {
                $stockScore += $this->inventory->getAvailableStock((int) $line['product_id'], $location->id);
            }

            $distanceScore = 0;
            $rule = AllocationRule::TYPE_PRIORITY_STORE;
            if ($lat !== null && $lng !== null && $location->latitude && $location->longitude) {
                $distance = $this->haversineKm($lat, $lng, (float) $location->latitude, (float) $location->longitude);
                $distanceScore = max(0, 1000 - $distance);
                $rule = AllocationRule::TYPE_NEAREST_STORE;
            } else {
                $distanceScore = max(0, 500 - (int) $location->priority);
            }

            // Prefer higher stock, then nearer / higher priority
            $score = ($stockScore * 10) + $distanceScore + (1000 - (int) $location->priority);

            if ($stockScore > 0) {
                $rule = $stockScore > 0 && $lat ? AllocationRule::TYPE_NEAREST_STORE : AllocationRule::TYPE_HIGHEST_STOCK;
            }

            return [
                'location' => $location,
                'score' => $score,
                'rule' => $rule,
            ];
        })->sortByDesc('score')->values();
    }

    protected function canFulfillFully(Location $location, array $lines): bool
    {
        foreach ($lines as $line) {
            if ($this->inventory->getAvailableStock((int) $line['product_id'], $location->id) < (int) $line['qty']) {
                return false;
            }
        }

        return true;
    }

    public function isSplitEnabled(): bool
    {
        return $this->splitEnabled();
    }

    /**
     * Activate Split Orders for a company if the rule row exists (live go-live helper).
     */
    public function ensureSplitOrdersEnabled(?int $companyId = null): void
    {
        $companyId = $companyId ?? Company::primary()?->id;
        if (!$companyId) {
            return;
        }

        AllocationRule::query()
            ->where('company_id', $companyId)
            ->where('rule_type', AllocationRule::TYPE_SPLIT_ORDERS)
            ->update(['is_active' => true]);
    }

    /**
     * Whether the cart lines can be fulfilled entirely by one online location.
     *
     * @param  array<int, array{product_id:int, qty:int}>  $lines
     */
    public function canFulfillAsSingleLocation(array $lines, string $fulfillmentType = 'home_delivery', ?string $pincode = null, ?Order $order = null): bool
    {
        $order = $order ?? new Order();
        $candidates = $this->resolveCandidates($order, $fulfillmentType, $pincode);
        if ($candidates->isEmpty()) {
            $default = Location::defaultStore();
            $candidates = $default ? collect([$default]) : collect();
        }

        foreach ($candidates as $location) {
            if ($this->canFulfillFully($location, $lines)) {
                return true;
            }
        }

        if ($this->warehouseFallbackEnabled()) {
            $warehouse = Location::centralWarehouse();
            if ($warehouse && $this->canFulfillFully($warehouse, $lines)) {
                return true;
            }
        }

        return false;
    }

    /**
     * True when Split is enabled and no single location can fulfill the full cart,
     * but every line can be fulfilled by at least one online location.
     *
     * @param  array<int, array{product_id:int, qty:int}>  $lines
     */
    public function wouldRequireSplit(array $lines, string $fulfillmentType = 'home_delivery', ?string $pincode = null, ?Order $order = null): bool
    {
        if (!$this->splitEnabled() || empty($lines)) {
            return false;
        }

        if ($this->canFulfillAsSingleLocation($lines, $fulfillmentType, $pincode, $order)) {
            return false;
        }

        $order = $order ?? new Order();
        $candidates = $this->resolveCandidates($order, $fulfillmentType, $pincode);
        if ($candidates->isEmpty()) {
            $default = Location::defaultStore();
            $candidates = $default ? collect([$default]) : collect();
        }
        if ($this->warehouseFallbackEnabled()) {
            $warehouse = Location::centralWarehouse();
            if ($warehouse && !$candidates->contains('id', $warehouse->id)) {
                $candidates = $candidates->push($warehouse);
            }
        }

        foreach ($lines as $line) {
            $ok = false;
            foreach ($candidates as $location) {
                if ($this->inventory->getAvailableStock((int) $line['product_id'], $location->id) >= (int) $line['qty']) {
                    $ok = true;
                    break;
                }
            }
            if (!$ok) {
                return false;
            }
        }

        return true;
    }


    protected function warehouseFallbackEnabled(): bool
    {
        $company = Company::primary();
        if (!$company) {
            return false;
        }

        return AllocationRule::query()
            ->where('company_id', $company->id)
            ->where('rule_type', AllocationRule::TYPE_WAREHOUSE_FALLBACK)
            ->where('is_active', true)
            ->exists();
    }

    protected function splitEnabled(): bool
    {
        $company = Company::primary();
        if (!$company) {
            return false;
        }

        return AllocationRule::query()
            ->where('company_id', $company->id)
            ->where('rule_type', AllocationRule::TYPE_SPLIT_ORDERS)
            ->where('is_active', true)
            ->exists();
    }

    protected function allocateSplit(Order $order, array $lines, Collection $scored, string $fulfillmentType): array
    {
        // Simplified split: assign each line to the best location that can fulfill that line
        $tokens = [];
        $order->update([
            'allocation_status' => 'split',
            'fulfillment_type' => $fulfillmentType,
            'is_split_parent' => true,
        ]);

        foreach ($lines as $line) {
            $assigned = null;
            foreach ($scored as $candidate) {
                /** @var Location $loc */
                $loc = $candidate['location'];
                if ($this->inventory->getAvailableStock((int) $line['product_id'], $loc->id) >= (int) $line['qty']) {
                    $assigned = $loc;
                    break;
                }
            }
            if (!$assigned) {
                throw new InsufficientStockException(
                    \App\Models\Product::find($line['product_id'])?->name ?? 'Product',
                    (int) $line['qty'],
                    0,
                    'One or more items cannot be fulfilled from any online store, even with a split shipment.'
                );
            }

            $token = $this->inventory->reserve((int) $line['product_id'], (int) $line['qty'], (int) $assigned->id, null, (int) $order->id);
            $tokens[] = $token;

            $allocation = OrderAllocation::create([
                'order_id' => $order->id,
                'location_id' => $assigned->id,
                'status' => 'allocated',
                'rule_matched' => AllocationRule::TYPE_SPLIT_ORDERS,
            ]);
            $allocation->items()->create([
                'order_item_id' => $line['order_item_id'] ?? null,
                'product_id' => $line['product_id'],
                'quantity' => $line['qty'],
            ]);

            OrderItem::where('order_id', $order->id)
                ->where('product_id', $line['product_id'])
                ->update([
                    'allocated_location_id' => $assigned->id,
                    'reserved_qty' => $line['qty'],
                ]);
        }

        // Primary fulfillment location = first allocation
        $first = $order->allocations()->first();
        if ($first) {
            $order->update(['fulfillment_location_id' => $first->location_id]);
        }

        return [
            'location_id' => (int) $order->fulfillment_location_id,
            'rule' => AllocationRule::TYPE_SPLIT_ORDERS,
            'tokens' => $tokens,
        ];
    }

    protected function haversineKm(float $lat1, float $lon1, float $lat2, float $lon2): float
    {
        $earth = 6371;
        $dLat = deg2rad($lat2 - $lat1);
        $dLon = deg2rad($lon2 - $lon1);
        $a = sin($dLat / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) ** 2;

        return 2 * $earth * asin(min(1, sqrt($a)));
    }
}
