<?php

namespace objects;

use classes\db;
use classes\object_property;
use classes\xlvask;
use Exception;
use helpers\xlvask_customer;
use helpers\xlvask_vehicle;
use helpers\xlvask_vehicle_type;
use helpers\xlvask_vehicles;
use mysqli_result;
use traits\db_object_t;

class customer_vehicles_o extends db
{
    use db_object_t;

    public object_property $customer_id; // The id of the customer
    public object_property $type;      // The type of the vehicle
    public object_property $reg;      // The registration number of the vehicle
    public object_property $wash_subscription; // The wash subscription of the vehicle (boolean)
    public object_property $notes;     // The notes of the vehicle
    public object_property $reference; // The reference of the vehicle
    public object_property $deleted_at; // The timestamp of when the record was "deleted"

    /**
     * @param array $getAddons [db_object_t child object]
     * @return array
     */
    private static function transformObjectsToArray(array $getAddons): array
    {
        return array_map(function ($addon) {
            return $addon->asArray();
        }, $getAddons);
    }

    /**
     * Convert the object to an array
     * @return array
     * @throws Exception If the object is not selected
     */
    public function asArray(): array
    {
        self::requireSelected();
        // Check if the vehicle is cached
        $cached = $this->getCached('asArray', $this->id);
        if ($cached) {
            return (array)$cached;
        }
        $wash_subscription = (bool)$this->wash_subscription->value();
        $addons = self::transformObjectsToArray(
            self::getAddons()
        );
        $available_addons = self::transformObjectsToArray(
            self::getAvailableAddons()
        );
        $last_order_id = self::getLastOrderId();
        $customer_number = (int)$this->customer_id->value();
        $xlvask = $this->hasXLVask() ? $this->getXLVask() : null;
        $tmp = [
            'id' => (int)$this->id,
            'user_id' => (int)(new users_o())->getUserIdFromEconomic((int)$customer_number),
            'customer_id' => (int)$this->customer_id->value(),
            'customer_name' => (string)(new users_o())->getCustomerName((int)$customer_number),
            'type' => (int)$this->type->value(),
            'reg' => (string)$this->reg->value(),
            'reference' => ($this->reference->value() ? (string)$this->reference->value() : null),
            'wash_subscription' => $wash_subscription,
            'barred' => (bool)(new users_o())->isCustomerBarred((int)$customer_number),
            'addons' => [
                'enabled' => count($addons),
                'available' => count($available_addons),
                'list' => $addons,
            ],
            'last_order_id' => ($last_order_id ? (int)$last_order_id : null),
            'xlvask' => ($xlvask?->toArray()),
            'vehicle_types' => array_map(function ($vehicle_type) {
                return $vehicle_type->toArray();
            }, $this->getVehicleTypes()),
        ];
        // Cache the result
        $this->cache('asArray', $tmp, $this->id);
        $this->setCachedExpiration('asArray', self::$asArrayCacheExpiration, $this->id);
        return $tmp;
    }

    /**
     * Get the addons for the vehicle
     * @return array{customer_vehicles_addons_o}
     * @throws Exception If the object is not selected
     */
    public function getAddons(): array
    {
        self::requireSelected();
        $addons = (new customer_vehicles_addons_o())->getFieldsWhere([
            'vehicle_id' => (int)$this->id,
        ], [
            'id',
        ]);
        return array_map(function ($addon) {
            return (new customer_vehicles_addons_o())->select((int)$addon['id']);
        }, $addons);
    }

    /**
     * Get the available addons for the vehicle
     * @return array{product_options_o}
     * @throws Exception If the object is not selected
     */
    public function getAvailableAddons(): array
    {
        self::requireSelected();
        $available_addons = (new product_options_o())->getProductOptions((int)$this->type->value());
        // Filter the addons, to only show the ones that has "subscription_allowed" set to true
        $available_addons = array_filter($available_addons, function ($addon) {
            return (bool)$addon['product']['subscription_allowed'];
        });
        return array_map(function ($addon) {
            return (new product_options_o())->select((int)$addon['id']);
        }, $available_addons);
    }

    /**
     * Get the last order id for the vehicle
     * @return int|null
     * @throws Exception If the object is not selected
     * @throws Exception If the there is an error selecting the order
     */
    private function getLastOrderId(): ?int
    {
        self::requireSelected();
        $orders_o = (new orders_o());
        $orders = $orders_o->getFieldsWhere([
            'reg_1' => (string)$this->reg->value(),
            'deleted_at' => null,
        ], [
            'id',
        ]);
        if (count($orders) > 0) {
            $last_order = array_pop($orders);
            return (int)$last_order['id'];
        }
        return null;
    }

    /**
     * Get last order with order items for plate (supports unregistered vehicles)
     * @param string $reg The registration number of the vehicle
     * @return orders_o|null The last order object for the vehicle, or null if no orders found
     * @throws Exception If there is an error selecting the order
     */
    public function getLastOrderByPlate(string $reg): ?orders_o
    {
        $orders_o = (new orders_o());
        $orders = $orders_o->getFieldsWhere([
            'reg_1' => $reg,
            'deleted_at' => null,
        ], [
            'id',
        ]);
        // Find the last order with order items
        while (count($orders) > 0) {
            $last_order = array_pop($orders);
            $order = (new orders_o())->select((int)$last_order['id']);
            if ($order->exists() && count((new order_items_o())->getFieldsWhere([
                    'order_id' => (int)$order->id,
                    'deleted_at' => null,
                ], ['id'])) > 0) {
                return $order;
            }
        }
        return null;
    }

    /**
     * Check if the vehicle has an XLVask object
     * @return bool
     * @throws Exception If the object is not selected
     * @note This method checks if the vehicle is registered in the XLVask system.
     */
    public function hasXLVask(): bool
    {
        self::requireSelected();
        $xlvask = new xlvask();
        $vehicles = new $xlvask->helpers->xlvask_vehicles();
        /** @var xlvask_vehicles $vehicles */
        return $vehicles->getVehicleByRegistrationNumber((string)$this->reg->value(), true) !== null;

    }

    /**
     * Get the XLVask object for the vehicle
     * @return xlvask_vehicle
     * @throws Exception If the object is not selected
     */
    public function getXLVask(): xlvask_vehicle
    {
        self::requireSelected();
        $xlvask = new xlvask();
        $vehicles = new $xlvask->helpers->xlvask_vehicles();
        /** @var xlvask_vehicles $vehicles */
        return $vehicles->getVehicleByRegistrationNumber((string)$this->reg->value());
    }

    /**
     * Get the vehicle types applicable to the vehicle (Wash types)
     * @return array{xlvask_vehicle_type}
     * @note This method retrieves the vehicle types from the XLVask system that are applicable to the vehicle.
     * @return array of xlvask_vehicle_type objects
     * @throws Exception If the object is not selected or if there is an error retrieving the vehicle types
     * @see xlvask_vehicle_type
     */
    public function getVehicleTypes(): array
    {
        self::requireSelected();
        return (new xlvask())->helpers->xlvask_vehicle_types::getVehicleTypesByProductId(
            (int)$this->type->value()
        );
    }

    /**
     * Add the default addons to the vehicle
     * @return void
     * @throws Exception If the object is not selected
     * @throws Exception If the there is an error adding the addon
     * @throws Exception If the there is an error selecting the addon
     * @throws Exception If the there is an error selecting the vehicle
     */
    public function addDefaultAddons(): void
    {
        self::requireSelected();
        // If any of the available addons have the product id in the $automatic_addon_product_ids array, add it to the vehicle
        $automatic_addon_product_ids = [
            23, // Spot Free- Varevogn
            24, // Spot Free- Lastbil
        ];
        $available_addons = self::getAvailableAddons();
        /** @var product_options_o $addon */
        foreach ( $available_addons as $addon ) {
            //echo 'Vehicle id: ' . $this->id . PHP_EOL;
            //echo 'Addon id: ' . $addon->id . PHP_EOL;
            //echo 'Primary product id: ' . $addon->product_id->value() . PHP_EOL;
            //echo 'Addon product id: ' . $addon->option_id->value() . PHP_EOL;
            //echo 'Is in array: ' . (in_array((int)$addon->option_id->value(), $automatic_addon_product_ids) ? 'true' : 'false') . PHP_EOL;
            //print_r($available_addons);
            if (in_array((int)$addon->option_id->value(), $automatic_addon_product_ids)) {
                // Add the addon to the vehicle
                (new customer_vehicles_addons_o())->add(
                    (int)$this->id,
                    (int)$addon->id,
                    (int)1,
                );
            }
        }
    }


    /**
     * @throws Exception If the creation of the object fails
     */
    public function add(int $customer_number, int $type, string $reg, bool $subscription, string|null $reference = null): void
    {
        global $db, $response;
        $tmp_id = self::add_object([
            'customer_id' => (int)$customer_number,
            'type' => (int)$type,
            'reg' => (string)$reg,
            'wash_subscription' => (bool)$subscription ? 1 : 0,
            'reference' => ($reference ? (string)$reference : null),
        ]);
        self::select((int)$tmp_id);
        self::objectChanged();
        self::addDefaultAddons();
    }

    public function objectChanged(): void
    {
        // Clear the cache for the object
        $this->deleteCached('asArray', $this->id);
    }

    public function structure(): void
    {
        $this->setTable('customer_vehicles');
    }

    public function getObjectProperties(): void
    {
        $this->customer_id = new object_property($this->table, $this->id, 'customer_id', 'int', true);
        $this->type = new object_property($this->table, $this->id, 'type', 'int', true);
        $this->reg = new object_property($this->table, $this->id, 'reg', 'string', true);
        $this->reference = new object_property($this->table, $this->id, 'reference', 'string', false);
        $this->wash_subscription = new object_property($this->table, $this->id, 'wash_subscription', 'bool', false);
        $this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'timestamp', false);
    }

    /**
     * Get the customer object for the vehicle
     * @param string $reg The registration number of the vehicle
     * @return int|null The customer number for the vehicle, or null if the vehicle is not registered to a customer
     * @throws Exception If the customer_id does not exist
     */
    public function getPlateCustomerNumber(string $reg): int|null
    {
        // Get the customer id from the vehicle
        $result = self::getFieldsWhere([
            'reg' => $reg,
        ], [
            'customer_id',
        ]);
        // If the customer id is not found, return null
        if (count($result) === 0) {
            return null;
        }
        // If the customer id is found, return it
        return (int)$result[0]['customer_id'];
    }

    /**
     * Get the plate reference if it exists, otherwise return a fallback value
     * @param string $RegistrationNumber The registration number of the vehicle
     * @param mixed $fallback The fallback value to return if the vehicle does not exist or has no reference
     * @throws Exception
     */
    public function getPlateReferenceIfExists(string $RegistrationNumber, mixed $fallback = ''): mixed
    {
        $vehicle = self::selectByPlate($RegistrationNumber);
        if (!$vehicle->exists()) {
            return $fallback; // If the vehicle does not exist, return the fallback value
        }
        // If the vehicle has a reference, return it
        if ($vehicle->reference->value() !== null) {
            return (string)$vehicle->reference->value();
        }
        // If the vehicle does not have a reference, return the fallback value
        return $fallback;
    }

    /**
     * @throws Exception
     */
    public function selectByPlate(string $reg): customer_vehicles_o
    {
        $result = self::getFieldsWhere([
            'reg' => $reg,
        ], [
            'id',
        ]);
        if (count($result) === 0) {
            return new customer_vehicles_o();
        }
        return (new customer_vehicles_o())->select((int)$result[0]['id']);
    }

    /**
     * Set the vehicle type id for the vehicle in the XLVask system
     * @param string $vehicleTypeId The vehicle type id to set in the XLVask system
     * @note This method updates the vehicle type in the XLVask system for the vehicle.
     * @throws Exception If the associated customer is not registered in the XLVask system
     * @throws Exception If the vehicle is not registered in the XLVask system
     * @throws Exception If the vehicle type id is not valid or if there is an error updating the vehicle type in the XLVask system
     * @throws Exception If the object is not selected
     * @see xlvask_vehicle
     * @example
     * $vehicle = new customer_vehicles_o();
     * $vehicle->select(1); // Select the vehicle with id 1
     * $vehicleTypeId = 'some-vehicle-type-id'; // The vehicle type id to set
     * $vehicle->setVehicleTypeId($vehicleTypeId);
     * @see xlvask_vehicle_type
     */
    public function setVehicleTypeId(string $vehicleTypeId): void
    {
        self::requireSelected();
        $xlvask = new xlvask();
        // Check if the vehicle is registered in the XLVask system
        if (!$this->hasXLVask()) {
            // Create a new XLVask vehicle
            throw new Exception('Vehicle is not registered in the XLVask system. Please register the vehicle first.');
        }
        // Set the vehicle type id
        $xlvask_vehicle = $this->getXLVask();
        $xlvask_vehicle->vehicleTypeId = $vehicleTypeId;
        // Update the XLVask vehicle
        $xlvask->updateVehicle($xlvask_vehicle);
        // Cache the updated vehicle
        $xlvask->getCache()->setVehicleCache($xlvask_vehicle->registrationNumber, $xlvask_vehicle);
        $this->objectChanged();
    }

    /**
     * Create a new vehicle in the XLVask system
     * * @param string $registrationNumber The registration number of the vehicle
     * @param int $customerId The customer id of the vehicle
     * @param string|null $vehicleTypeId The vehicle type id of the vehicle, if not provided, the default vehicle type will be used
     * @return xlvask_vehicle The created XLVask vehicle object
     * @throws Exception If the customer is not registered in the XLVask system
     * @throws Exception If the vehicle is already registered in the XLVask system
     * @throws Exception If the vehicle type id is not valid or if there is an error creating the vehicle in the XLVask system
     * @throws Exception If the object is not selected
     * @note This method creates a new vehicle in the XLVask system for the vehicle.
     * @see xlvask_vehicle
     * @see xlvask_vehicle_type
     * @example
     * $vehicle = new customer_vehicles_o();
     * $vehicle->select(1); // Select the vehicle with id 1
     * $vehicleTypeId = 'some-vehicle-type-id'; // The vehicle type id to set, if not provided, the default vehicle type will be used
     * $vehicle->createXLVaskVehicle($vehicleTypeId);
     */
    public function createXLVaskVehicle(string $vehicleTypeId): xlvask_vehicle
    {
        self::requireSelected();
        $xlvask = new xlvask();
        // Check if the customer is registered in the XLVask system
        $xlvask_customer = $this->getXLVaskCustomer();
        // Make sure the vehicle type id is valid for this vehicle type
        $valid_vehicle_types = $xlvask->helpers->xlvask_vehicle_types::getVehicleTypesByProductId(
            (int)$this->type->value()
        );
        if (!in_array($vehicleTypeId, array_map(fn($type) => $type->vehicleTypeId, $valid_vehicle_types))) {
            throw new Exception('Invalid vehicle type id provided. Please provide a valid vehicle type id for this vehicle type.');
        }
        // Create a new XLVask vehicle
        $xlvask_vehicle = new xlvask_vehicle();
        $xlvask_vehicle->registrationNumber = (string)$this->reg->value();
        $xlvask_vehicle->customerId = $xlvask_customer->customerId;
        $xlvask_vehicle->vehicleTypeId = $vehicleTypeId;
        $xlvask_vehicle->active = true; // Set the vehicle as active
        $xlvask_vehicle->autoStartOnLpr = true; // Enable auto start on LPR
        // Add the vehicle to the XLVask system
        $xlvask->createVehicle($xlvask_vehicle);
        // Cache the vehicle
        $vehicles = $xlvask->helpers->new($xlvask->helpers->xlvask_vehicles);
        /** @var xlvask_vehicles $vehicles */
        $xlvask_vehicle = $vehicles::getVehicleByRegistrationNumber((string)$this->reg->value());
        $xlvask->getCache()->setVehicleCache($xlvask_vehicle->registrationNumber, $xlvask_vehicle);
        $this->objectChanged();
        return $xlvask_vehicle;
    }

    /**
     * Get the XLVask customer object for the vehicle
     * @return xlvask_customer The XLVask customer object for the vehicle
     * @throws Exception If the object is not selected
     * @note This method retrieves the XLVask customer object for the vehicle.
     */
    public function getXLVaskCustomer(): xlvask_customer
    {
        self::requireSelected();
        $customer_number = (int)$this->customer_id->value();
        $xlvask = new xlvask();
        if ($xlvask->getCache()->isCustomerCached($customer_number)) {
            return $xlvask->getCache()->getCustomerCache($customer_number);
        }
        // If the customer is not cached, throw an exception
        throw new Exception('Customer is not registered in the XLVask system. Please register the customer first.');
    }

    /**
     * Set the auto start on LPR for the vehicle in the XLVask system
     * @param bool $autoStartOnLpr Whether to enable auto start on LPR for the vehicle
     * @throws Exception If the object is not selected
     * @note This method updates the auto start on LPR setting for the vehicle in the XLVask system.
     */
    public function setAutoStartOnLpr(bool $autoStartOnLpr): void
    {
        self::requireSelected();
        $xlvask_vehicle = $this->getXLVask();
        $xlvask_vehicle->autoStartOnLpr = $autoStartOnLpr;
        $xlvask = new xlvask();
        $xlvask->updateVehicle($xlvask_vehicle);
        // Cache the updated vehicle
        $xlvask->getCache()->setVehicleCache($xlvask_vehicle->registrationNumber, $xlvask_vehicle);
        $this->objectChanged();
    }

    /**
     * Get the first two transactions, that are not deleted, and contains the product of the vehicle type for the vehicle in the list of transaction ids
     * @param array $transaction_ids [ 1, 2, 3 ]
     * @return array An array of transaction ids
     * @throws Exception If the object is not selected
     * @note This method retrieves the first two transactions that are not deleted and contain the product of the vehicle type for the vehicle in the provided list of transaction ids.
     * The transactions are ordered by creation date in descending order.
     * If no transactions are found, an empty array is returned.
     * If only one transaction is found, an array with one transaction id is returned.
     * If two transactions are found, an array with two transaction ids is returned.
     * @example
     * $vehicle = new customer_vehicles_o();
     * $vehicle->select(1); // Select the vehicle with id 1
     * $transaction_ids = [1, 2, 3, 4, 5]; // The list of transaction ids to check
     * $result = $vehicle->getSubscriptionAppliedTransactionsFromList($transaction_ids);
     * print_r($result); // Array of transaction ids
     */
    public function getSubscriptionAppliedTransactionsFromList(array $transaction_ids): array
    {
        global $db;
        self::requireSelected();
        if (count($transaction_ids) === 0) {
            return [];
        }
        //print_r($transaction_ids);
        // Convert the array of transaction ids to a comma separated string
        $orders = '';
        foreach ($transaction_ids as $transaction_id) {
            // Check if the transaction is included in the invoicing.
            $tmp = (new orders_o())->select((int)$transaction_id);
            if (!$tmp->isIncludedInInvoicing()) {
                continue; // The transaction is not included in the invoicing, skip it
            }
            $orders .= (int)$transaction_id . ',';
        }
        // Remove the last comma
        $orders = rtrim($orders, ',');
        // Get the first two transactions that are not deleted and contains at least one order item with the 'product_id' of the vehicle type for the vehicle
        $query = "
            SELECT o.id
            FROM orders o
            JOIN order_items oi ON oi.order_id = o.id
            WHERE o.deleted_at IS NULL
            AND oi.deleted_at IS NULL
            AND o.id IN ($orders)
            AND oi.product_id = " . (int)$this->type->value() . "
            GROUP BY o.id
            ORDER BY o.created_at ASC
            LIMIT 2
        ";
        //echo $query . PHP_EOL;
        /** @var mysqli_result $result */
        $result = $db->query($query);
        $transaction_ids = $db->fetch_all($result);
        return array_map(fn($transaction) => (int)$transaction['id'], $transaction_ids);
    }

    /**
     * @throws Exception
     */
    public function getLastTransactions(int $numberOfTransactions = 5): array
    {
        self::requireSelected();
        $orders_o = new orders_o();
        $orders = $orders_o->getFieldsWhere([
            'reg_1' => (string)$this->reg->value(),
            'deleted_at' => null,
        ], [
            'id',
        ], [
            'created_at' => 'DESC',
        ], $numberOfTransactions);
        return array_map(fn($order) => (int)$order['id'], $orders);
    }
}