<?php

namespace Modules\NsGastro\Services;

use App\Events\OrderAfterUpdatedEvent;
use App\Exceptions\NotAllowedException;
use App\Exceptions\NotFoundException;
use App\Models\Order;
use App\Models\Product;
use Illuminate\Database\Eloquent\Collection;
use Modules\NsGastro\Events\GastroOrderAfterMovedEvent;
use Modules\NsGastro\Events\GastroOrderAfterSetEvent;
use Modules\NsGastro\Events\KitchenAfterUpdatedOrderEvent;
use Modules\NsGastro\Models\Area;
use Modules\NsGastro\Models\Order as ModelsOrder;
use Modules\NsGastro\Models\OrderProduct;
use Modules\NsGastro\Models\Table;
use Modules\NsGastro\Models\TableSession;
use Modules\NsGastro\Models\TableSessionHistory;

class TableService
{
    /**
     * fetch order assigned
     * to a specific table
     *
     * @param  Table  $table
     * @return Collection
     */
    public function getTableOrders(Table $table, $range_starts = null, $range_ends = null)
    {
        if ((bool) ns()->option->get('ns_gastro_enable_table_sessions', false)) {
            $session = $table
                ->sessions()
                ->active()
                ->first();

            if ($session instanceof TableSession) {
                return $session
                    ->orders()
                    ->orderBy('id', 'desc')
                    ->with('products.modifiers', 'customer', 'user')
                    ->get();
            }

            return collect([]);
        } else {
            $range_starts = $range_starts === null ? ns()->date->copy()->startOfDay()->toDateTimeString() : $range_starts;
            $range_ends = $range_ends === null ? ns()->date->copy()->endOfDay()->toDateTimeString() : $range_ends;

            return $table->orders()
                ->orderBy('id', 'desc')
                ->where('created_at', '>=', $range_starts)
                ->where('created_at', '<=', $range_ends)
                ->with('products.modifiers', 'customer', 'user')
                ->get();
        }
    }

    /**
     * Will search table matching
     * the provided query
     *
     * @param string table name
     * @return array
     */
    public function searchTables( string | null $name, int $ignore_table_id = 0)
    {
        return Table::where('name', 'like', '%'.$name.'%')
            ->where( 'id', '<>', $ignore_table_id )
            ->get();
    }

    /**
     * Change the table status
     *
     * @param Table  $table
     * @param bool $busy
     * @return void
     */
    public function changeTableAvailability(Table $table, bool $busy )
    {
        /**
         * from now, we should only change the status of a 
         * table if we're sure the table session is enabled.
         */
        if (in_array($busy, [ true, false ]) && ( bool ) ns()->option->get( 'ns_gastro_enable_table_sessions' ) ) {
            /**
             * if a session has been created, while closing
             * we'll make sure to get an active session.
             */
            $table->busy = $busy;
            $table->save();

            return [
                'status'    =>  'success',
                'message'   =>  __m('The table availability has been updated.', 'NsGastro'),
            ];
        }

        throw new NotAllowedException( __m('The table availability can\'t be updated.', 'NsGastro') );
    }

    public function getTableSessions(Table $table, $rangeStarts, $rangeEnds)
    {
        return $table->sessions()
            ->where('session_starts', '>=', $rangeStarts ?: ns()->date->getNow()->startOfDay()->toDateTimeString())
            ->where('session_starts', '<=', $rangeEnds ?: ns()->date->getNow()->endOfDay()->toDateTimeString())
            ->orderBy('id', 'desc')
            ->get()
            ->map(function ($session) {
                $session->ordersCount = $session->orders()->count();

                return $session;
            });
    }

    public function getActiveTableSessionOrders( Table $table )
    {
        $session = $this->getActiveTableSession($table);

        if (! $session instanceof TableSession) {
            return collect([]);
        }

        return $session->orders()
            ->orderBy('id', 'desc')
            ->with('products.modifiers', 'customer', 'user')
            ->get();
    }

    public function stopTableSession( Table $table )
    {
        $activeSession   =  $table->sessions()->active()->first(); 
        
        if ( $activeSession instanceof TableSession ) {
            $activeSession->active = false;
            $activeSession->session_ends = ns()->date->getNow()->toDateTimeString();
            $activeSession->save();
        }

        return [
            'status'    =>  'success',
            'message'   =>  __('The session has been successfully updated.'),
        ];
    }

    /**
     * Closes a table session
     * and make the table available by the same way
     *
     * @param  Tablesession  $session
     * @return array $response
     */
    public function closeTableSession(TableSession $session)
    {
        /**
         * Let's now save the session
         * as it should be saved.
         */
        $session->active = false;
        $session->session_ends = ns()->date->toDateTimeString();
        $session->save();

        return [
            'status'    =>  'success',
            'message'   =>  __('The session has been successfully updated.'),
        ];
    }

    /**
     * Move an order from one table to another
     *
     * @param  Order  $order
     * @param  int  $table_id new destination table
     * @return array result
     */
    public function changeTable(Order $order, $table_id)
    {
        $prevOrder  =   clone $order;

        if ($order->table_id === $table_id) {
            throw new NotAllowedException(__m('The order is already assigned to this table.', 'NsGastro'));
        }

        $previousTable  =   Table::find( $order->table_id );
        $table = Table::find($table_id);

        if (! $table instanceof Table) {
            throw new NotFoundException(__m('Unable to find the destination table.', 'NsGastro'));
        }

        /**
         * let's verify if the table has an order and
         * if that order customer match the new customer
         * in case we've disabled multiple customer per table
         */
        if (! (bool) $table->allow_multi_clients) {
            $session = $this->getActiveTableSession($table);

            if ($session instanceof TableSession) {
                $session->orders->each(function ($_order) use ($order) {
                    if ($_order->customer_id !== $order->customer_id) {
                        throw new NotAllowedException(__m('This table doesn\'t allow multiple customers', 'NsGastro'));
                    }
                });
            }
        }

        /**
         * We'll now check if the destination table has an ongoing session
         * if not, we'll open a new session for that table
         */
        $session = $this->startTableSession($table, true);

        /**
         * Now let's assign the order to the table
         * and to the session.
         */
        $order->table_id = $table_id;
        $order->gastro_table_session_id = $session->id;
        $order->save();

        /**
         * set the previous table as no longer
         * busy will eventually close the table session if that exists
         */
        $previousTable->busy    =   false;
        $previousTable->save();
        
        GastroOrderAfterMovedEvent::dispatch( $previousTable, $table, $order );

        return [
            'status'    =>  'success',
            'message'   =>  sprintf(__m('The order has been successfully moved to %s', 'NsGastro'), $table->name),
        ];
    }

    public function saveTable( $order )
    {
        $fields     =   $order->getData();
        
        if (isset($fields['table']) && isset($fields['table']['selected']) && $fields['table']['selected'] === true) {
            
            /**
             * We'll start by checking if the table exists.
             * then, fi the session is enabled, weùll mark the table as busy.
             * That will ensure sessions starts accordingly.
             */
            $table = Table::findOrFail( $fields['table']['id']);

            if ( ( bool ) ns()->option->get( 'ns_gastro_enable_table_sessions', 0 ) ) {
                $table->busy = true;
                $table->save();
            }

            $area = Area::find($table->area_id);

            /**
             * Let's check if the session has started. We should remember that the
             * session stating process is not run asynchronously. So, from this point,
             * we can assume that the session has been started.
             */
            $session = $this->getActiveTableSession($table);

            /**
             * if by any change the session hasn't started
             * that probably means the session feature is disabled
             * we shouldn't assign the order to a session.
             */
            if( $session instanceof TableSession) {
                $order->gastro_table_session_id = $session->id;
            }

            $order->table_id = $table->id;
            $order->table_name = $table->name;
            $order->area_name = $area instanceof Area ? $area->name : null;
            $order->area_id = $table->area_id;
            $order->seats = $table->seats ?? 0;
            $order->gastro_order_status = $fields['gastro_order_status'] ?? ModelsOrder::COOKING_PENDING;

            /**
             * We might check if all products
             * that has been submitted are premarked
             * as ready
             */
            $order->products->each(function ($orderProduct) {
                if ($orderProduct->product instanceof Product && (bool) $orderProduct->product->skip_cooking) {
                    $orderProduct->cooking_status = OrderProduct::COOKING_READY;
                    $orderProduct->save();
                }
            });
        }

        KitchenAfterUpdatedOrderEvent::dispatch($order);
        GastroOrderAfterSetEvent::dispatch( $order );
        
        $order->save();
    }

    public function closeTableSessionIfFreed( Table | null $table)
    {
        if ( $table instanceof Table ) {
            // Check the table sessions orders and see if either the sessions has no, unpaid or hold order
            // If that's the case, we'll close the session
            $session = $this->getActiveTableSession($table);
    
            if (! $session instanceof TableSession) {
                return;
            }
    
            $session->load('orders');

            // Fetch all counts in a single query
            $orderCounts = $session->orders()
                ->selectRaw('
                    COUNT(*) as all_orders,
                    SUM(payment_status = ?) as void_orders,
                    SUM(payment_status = ?) as refunded_orders,
                    SUM(payment_status = ?) as paid_orders
                ', [Order::PAYMENT_VOID, Order::PAYMENT_REFUNDED, Order::PAYMENT_PAID])
                ->first();
    
            if ($orderCounts->all_orders === ($orderCounts->void_orders + $orderCounts->refunded_orders + $orderCounts->paid_orders)) {
                $table->busy = false;
                $table->save();
            }
        }
    }

    /**
     * Will start a table session
     * using an order
     *
     * @param  Order  $order
     * @return TableSession | null
     * @deprecated
     */
    public function startTableSessionUsingOrder( Order $order ): null | TableSession
    {
        if ( empty( $order->table_id  ) ) {
            return null;
        }

        if ( ( bool ) ns()->option->get('ns_gastro_enable_table_sessions', false) === false ) {
            return null;
        }

        $table = Table::find($order->table_id);

        if (! $table instanceof Table) {
            throw new NotFoundException(__m('Unable to find the destination table.', 'NsGastro'));
        }

        $session = $this->startTableSession(
            table: $table,
            silent: true
        );

        $tableHistory   =   TableSessionHistory::where( 'order_id', $order->id )
            ->where( 'session_id', $session->id )
            ->where( 'table_id', $table->id )
            ->first();

        if ( ! $tableHistory instanceof TableSessionHistory ) {
            $tableHistory = new TableSessionHistory;
            $tableHistory->table_id = $table->id;
            $tableHistory->order_id = $order->id;
            $tableHistory->session_id = $session->id;
            $tableHistory->author = $order->author;
            $tableHistory->save();
        }

        /**
         * We'll assign the session id to the order
         */
        $order->gastro_table_session_id = $session->id;
        $order->save();

        return $session;
    }

    /**
     * Will start a table session
     *
     * @param  Table  $table
     * @param  bool  $silent
     * @return TableSession
     */
    public function startTableSession(Table $table, $silent = false)
    {
        /**
         * if the table session is disabled
         * there is no need to start a session
         */
        if ( ( bool ) ns()->option->get('ns_gastro_enable_table_sessions', false) === false ) {
            return null;
        }

        $session = $this->getActiveTableSession($table);

        if ($session instanceof TableSession && $silent === false) {
            throw new NotAllowedException(__m('The table session has already be opened.', 'NsGastro'));
        }

        if (! $session instanceof TableSession) {
            $session = new TableSession;
            $session->table_id = $table->id;
            $session->session_starts = ns()->date->getNow()->toDateTimeString();
            $session->save();
        }

        return $session;
    }

    public function getActiveTableSession(Table $table): TableSession | null
    {
        return $table->sessions()->where('active', 1)->orderBy('session_starts', 'desc')->first();
    }

    /**
     * @todo check usage
     * @deprecated ?
     */
    public function openTableSession(TableSession $session)
    {
        $session->active = true;
        $session->session_ends = null;
        $session->save();

        return [
            'status'    =>  'success',
            'message'   =>  __('The session has been successfully updated.'),
        ];
    }

    /**
     * $order is not an instance of App/Models/Order because
     * the model has yet been deleted. NexoPOS however turn that into a stdClass.
     * We can't therefore define the type of the parameter.
     * 
     * @param stdClass $order
     */
    public function deleteOrderSessionHistory( $order )
    {
        TableSessionHistory::where('order_id', $order->id)->delete();

        return [
            'status'    =>  'success',
            'message'   =>  __('The order session history has been successfully deleted.'),
        ];
    }
}
