<?php

namespace App\Services;

use App\Models\CashDrawerTransaction;
use App\Models\Customer;
use App\Models\DiningTable;
use App\Models\KitchenTicket;
use App\Models\LoyaltyTransaction;
use App\Models\Order;
use App\Models\PaymentMethod;
use App\Models\Product;
use App\Models\Promotion;
use App\Models\Shift;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class OrderService
{
    private const LOYALTY_REDEMPTION_RATE = 1;
    private const LOYALTY_EARN_SPEND_STEP = 100;
    private const LOYALTY_EARN_POINTS_PER_STEP = 1;

    public function __construct(
        private readonly InventoryService $inventoryService,
        private readonly AuditLogService $auditLogService,
    ) {
    }

    public function create(User $user, int $companyId, int $branchId, array $data): Order
    {
        return DB::transaction(function () use ($user, $companyId, $branchId, $data) {
            if (! empty($data['device_id']) && ! empty($data['local_id'])) {
                $existing = Order::query()
                    ->where('device_id', $data['device_id'])
                    ->where('local_id', $data['local_id'])
                    ->first();

                if ($existing !== null) {
                    return $existing->load(['items', 'payments']);
                }
            }

            $shift = $this->resolveShift($user, $branchId, $data['shift_id'] ?? null);
            $customer = ! empty($data['customer_id'])
                ? Customer::query()->where('company_id', $companyId)->findOrFail($data['customer_id'])
                : null;
            $itemSnapshots = [];
            $subtotal = 0.0;
            $taxAmount = 0.0;
            $lineDiscounts = 0.0;

            foreach ($data['items'] as $line) {
                $product = Product::with('taxRate')->findOrFail($line['product_id']);
                $quantity = (float) $line['quantity'];
                $unitPrice = (float) ($line['unit_price'] ?? $product->selling_price);

                if ($product->min_selling_price !== null && $unitPrice < (float) $product->min_selling_price) {
                    throw ValidationException::withMessages([
                        'items' => ["{$product->name} cannot be sold below the minimum price."],
                    ]);
                }

                $gross = round($quantity * $unitPrice, 4);
                $discount = round((float) ($line['discount_amount'] ?? 0), 4);
                $lineTaxRate = (float) ($line['tax_rate'] ?? $product->taxRate?->rate ?? 0);
                $taxable = $gross - $discount;
                $lineTax = round($taxable * ($lineTaxRate / 100), 4);
                $total = round($taxable + $lineTax, 4);

                $subtotal += $gross;
                $taxAmount += $lineTax;
                $lineDiscounts += $discount;

                $itemSnapshots[] = [
                    'product' => $product,
                    'variant_id' => $line['variant_id'] ?? null,
                    'batch_id' => $line['batch_id'] ?? null,
                    'quantity' => $quantity,
                    'unit_price' => $unitPrice,
                    'cost_price' => (float) $product->cost_price,
                    'discount_amount' => $discount,
                    'tax_rate' => $lineTaxRate,
                    'tax_amount' => $lineTax,
                    'total_amount' => $total,
                    'notes' => $line['notes'] ?? null,
                ];
            }

            $promotionDiscount = $this->resolvePromotionDiscount(
                $companyId,
                $subtotal - $lineDiscounts,
                $data,
                collect($itemSnapshots),
                $customer,
            );
            $manualOrderDiscount = $this->calculateOrderDiscount($subtotal, $data);
            $redeemedPoints = (int) ($data['loyalty_points_to_redeem'] ?? 0);
            $loyaltyDiscount = $this->resolveLoyaltyDiscount($customer, $redeemedPoints, $subtotal - $lineDiscounts - $manualOrderDiscount - $promotionDiscount);
            $orderDiscount = $manualOrderDiscount + $promotionDiscount + $loyaltyDiscount;
            $totalAmount = round($subtotal - $lineDiscounts - $orderDiscount + $taxAmount, 4);

            $order = Order::create([
                'company_id' => $companyId,
                'branch_id' => $branchId,
                'shift_id' => $shift->id,
                'user_id' => $user->id,
                'customer_id' => $customer?->id,
                'order_number' => $data['order_number'] ?? $this->generateOrderNumber(),
                'type' => $data['type'] ?? 'sale',
                'status' => 'pending',
                'currency_code' => $data['currency_code'] ?? 'KES',
                'exchange_rate' => $data['exchange_rate'] ?? 1,
                'subtotal' => $subtotal,
                'discount_amount' => $lineDiscounts + $orderDiscount,
                'discount_type' => $data['discount_type'] ?? null,
                'promotion_id' => $data['promotion_id'] ?? null,
                'tax_amount' => $taxAmount,
                'total_amount' => $totalAmount,
                'loyalty_points_earned' => 0,
                'loyalty_points_redeemed' => $redeemedPoints,
                'notes' => $data['notes'] ?? null,
                'device_id' => $data['device_id'] ?? null,
                'local_id' => $data['local_id'] ?? null,
            ]);

            foreach ($itemSnapshots as $snapshot) {
                $item = $order->items()->create([
                    'product_id' => $snapshot['product']->id,
                    'variant_id' => $snapshot['variant_id'],
                    'batch_id' => $snapshot['batch_id'],
                    'name' => $snapshot['product']->name,
                    'sku' => $snapshot['product']->sku,
                    'quantity' => $snapshot['quantity'],
                    'unit_price' => $snapshot['unit_price'],
                    'cost_price' => $snapshot['cost_price'],
                    'discount_amount' => $snapshot['discount_amount'],
                    'tax_rate' => $snapshot['tax_rate'],
                    'tax_amount' => $snapshot['tax_amount'],
                    'total_amount' => $snapshot['total_amount'],
                    'notes' => $snapshot['notes'],
                ]);

                if ($snapshot['product']->track_stock) {
                    $this->inventoryService->adjust(
                        user: $user,
                        branchId: $branchId,
                        product: $snapshot['product'],
                        variantId: $snapshot['variant_id'],
                        quantityDelta: -1 * $snapshot['quantity'],
                        type: 'sale',
                        unitCost: $snapshot['cost_price'],
                        batchId: $snapshot['batch_id'],
                        referenceType: Order::class,
                        referenceId: $order->id,
                        notes: "Sale item {$item->id}",
                        deviceId: $order->device_id,
                    );
                }
            }

            $shift->increment('total_orders');
            $shift->increment('total_sales', $totalAmount);
            $shift->activities()->create([
                'user_id' => $user->id,
                'activity_type' => 'sale',
                'description' => "Order {$order->order_number} created",
                'amount' => $totalAmount,
                'reference_type' => Order::class,
                'reference_id' => $order->id,
            ]);

            if (! empty($data['payments'])) {
                $this->recordPayments($user, $order, $shift, $data['payments']);
            } else {
                $order->status = 'processing';
                $order->save();
            }

            if (! empty($data['table_id'])) {
                $this->handleTableOccupation($data['table_id'], $order);
            }

            $this->generateKitchenTickets($order, $data);

            $this->finalizeCustomerBenefits($user, $order, $customer, $redeemedPoints);

            $this->auditLogService->log($user, 'order.created', $order, [], $order->toArray(), $order->device_id);

            return $order->load(['items', 'payments.paymentMethod', 'customer']);
        });
    }

    public function addPayments(User $user, int $branchId, Order $order, array $payments): Order
    {
        return DB::transaction(function () use ($user, $branchId, $order, $payments) {
            $shift = $this->resolveShift($user, $branchId, $order->shift_id);
            $this->recordPayments($user, $order, $shift, $payments);
            $this->auditLogService->log($user, 'order.payments_added', $order, [], $order->toArray(), $order->device_id);

            return $order->load(['items', 'payments.paymentMethod']);
        });
    }

    private function recordPayments(User $user, Order $order, Shift $shift, array $payments): void
    {
        $paidBefore = (float) $order->amount_paid;
        $paidNow = $paidBefore;

        foreach ($payments as $payload) {
            $method = PaymentMethod::query()
                ->where('company_id', $order->company_id)
                ->where('is_active', true)
                ->findOrFail($payload['payment_method_id']);

            $amount = (float) $payload['amount'];
            $paidNow += $amount;

            $payment = $order->payments()->create([
                'payment_method_id' => $method->id,
                'amount' => $amount,
                'currency_code' => $payload['currency_code'] ?? $order->currency_code,
                'exchange_rate' => $payload['exchange_rate'] ?? $order->exchange_rate,
                'amount_kes' => round($amount * (float) ($payload['exchange_rate'] ?? $order->exchange_rate), 4),
                'status' => $payload['status'] ?? 'completed',
                'reference_number' => $payload['reference_number'] ?? null,
                'notes' => $payload['notes'] ?? null,
                'processed_at' => now(),
            ]);

            if ($method->type === 'cash') {
                CashDrawerTransaction::create([
                    'shift_id' => $shift->id,
                    'cash_register_id' => $shift->cash_register_id,
                    'user_id' => $user->id,
                    'type' => 'sale',
                    'amount' => $amount,
                    'running_balance' => ($shift->opening_float ?? 0) + $paidNow,
                    'notes' => "Cash payment for order {$order->order_number}",
                    'reference_type' => $payment::class,
                    'reference_id' => $payment->id,
                ]);
            }
        }

        $change = max($paidNow - (float) $order->total_amount, 0);
        $status = $paidNow >= (float) $order->total_amount ? 'completed' : 'processing';

        $order->forceFill([
            'amount_paid' => $paidNow,
            'change_amount' => $change,
            'status' => $status,
        ])->save();
    }

    private function calculateOrderDiscount(float $subtotal, array $data): float
    {
        $discountType = $data['discount_type'] ?? null;
        $discountValue = (float) ($data['discount_value'] ?? 0);

        return match ($discountType) {
            'percentage' => round($subtotal * ($discountValue / 100), 4),
            'fixed' => round($discountValue, 4),
            default => 0.0,
        };
    }

    private function resolvePromotionDiscount(
        int $companyId,
        float $discountableSubtotal,
        array $data,
        Collection $itemSnapshots,
        ?Customer $customer
    ): float {
        if (empty($data['promotion_id'])) {
            return 0.0;
        }

        $promotion = Promotion::query()
            ->where('company_id', $companyId)
            ->findOrFail($data['promotion_id']);

        if (! $promotion->is_active || $promotion->starts_at->isFuture() || ($promotion->ends_at !== null && $promotion->ends_at->isPast())) {
            throw ValidationException::withMessages([
                'promotion_id' => ['The selected promotion is not active.'],
            ]);
        }

        if ($promotion->usage_limit !== null && $promotion->usage_count >= $promotion->usage_limit) {
            throw ValidationException::withMessages([
                'promotion_id' => ['This promotion has reached its usage limit.'],
            ]);
        }

        if ($promotion->per_customer_limit !== null && $customer !== null) {
            $usageCount = Order::query()
                ->where('promotion_id', $promotion->id)
                ->where('customer_id', $customer->id)
                ->count();

            if ($usageCount >= $promotion->per_customer_limit) {
                throw ValidationException::withMessages([
                    'promotion_id' => ['This customer has reached the promotion usage limit.'],
                ]);
            }
        }

        if ($promotion->min_order_amount !== null && $discountableSubtotal < (float) $promotion->min_order_amount) {
            throw ValidationException::withMessages([
                'promotion_id' => ['The order does not meet the promotion minimum amount.'],
            ]);
        }

        if (! $this->promotionAppliesToItems($promotion, $itemSnapshots)) {
            throw ValidationException::withMessages([
                'promotion_id' => ['The selected promotion does not apply to these items.'],
            ]);
        }

        $discount = match ($promotion->type) {
            'percentage' => round($discountableSubtotal * ((float) $promotion->discount_value / 100), 4),
            'fixed_amount' => round((float) $promotion->discount_value, 4),
            default => throw ValidationException::withMessages([
                'promotion_id' => ['The selected promotion type is not supported at checkout yet.'],
            ]),
        };

        if ($promotion->max_discount_amount !== null) {
            $discount = min($discount, (float) $promotion->max_discount_amount);
        }

        return min($discount, max($discountableSubtotal, 0));
    }

    private function promotionAppliesToItems(Promotion $promotion, Collection $itemSnapshots): bool
    {
        return match ($promotion->applies_to) {
            'all' => true,
            'products' => $itemSnapshots->contains(function (array $snapshot) use ($promotion): bool {
                return $promotion->products()->whereKey($snapshot['product']->id)->exists();
            }),
            'categories' => $itemSnapshots->contains(function (array $snapshot) use ($promotion): bool {
                $categoryId = $snapshot['product']->category_id;

                if ($categoryId === null) {
                    return false;
                }

                return DB::table('promotion_products')
                    ->where('promotion_id', $promotion->id)
                    ->where('category_id', $categoryId)
                    ->exists();
            }),
            default => false,
        };
    }

    private function resolveLoyaltyDiscount(?Customer $customer, int $pointsToRedeem, float $discountableSubtotal): float
    {
        if ($pointsToRedeem === 0) {
            return 0.0;
        }

        if ($customer === null) {
            throw ValidationException::withMessages([
                'loyalty_points_to_redeem' => ['A customer is required to redeem loyalty points.'],
            ]);
        }

        if ($pointsToRedeem > $customer->loyalty_points) {
            throw ValidationException::withMessages([
                'loyalty_points_to_redeem' => ['The customer does not have enough loyalty points.'],
            ]);
        }

        $maxRedeemable = (int) floor(max($discountableSubtotal, 0) / self::LOYALTY_REDEMPTION_RATE);

        if ($pointsToRedeem > $maxRedeemable) {
            throw ValidationException::withMessages([
                'loyalty_points_to_redeem' => ['The requested loyalty points exceed the discountable order amount.'],
            ]);
        }

        return round($pointsToRedeem * self::LOYALTY_REDEMPTION_RATE, 4);
    }

    private function finalizeCustomerBenefits(User $user, Order $order, ?Customer $customer, int $redeemedPoints): void
    {
        if ($order->promotion_id !== null) {
            Promotion::query()->whereKey($order->promotion_id)->increment('usage_count');
        }

        if ($customer === null) {
            return;
        }

        $customer->refresh();

        if ($redeemedPoints > 0) {
            $balanceAfter = max($customer->loyalty_points - $redeemedPoints, 0);
            $customer->forceFill([
                'loyalty_points' => $balanceAfter,
            ])->save();

            LoyaltyTransaction::create([
                'customer_id' => $customer->id,
                'user_id' => $user->id,
                'type' => 'redeemed',
                'points' => -1 * $redeemedPoints,
                'balance_after' => $balanceAfter,
                'reference_type' => Order::class,
                'reference_id' => $order->id,
                'notes' => "Redeemed on order {$order->order_number}",
            ]);

            $customer->refresh();
        }

        $earnedPoints = (int) floor(((float) $order->total_amount) / self::LOYALTY_EARN_SPEND_STEP) * self::LOYALTY_EARN_POINTS_PER_STEP;

        if ($earnedPoints > 0) {
            $balanceAfter = $customer->loyalty_points + $earnedPoints;
            $customer->forceFill([
                'loyalty_points' => $balanceAfter,
                'total_purchases' => round((float) $customer->total_purchases + (float) $order->total_amount, 2),
            ])->save();

            LoyaltyTransaction::create([
                'customer_id' => $customer->id,
                'user_id' => $user->id,
                'type' => 'earned',
                'points' => $earnedPoints,
                'balance_after' => $balanceAfter,
                'reference_type' => Order::class,
                'reference_id' => $order->id,
                'notes' => "Earned from order {$order->order_number}",
            ]);
        } else {
            $customer->increment('total_purchases', (float) $order->total_amount);
        }

        $order->forceFill([
            'loyalty_points_earned' => $earnedPoints,
        ])->save();
    }

    private function resolveShift(User $user, int $branchId, ?int $shiftId): Shift
    {
        $query = Shift::query()
            ->where('branch_id', $branchId)
            ->where('user_id', $user->id)
            ->where('status', 'open');

        if ($shiftId !== null) {
            $query->whereKey($shiftId);
        }

        $shift = $query->latest('opened_at')->first();

        if ($shift === null) {
            throw ValidationException::withMessages([
                'shift_id' => ['An open shift is required before checkout.'],
            ]);
        }

        return $shift;
    }

    private function handleTableOccupation(int $tableId, Order $order): void
    {
        $table = DiningTable::query()
            ->where('branch_id', $order->branch_id)
            ->findOrFail($tableId);
        
        $table->update([
            'status' => 'occupied',
            'current_order_id' => $order->id,
        ]);
    }

    private function generateKitchenTickets(Order $order, array $data): void
    {
        // Simple logic: create one ticket per order if it's a restaurant order.
        // In more complex systems, you'd split by category (Kitchen vs Bar).
        
        $ticket = KitchenTicket::create([
            'order_id' => $order->id,
            'branch_id' => $order->branch_id,
            'ticket_number' => 'TKT-' . $order->order_number,
            'status' => 'pending',
            'priority' => 0,
            'notes' => $data['kitchen_notes'] ?? null,
        ]);

        foreach ($order->items as $item) {
            $ticket->items()->create([
                'order_item_id' => $item->id,
                'product_name' => $item->name,
                'quantity' => $item->quantity,
                'notes' => $item->notes,
            ]);
        }
    }

    private function generateOrderNumber(): string
    {
        return 'ORD-'.now()->format('YmdHis').'-'.str_pad((string) random_int(1, 9999), 4, '0', STR_PAD_LEFT);
    }
}
