<?php

namespace Modules\Order\Services\CreateOrder;

use DB;
use Exception;
use Illuminate\Support\Collection;
use Modules\Branch\Models\Branch;
use Modules\Cart\Facades\PosCart;
use Modules\Currency\Models\CurrencyRate;
use Modules\Order\Enums\OrderPaymentStatus;
use Modules\Order\Enums\OrderStatus;
use Modules\Order\Enums\OrderType;
use Modules\Order\Events\OrderCreated;
use Modules\Order\Models\Order;
use Modules\Order\Services\Order\OrderServiceInterface;
use Modules\Payment\Enums\PaymentType;
use Modules\Pos\Enums\PosSessionStatus;
use Modules\Pos\Models\PosRegister;
use Modules\Pos\Models\PosSession;
use Modules\Printer\Enum\PrinterRole;
use Modules\SeatingPlan\Enums\TableMergeType;
use Modules\SeatingPlan\Enums\TableStatus;
use Modules\SeatingPlan\Models\Table;
use Modules\User\Enums\DefaultRole;
use Modules\User\Models\User;

class CreateOrderService implements CreateOrderServiceInterface
{
    /** @inheritDoc */
    public function create(array $data): Order
    {
        return DB::transaction(function () use ($data) {
            $user = auth()->user();

            /** @var Branch $branch */
            $branch = $user->effective_branch;
            $data['type'] = PosCart::orderType()->value();
            $isDineIn = $data['type'] == OrderType::DineIn->value;

            $data['payment_methods'] = $isDineIn ? [] : ($data['payment_methods'] ?? []);
            $data['payments'] = $isDineIn ? [] : ($data['payments'] ?? []);

            $posSession = $this->getPosActiveSession(
                branch: $branch,
                posRegisterId: $data['pos_register_id'],
            );

            $data['pos_session_id'] = $posSession->id;
            $data['currency_rate'] = CurrencyRate::for($branch->currency);
            $totalRound = PosCart::total()->round(3)->amount();

            abort_if(
                !empty($data['payment_methods'])
                && (($data['payment_type'] == PaymentType::Full->value && $data['amount_to_be_paid'] != $totalRound)
                    || ($data['payment_type'] == PaymentType::Partial->value && $data['amount_to_be_paid'] >= $totalRound)),
                400,
                __("order::messages.amount_to_be_paid_is_not_valid")
            );

            $order = $this->store($branch, $user, $data);
            $this->updateTableStatus($order);
            if (!$isDineIn) {
                $this->storePayments($user, $order, $posSession, $data);
            }
            $this->storeOrderProducts($order, PosCart::items());
            $order->storeOrUpdateTaxes(PosCart::taxes());
            $order->storeOrUpdateDiscount(PosCart::discount());

            event(new OrderCreated($order));

            $order = $order->fresh();

            try {
                app(OrderServiceInterface::class)->print($order->id, [PrinterRole::Cashier]);
            } catch (Exception) {
            }

            return $order;
        });
    }


    /**
     * Get pos active session
     *
     * @param Branch $branch
     * @param int $posRegisterId
     * @return PosSession
     */
    public function getPosActiveSession(
        Branch $branch,
        int    $posRegisterId,
    ): PosSession
    {
        $posRegister = PosRegister::query()
            ->with(["lastSession" => fn($query) => $query->with("branch:id,currency")
                ->where('status', PosSessionStatus::Open)])
            ->where('id', $posRegisterId)
            ->where("branch_id", $branch->id)
            ->withOutGlobalBranchPermission()
            ->firstOrFail();

        abort_if(
            is_null($posRegister->lastSession),
            400,
            __("pos::messages.no_active_session", [
                "action" => __("pos::messages.cash_payment")
            ])
        );

        return $posRegister->lastSession;
    }

    /**
     * Store order
     *
     * @param Branch $branch
     * @param User $user
     * @param array $data
     * @return Order
     */
    private function store(Branch $branch, User $user, array $data): Order
    {

        $isDineIn = $data['type'] == OrderType::DineIn->value;
        $tableMergeId = null;
        $hasPayments = !$isDineIn
            && $user->can('admin.orders.receive_payment')
            && isset($data['payment_methods'])
            && !empty($data['payment_methods']);

        $tableId = $isDineIn ? ($data['table_id'] ?? null) : null;
        $waiterId = $user->hasRole(DefaultRole::Waiter->value) && $isDineIn
            ? $user->id
            : null;

        if (!is_null($tableId)) {
            $table = Table::query()->with(["currentMerge", "activeOrder"])->findOrFail($tableId);

            abort_unless(is_null($table->activeOrder), 400, __("order::messages.table_already_have_active_order"));

            if (is_null($waiterId) && !is_null($table->assigned_waiter_id)) {
                $waiterId = $table->assigned_waiter_id;
            }

            if (!is_null($table->currentMerge) && $table->currentMerge->type == TableMergeType::Billing) {
                $tableMergeId = $table->currentMerge->id;
            }
        }

        return Order::query()
            ->create([
                "branch_id" => $branch->id,
                "table_id" => $tableId,
                "customer_id" => PosCart::customer()?->id(),
                "pos_register_id" => $data['pos_register_id'] ?? null,
                "pos_session_id" => $data['pos_session_id'],
                "waiter_id" => $waiterId,
                "cashier_id" => $hasPayments
                    ? $user->id
                    : null,
                "status" => OrderStatus::Pending,
                "type" => $data['type'],
                "payment_status" => OrderPaymentStatus::Unpaid,
                "currency" => $branch->currency,
                "currency_rate" => $data['currency_rate'],
                "subtotal" => PosCart::subTotal()->amount(),
                "total" => PosCart::total()->amount(),
                "due_amount" => PosCart::total()->amount(),
                ...($data['type'] == OrderType::DriveThru->value
                    ? [
                        "car_plate" => $data['car_plate'],
                        "car_description" => $data['car_description'],
                    ]
                    : []),
                "scheduled_at" => in_array($data['type'], [OrderType::PreOrder->value, OrderType::Catering->value])
                    ? ($data['scheduled_at'] ?? null)
                    : null,
                "guest_count" => $data['guest_count'] ?? 1,
                "notes" => $data["notes"] ?? null,
                "order_date" => now(),
                "served_at" => $isDineIn ? now() : null,
                "table_merge_id" => $tableMergeId
            ]);
    }

    /**
     * Update table status
     *
     * @param Order $order
     * @return void
     */
    private function updateTableStatus(Order $order): void
    {
        if ($order->type == OrderType::DineIn && !is_null($order->table_id)) {
            if ($order->table->status != TableStatus::Occupied) {
                $order->table->update(["status" => TableStatus::Occupied]);
                $order->table->storeStatusLog(
                    status: TableStatus::Occupied,
                    note: "ORDER_CREATED :$order->reference_no"
                );
            }
        }
    }

    /** @inheritDoc */
    public function storePayments(
        User            $user,
        Order           $order,
        PosSession|null $posSession,
        array           $data,
    ): void
    {
        $paymentMethods = $data['payment_methods'];
        $payments = $data['payments'];

        if ($user->can('admin.orders.receive_payment') && !empty($paymentMethods)) {
            $total = $data['amount_to_be_paid'];
            $data = collect();

            if (count($paymentMethods) == 1) {
                $payments = [
                    [
                        "method" => $paymentMethods[0],
                        "amount" => $total
                    ]
                ];
            }

            foreach ($payments as $payment) {
                $data->push([
                    "cashier_id" => $user->id,
                    "method" => $payment['method'],
                    "amount" => $payment['amount'],
                    "session" => $posSession
                ]);
            }

            $paymentTotal = $data->sum('amount');

            abort_if(
                round($paymentTotal, 3) != round($total, 3),
                400,
                __("order::messages.insufficient_payment", [
                    "paid" => number_format($paymentTotal, 2),
                    "total" => number_format($total, 2)
                ])
            );

            $order->storePayments($data->all());
        }
    }

    /**
     * Store order products
     *
     * @param Order $order
     * @param Collection $products
     * @return void
     */
    private function storeOrderProducts(Order $order, Collection $products): void
    {
        foreach ($products as $product) {
            $order->storeOrUpdateProduct($product);
        }
    }
}
