<?php

namespace Modules\Order\Services\Order;

use App\Forkiva;
use DB;
use Exception;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use Modules\Cart\Facades\EditOrderCart;
use Modules\Discount\Models\Discount;
use Modules\Menu\Models\Menu;
use Modules\Order\Enums\OrderPaymentStatus;
use Modules\Order\Enums\OrderStatus;
use Modules\Order\Enums\OrderType;
use Modules\Order\Enums\ReasonType;
use Modules\Order\Events\OrderUpdateStatus;
use Modules\Order\Models\Order;
use Modules\Order\Models\Reason;
use Modules\Order\Services\CreateOrder\CreateOrderServiceInterface;
use Modules\Payment\Enums\PaymentMethod;
use Modules\Payment\Enums\PaymentType;
use Modules\Payment\Enums\RefundPaymentMethod;
use Modules\Pos\Enums\PosSessionStatus;
use Modules\Pos\Models\PosRegister;
use Modules\Pos\Models\PosSession;
use Modules\Pos\Services\Pos\PosServiceInterface;
use Modules\Printer\Enum\PrinterRole;
use Modules\Printer\Services\Printer\PrinterServiceInterface;
use Modules\SeatingPlan\Enums\TableMergeType;
use Modules\SeatingPlan\Enums\TableStatus;
use Modules\SeatingPlan\Models\Table;
use Modules\SeatingPlan\Models\TableMerge;
use Modules\Support\Enums\DateTimeFormat;
use Modules\Support\GlobalStructureFilters;
use Modules\User\Enums\DefaultRole;

class OrderService implements OrderServiceInterface
{
    /** @inheritDoc */
    public function label(): string
    {
        return __("order::orders.order");
    }

    /** @inheritDoc */
    public function getStructureFilters(): array
    {
        $branchFilter = GlobalStructureFilters::branch();
        return [
            ...(is_null($branchFilter) ? [] : [$branchFilter]),
            [
                "key" => 'status',
                "label" => __('order::orders.filters.status'),
                "type" => 'select',
                "options" => OrderStatus::toArrayTrans(),
            ],
            [
                "key" => 'type',
                "label" => __('order::orders.filters.type'),
                "type" => 'select',
                "options" => OrderType::toArrayTrans(),
            ],
            [
                "key" => 'payment_status',
                "label" => __('order::orders.filters.payment_status'),
                "type" => 'select',
                "options" => OrderPaymentStatus::toArrayTrans(),
            ],
            GlobalStructureFilters::from(),
            GlobalStructureFilters::to(),
        ];
    }

    /** @inheritDoc */
    public function cancel(int|string $id, array $data): void
    {
        /** @var Order $order */
        $order = $this->findOrFail($id);

        abort_unless(
            $order->cancelIsAllowed(),
            400,
            __("order::messages.order_cannot_cancel", ["status" => $order->status->trans()])
        );

        $activeSession = $this->getPosActiveSession(
            $data['pos_register_id'],
            __("admin::resource.system_cancel", ["resource" => __("order::orders.order")])
        );

        $order->update(['status' => OrderStatus::Cancelled]);


        event(
            new OrderUpdateStatus(
                order: $order,
                status: OrderStatus::Cancelled,
                reasonId: $data['reason_id'],
                changedById: auth()->id(),
                note: $data['note'] ?? null,
                posSession: $activeSession
            )
        );
    }

    /** @inheritDoc */
    public function findOrFail(int|string $id, bool $withBranch = false): Builder|array|EloquentCollection|Order
    {
        return $this->getModel()
            ->query()
            ->when($withBranch, fn(Builder $query) => $query->with("branch"))
            ->where(fn($query) => $query->where('id', $id)
                ->orWhere('reference_no', $id))
            ->firstOrFail();
    }

    /** @inheritDoc */
    public function getModel(): Order
    {
        return new ($this->model());
    }

    /** @inheritDoc */
    public function model(): string
    {
        return Order::class;
    }

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

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

        return $posRegister->lastSession;
    }

    /** @inheritDoc */
    public function refund(int|string $id, array $data): void
    {
        /** @var Order $order */
        $order = $this->findOrFail($id);

        abort_unless($order->refundIsAllowed(), 400, __("order::messages.order_cannot_refund"));

        $activeSession = $this->getPosActiveSession(
            $data['pos_register_id'],
            __("admin::resource.system_refund", ["resource" => __("order::orders.order")])
        );

        $order->update(['status' => OrderStatus::Refunded]);

        event(
            new OrderUpdateStatus(
                order: $order,
                status: OrderStatus::Refunded,
                reasonId: $data['reason_id'],
                changedById: auth()->id(),
                note: $data['note'] ?? null,
                posSession: $activeSession
            )
        );
    }

    /** @inheritDoc */
    public function getUpdateStatusMeta(int|string $id = null): array
    {
        $order = $this->findOrFail($id);

        return [
            "reasons" => Reason::list(($order->cancelIsAllowed() ? ReasonType::Cancellation : ReasonType::Refund)->value),
            "pos_registers" => PosRegister::list($order->branch_id),
        ];
    }

    /** @inheritDoc */
    public function addPayment(int|string $id, array $data): void
    {
        $order = $this->findOrFail($id);

        abort_unless($order->allowAddPayment() || is_null($order->table_merge_id), 400, __("order::messages.order_payment_not_allowed"));

        $session = $this->getPosActiveSession($data['pos_register_id'], __("order::orders.add_payment"));

        DB::transaction(function () use ($order, $session, $data) {
            $user = auth()->user();
            $isOrderComplete = (round($order->due_amount->amount(), 3) - round($data['amount_to_be_paid'], 3)) == 0 && $order->next_status == OrderStatus::Completed;

            app(CreateOrderServiceInterface::class)->storePayments(
                user: $user,
                order: $order,
                posSession: $session,
                data: $data
            );

            $order->update([
                "status" => $isOrderComplete ? OrderStatus::Completed : $order->status,
                "closed_at" => $isOrderComplete && $order->type === OrderType::DineIn ? now() : null,
                "cashier_id" => $user->id,
                "pos_session_id" => $order->pos_session_id ?: $session->id
            ]);

            if ($isOrderComplete) {
                event(
                    new OrderUpdateStatus(
                        order: $order,
                        status: OrderStatus::Completed,
                        changedById: $user->id,
                        posSession: $session
                    )
                );
            }
        });

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

    /** @inheritDoc */
    public function print(int|string $id, ?array $roles = null): void
    {
        $data = $this->getPrintData($id);
        $roles = $roles ?: PrinterRole::cases();
        $error = null;

        foreach ($roles as $role) {
            if ($role == PrinterRole::Cashier && !is_null($data['error_for_print_receipt'])) {
                $error = $data['error_for_print_receipt'];
                continue;
            }
            app(PrinterServiceInterface::class)->dispatch($role, $data);
        }

        abort_if(!is_null($error) && count($roles) == 1 && $roles[0] == PrinterRole::Cashier, 400, $error);
    }

    /** @inheritDoc */
    public function getPrintData(int|string $id): array
    {
        $errorForPrintReceipt = null;
        $with = [
            "products" => fn($query) => $query->with(["product" => fn($query) => $query->with("categories:id,slug")]),
            "taxes",
            "table" => fn($query) => fn($query) => $query->select("id", "name")->with(["floor:id,name", "zone:id,name"]),
        ];
        $mainOrder = $this->getModel()
            ->query()
            ->with([...$with, "tableMerge", "branch:id,name"])
            ->where(fn($query) => $query->where('id', $id)
                ->orWhere('reference_no', $id))
            ->firstOrFail();

        if ($mainOrder->payment_status != OrderPaymentStatus::Paid) {
            $errorForPrintReceipt = __("order::messages.order_not_allow_print_receipt");
        }

        $subTotal = $mainOrder->subtotal;
        $taxes = [];
        $total = $mainOrder->total;
        $items = [];
        $totalQuantity = 0;

        if (
            !is_null($mainOrder->table_merge_id)
            && $mainOrder->tableMerge->type == TableMergeType::Billing
        ) {

            $orders = [
                $mainOrder,
                ...$this->getModel()
                    ->query()
                    ->with($with)
                    ->where("id", "<>", $mainOrder->id)
                    ->where("table_merge_id", $mainOrder->table_merge_id)
                    ->whereNotIn("status", [
                        OrderStatus::Cancelled,
                        OrderStatus::Refunded,
                        OrderStatus::Merged
                    ])
                    ->get()
            ];

        } else {
            $orders = [$mainOrder];
        }

        /** @var Order $order */
        foreach ($orders as $order) {

            if ($order->id != $mainOrder->id) {
                if (is_null($errorForPrintReceipt) && $order->payment_status != OrderPaymentStatus::Paid) {
                    $errorForPrintReceipt = __("order::messages.not_allow_print_receipt_on_order_merged");
                }

                $subTotal = $subTotal->add($order->subtotal);
                $total = $total->add($order->total);
            }

            foreach ($order->taxes as $tax) {
                if (isset($taxes[$tax->tax_id])) {
                    $taxes[$tax->tax_id]["amount"] = $taxes[$tax->tax_id]["amount"]->add($tax->amount);
                } else {
                    $taxes[$tax->tax_id] = [
                        "name" => $tax->name,
                        "amount" => $tax->amount,
                    ];
                }
            }

            foreach ($order->products as $product) {
                $totalQuantity += $product->quantity;

                $items[] = [
                    "id" => $product->id,
                    "name" => $product->product->name,
                    "status" => $product->status,
                    "quantity" => $product->quantity,
                    "category_slugs" => $product->product->categories->pluck("slug")->toArray(),
                    "unit_price" => $product->unit_price->add($product->tax_total->divide($product->quantity)),
                    "total" => $product->total,
                    "options" => $product->options->map(fn($option) => [
                        "id" => $option->id,
                        "name" => $option->name,
                        "value" => is_null($option->value)
                            ? $option->values
                                ->map(fn($optionValue) => [
                                    "id" => $optionValue->id,
                                    "label" => $optionValue->label,
                                ])->toArray()
                            : $option->value,
                    ])
                ];
            }
        }

        return [
            "type" => $mainOrder->type,
            "branch_id" => $mainOrder->branch_id,
            "title" => $mainOrder->branch->name,
            "table" => !is_null($mainOrder->table) ? [
                "table" => $mainOrder->table->name,
                "floor" => $mainOrder->table->floor->name,
                "zone" => $mainOrder->table->zone->name,
            ] : null,
            "order_number" => $mainOrder->order_number,
            "reference_no" => $mainOrder->reference_no,
            "items" => $items,
            "sub_total" => $subTotal,
            "taxes" => array_values($taxes),
            "discount" => $order->hasDiscount() ? $order->discount->amount : null,
            "total" => $total,
            "total_quantity" => $totalQuantity,
            "total_items" => count($items),
            "error_for_print_receipt" => $errorForPrintReceipt,
            "pos_register_id" => $mainOrder->pos_register_id,
            "is_modified" => !is_null($mainOrder->modified_at),
            "date" => dateTimeFormat($mainOrder->payment_at ?: $mainOrder->created_at, DateTimeFormat::Date),
            "time" => dateTimeFormat($mainOrder->payment_at ?: $mainOrder->created_at, DateTimeFormat::Time),
        ];
    }

    /** @inheritDoc */
    public function get(array $filters = [], ?array $sorts = []): LengthAwarePaginator
    {
        return $this->getModel()
            ->query()
            ->with(["branch:id,name", "customer:id,name,phone,phone_country_iso_code"])
            ->filters($filters)
            ->sortBy($sorts)
            ->paginate(Forkiva::paginate())
            ->withQueryString();
    }

    /** @inheritDoc */
    public function getAddPaymentMeta(int|string $id = null): array
    {
        $order = $this->findOrFail($id, true);

        return [
            "payment_methods" => array_filter(
                PaymentMethod::toArrayTrans(),
                fn($orderType) => in_array(
                    $orderType['id'],
                    $order->branch->payment_methods ?: []
                )
            ),
            "payment_types" => PaymentType::toArrayTrans(),
            "pos_registers" => PosRegister::list($order->branch_id),
        ];
    }

    /** @inheritDoc */
    public function moveToNextStatus(int|string $id): OrderStatus
    {
        $order = $this->findOrFail($id);
        $nextStatus = $order->next_status;

        abort_if(
            is_null($nextStatus)
            || ($order->next_status != OrderStatus::Confirmed && !$order->isScheduledForToday())
            || (!$order->payment_status->isPaid() && $nextStatus == OrderStatus::Completed),
            400,
            __("order::messages.could_not_update_order_status")
        );

        return DB::transaction(function () use ($order, $nextStatus) {
            $order->update([
                "status" => $nextStatus,
                "closed_at" => ($order->type === OrderType::DineIn && $nextStatus == OrderStatus::Completed) ? now() : null
            ]);

            event(
                new OrderUpdateStatus(
                    order: $order,
                    status: $nextStatus,
                    changedById: auth()->id(),
                )
            );

            return $nextStatus;
        });

    }

    /** @inheritDoc */
    public function kitchenMoveToNextStatus(int|string $id): OrderStatus
    {
        $order = $this->findOrFail($id);
        $nextStatus = $order->next_status;

        abort_if(
            is_null($nextStatus) || !in_array($nextStatus, [OrderStatus::Preparing, OrderStatus::Ready]),
            400, __("order::messages.could_not_update_order_status")
        );

        return DB::transaction(function () use ($order, $nextStatus) {
            $order->update(["status" => $nextStatus]);

            event(
                new OrderUpdateStatus(
                    order: $order,
                    status: $nextStatus,
                    changedById: auth()->id(),
                )
            );

            return $nextStatus;
        });

    }

    /** @inheritDoc */
    public function addPaymentMerge(int $mergeId, array $data): void
    {
        $user = auth()->user();
        $merge = TableMerge::query()
            ->where('type', TableMergeType::Billing)
            ->whereNull('closed_at')
            ->whereNull('closed_by')
            ->where('id', $mergeId)
            ->firstOrFail();

        $orders = $this->getModel()->query()
            ->with(["branch:id,name", "table:id,name,status"])
            ->where('table_merge_id', $merge->id)
            ->activeOrders()
            ->get();


        abort_if($orders->count() == 0, 400, __("order::messages.order_payment_not_allowed"));

        $totalOrderAmount = 0;
        /** @var Order $order */
        foreach ($orders as $order) {
            $totalOrderAmount += $order->due_amount->amount();
            abort_if($order->next_status != OrderStatus::Completed || !$order->allowAddPayment(), 400, __("order::messages.order_payment_not_allowed"));
        }

        $posSession = $this->getPosActiveSession($data['pos_register_id'], __("order::orders.add_payment"));
        $paymentMethods = $data['payment_methods'];

        $total = round($data['amount_to_be_paid'], 3);
        $totalOrderAmount = round($totalOrderAmount, 3);


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

        $payments = collect(count($paymentMethods) == 1
            ? [["method" => $paymentMethods[0], "amount" => $total]]
            : $data['payments']);

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

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

        $perOrderPayments = [];
        $orderPercents = [];
        $numberOfOrders = $orders->count();

        foreach ($orders as $orderIndex => $order) {
            $orderPercents[$order->id] = 0;
            $orderPercent = $order->due_amount->amount() / $totalOrderAmount;

            foreach ($payments as $paymentIndex => $payment) {
                $isLastOrder = ($orderIndex === $numberOfOrders - 1);
                $isLastPayment = ($paymentIndex === $payments->count() - 1);

                if ($isLastOrder && $isLastPayment) {
                    $alreadyDistributed = collect($perOrderPayments)
                        ->flatten(1)
                        ->where('method', $payment['method'])
                        ->sum('amount');

                    $amount = round($payment['amount'] - $alreadyDistributed, 3);
                } else {
                    $amount = round($payment['amount'] * $orderPercent, 3);
                }

                $perOrderPayments[$order->id][] = [
                    "cashier_id" => $user->id,
                    "method" => $payment['method'],
                    "amount" => $amount,
                    "session" => $posSession,
                ];

                $orderPercents[$order->id] += $amount;
            }
        }

        DB::transaction(function () use ($orders, $user, $merge, $posSession, $perOrderPayments, $orderPercents) {
            /** @var Order $order */
            foreach ($orders as $order) {
                $isOrderComplete = (round($order->due_amount->amount(), 3) - round($orderPercents[$order->id], 3)) == 0 && $order->next_status == OrderStatus::Completed;
                $order->storePayments($perOrderPayments[$order->id]);

                $order->update([
                    "status" => $isOrderComplete ? OrderStatus::Completed : $order->status,
                    "closed_at" => $isOrderComplete ? now() : null,
                    "cashier_id" => $user->id,
                    "pos_session_id" => $order->pos_session_id ?: $posSession->id
                ]);

                if ($isOrderComplete) {
                    event(
                        new OrderUpdateStatus(
                            order: $order,
                            status: OrderStatus::Completed,
                            changedById: $user->id,
                            posSession: $posSession
                        )
                    );
                }
            }

            if ($orders->filter(fn($order) => $order->status != OrderStatus::Completed)->isEmpty()) {
                $query = Table::query()->where('current_merge_id', $merge->id);

                $tables = $query->get();

                $query->update(["current_merge_id" => null, "status" => TableStatus::Cleaning]);

                $tables->each(fn(Table $table) => $table->storeStatusLog(status: TableStatus::Cleaning));

                $merge->update(["closed_at" => now(), "closed_by" => $user->id]);
            }
        });

        try {
            $this->print($orders[0]->id, [PrinterRole::Cashier]);
        } catch (Exception) {
        }
    }

    /** @inheritDoc */
    public function activeOrders(): Collection
    {
        return Order::query()
            ->where("branch_id", auth()->user()->effective_branch->id)
            ->whereNot("type", OrderType::DineIn)
            ->where(fn($query) => $query->whereNull("scheduled_at")
                ->orWhereDate("scheduled_at", "<=", today())
            )
            ->activeOrders()
            ->latest()
            ->get();
    }

    /** @inheritDoc */
    public function upcomingOrders(): Collection
    {
        return Order::query()
            ->where("branch_id", auth()->user()->effective_branch->id)
            ->whereDate("scheduled_at", ">", today())
            ->activeOrders()
            ->latest()
            ->get();
    }

    /** @inheritDoc */
    public function getOrdersForKitchen(): Collection
    {
        return Order::query()
            ->with(["table:id,name", "products"])
            ->where("branch_id", auth()->user()->effective_branch->id)
            ->whereIn("status", [OrderStatus::Preparing, OrderStatus::Confirmed])
            ->activeOrders()
            ->get();
    }

    /** @inheritDoc */
    public function getFormEditMeta(int|string $id): array
    {

        $user = auth()->user();
        // TODO: Change to other method to trim relationship
        $order = $this->show($id);
        $posService = app(PosServiceInterface::class);

        $menu = Menu::getActiveMenu($order->branch_id, true);

        abort_if(is_null($menu), 400, __("pos::messages.menu_is_not_active"));

        $orderTypes = $menu->branch->order_types ?: [];

        if ($user->hasRole(DefaultRole::Waiter->value) && in_array(OrderType::DineIn->value, $orderTypes)) {
            $orderTypes = [OrderType::DineIn->value];
        }

        EditOrderCart::initEditOrder($order);

        return [
            "cart" => EditOrderCart::instance(),
            "order" => $order,
            "branch" => [
                "id" => $order->branch_id,
                "name" => $order->branch->name,
                "currency" => $order->currency,
            ],
            "discounts" => Discount::list($order->branch_id)->values(),
            "categories" => $posService->getCategories($menu),
            "products" => $posService->getProducts($menu),
            "order_types" => array_filter(
                OrderType::toArrayTrans(),
                fn($orderType) => in_array($orderType['id'], $orderTypes)
            ),
            "refund_payment_methods" => array_filter(
                RefundPaymentMethod::toArrayTrans(),
                fn($orderType) => in_array(
                    $orderType['id'],
                    $menu->branch->payment_methods ?: []
                )
            ),
            "pos_registers" => PosRegister::list($order->branch_id),
        ];
    }

    /** @inheritDoc */
    public function show(int|string $id): Order
    {
        return $this->getModel()
            ->query()
            ->with([
                "branch:id,name",
                "customer:id,name,phone,phone_country_iso_code",
                "products.gift",
                "taxes",
                "payments",
                "posRegister:id,name",
                "createdBy:id,name",
                "cashier:id,name",
                "waiter:id,name",
                "table:id,name",
                "mergedIntoOrder:id,order_number,reference_no",
                "mergedBy:id,name",
                "discount.gift",
                "statusLogs" => fn($query) => $query
                    ->with([
                        "reason:id,name",
                        "changedBy:id,name"
                    ])
            ])
            ->where(function ($query) use ($id) {
                if (is_numeric($id)) {
                    $query->where('id', $id);
                } else {
                    $query->where('reference_no', $id);
                }
            })
            ->firstOrFail();
    }
}
