<?php

namespace objects;

use classes\db;
use classes\language_packs;
use classes\object_property;
use classes\redis;
use classes\response;
use classes\xlvask;
use customers\economic_customer_mo;
use customers\economicCustomers;
use Exception;
use languages\language_pack_en_us;
use traits\db_object_t;

class users_o extends db
{
    use db_object_t;

    public object_property $customer_number;
    public object_property $display_name;
    public object_property $group_id;
    public economic_customer_mo $economic_customer;
    public object_property $created_at;
    public object_property $updated_at;
    public array $permissions;
    public array $attributes;
    public array $discounts;
    public array $orders_not_invoiced;
    public array $all_keys;
    public customer_codes_o $customer_codes;
    public user_key_value_pairs_o $keys;
    public user_price_overrides_o $price_overrides;
    public language_pack_en_us $language_pack;
    public object_property $xlvask_customer_id;
    protected object_property $password;
    public object_property $phone_country_code;
    public object_property $phone;
    public object_property $email;
    public object_property $sms_notifications_enabled;
    public object_property $email_notifications_enabled;
    public object_property $wash_certificate_email; // Optional
    protected array $wash_subscription_transactions;


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


    public function edit(int $id, string $customer_number, string|null $role, string|null $password, string|null $display_name): void
    {
        global $db;
        $this->id = $id;
        // Avoid SQL injection
        $customer_number = $db->escape_string($customer_number);
        if ($password !== null) {
            // Hash the password
            $password = password_hash($password, PASSWORD_DEFAULT);
            $password = $db->escape_string($password);
        }
        if ($role !== null) {
            $role = $db->escape_string($role);
        }
        if ($display_name !== null) {
            $display_name = $db->escape_string($display_name);
        }
        // Update the record in the database
        $sql = "UPDATE $this->table SET customer_number = '$customer_number'";
        if ($password !== null) {
            $sql .= ", password = '$password'";
        }
        if ($role !== null) {
            $sql .= ", group_id = '$role'";
        }
        if ($display_name !== null) {
            $sql .= ", display_name = '$display_name'";
        }
        $sql .= " WHERE id = $this->id";
        $db->query($sql);

        // Set the values of the object properties
        $this->getObjectProperties();
    }

    public function getObjectProperties(): void
    {
        $this->customer_number = new object_property($this->table, $this->id, 'customer_number', 'string', true);
        $this->display_name = new object_property($this->table, $this->id, 'display_name', 'string', false);
        $this->password = new object_property($this->table, $this->id, 'password', 'string', true);
        $this->group_id = new object_property($this->table, $this->id, 'group_id', 'int', true);
        $this->phone_country_code = new object_property($this->table, $this->id, 'phone_country_code', 'int', false);
        $this->phone = new object_property($this->table, $this->id, 'phone', 'int', false);
        $this->email = new object_property($this->table, $this->id, 'email', 'string', false);
        $this->xlvask_customer_id = new object_property($this->table, $this->id, 'xlvask_customer_id', 'string', false);
        $this->created_at = new object_property($this->table, $this->id, 'created_at', 'string', false);
        $this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'string', false);
        $this->keys = (new user_key_value_pairs_o())->setUser($this->id);
        $this->price_overrides = (new user_price_overrides_o())->setUser($this->id);
        $this->sms_notifications_enabled = new object_property($this->table, $this->id, 'sms_notifications_enabled', 'bool', false);
        $this->email_notifications_enabled = new object_property($this->table, $this->id, 'email_notifications_enabled', 'bool', false);
        $this->wash_certificate_email = new object_property($this->table, $this->id, 'wash_certificate_email', 'string', false);

        $this->renderLanguagePack();
    }

    /**
     * Render the language pack of the user
     * @return self
     */
    private function renderLanguagePack(): self
    {
        global /** @var response $response */
        $response;
        // Check if the user has a key called 'language_pack'
        if ($this->keys->getValue('language_pack') !== null) {
            // Get the language pack of the user
            $language_pack = $this->keys->getValue('language_pack');
            $this->language_pack = (new language_packs())->getLanguagePack($language_pack);
        } else {
            // Get the default language pack
            $this->language_pack = (new language_packs())->getDefaultLanguagePack();
            //$response->add_debug(['message' => 'No language pack found for user, using default language pack']);
        };
        //$response->add_meta('language_pack', $this->language_pack);
        return $this;
    }

    public function getLanguagePack(): object
    {
        // Get the language pack of the user
        return new $this->language_pack();
    }


    private function importCustomerFromExternalSource(int $customer_number): object|bool
    {
        global $db;
        // Get the customer data from the external source
        $economic = new economicCustomers();
        $customer_data = $economic->getCustomerId($customer_number);
        // DEBUG: Return the customer data
        // Check if the customer exists
        if ($customer_data) {
            // Avoid SQL injection
            $customer_number = $db->escape_string($customer_data->customerNumber);
            // Double check if the customer exists
            $sql = "SELECT * FROM $this->table WHERE customer_number = '$customer_number'";
            $result = $db->query($sql);
            if ($result->num_rows > 0) {
                $this->id = $result->fetch_assoc()['id'];
                $this->getObjectProperties();
            } else {
                // Import the customer
                $this->add($customer_number, '', 0);
                // Nullify the password
                $this->password->nullify();
                // If the customer has an email address, save it
                if (isset($customer_data->email)) {
                    $this->email->set($customer_data->email);
                }
                // If the customer has a name, save it as the display name
                if (isset($customer_data->name)) {
                    $this->display_name->set($customer_data->name);
                }
            }
        }
        // Else return false
        return false;
    }

    /**
     * @throws Exception
     */
    public function wantsSmsNotifications(): bool
    {
        self::requireSelected();
        return (bool)$this->sms_notifications_enabled->value();
    }

    /**
     * @throws Exception
     */
    public function wantsEmailNotifications(): bool
    {
        self::requireSelected();
        return (bool)$this->email_notifications_enabled->value();
    }

    /**
     * @throws Exception
     */
    public function getWashCertificateEmail(): string|null
    {
        self::requireSelected();
        return $this->wash_certificate_email->value();
    }


    public function add(string $customer_number, mixed $password, int $role = 0): void
    {
        global $db;
        // Avoid SQL injection
        $customer_number = $db->escape_string($customer_number);
        $role = $db->escape_string($role);
        // Hash the password
        $password = password_hash($password, PASSWORD_DEFAULT);
        $password = $db->escape_string($password);
        // Create a new record in the database
        $sql = "INSERT INTO $this->table (customer_number, password, group_id) VALUES ('$customer_number', '$password', $role)";
        $db->query($sql);

        // Get the id of the new record
        $this->id = $db->insert_id();

        // Set the values of the object properties
        $this->getObjectProperties();
        // Set the default attributes
        //$this->addAttribute('invoiceAllOrdersIndividually');
        $this->addAttribute('restrictTankCleaning');
    }

    /**
     * Add an attribute to the user
     * @param string $attribute The attribute to add
     * @param int|null $user_id The user id to add the attribute to
     * @throws Exception If the user is not selected, and the user_id is null
     */
    public function addAttribute(string $attribute, int $user_id = null): void
    {
        global $db;
        if ($user_id === null) {
            self::requireSelected();
            $user_id = $this->id;
        }
        $attribute = $db->escape_string($attribute);
        // To prevent two attributes with the same name for the same user, we will delete the old one if it exists
        $this->deleteAttribute($attribute, $user_id);
        $sql = "INSERT INTO customer_attributes (user_id, attribute) VALUES ($user_id, '$attribute')";
        $db->query($sql);
    }

    public function deleteAttribute(string $attribute, int $user_id = null): void
    {
        global $db;
        if ($user_id === null) {
            $user_id = $this->id;
        }
        $attribute = $db->escape_string($attribute);
        // If the user has a customer number, we will delete the attribute from all associated customers
        $customer_number = $this->customer_number->value();
        if (!empty($customer_number)) {
            $sql = "SELECT * FROM users WHERE customer_number = $customer_number";
            $result = $db->query($sql);
            $user_ids = [];
            while ($row = $result->fetch_assoc()) {
                $user_ids[] = (int)$row['id'];
            }
            // Delete the attribute from all users
            foreach ($user_ids as $user_id) {
                $sql = "DELETE FROM customer_attributes WHERE user_id = $user_id AND attribute = '$attribute' LIMIT 1";
                $db->query($sql);
            }
            return;
        }
        // Make sure the attribute exists
        if (!$this->doesUserHaveAttribute((string)$attribute, (int)$user_id)) {
            return;
        }
        // If the attribute exists, delete it, if it does not exist, nothing will happen
        $sql = "DELETE FROM customer_attributes WHERE user_id = $user_id AND attribute = '$attribute' LIMIT 1";
        $db->query($sql);
    }

    public function doesUserHaveAttribute(string $attribute, int $user_id = null): bool
    {
        global $db;
        if ($user_id === null) {
            $user_id = $this->id;
        }
        $attribute = $db->escape_string($attribute);
        $sql = "SELECT * FROM customer_attributes WHERE user_id = $user_id AND attribute = '$attribute' LIMIT 1";
        $result = $db->query($sql);
        if ($result->num_rows > 0) {
            return true;
        }
        return false;
    }

    public function automaticGetTargetUserFromRequest(): users_o
    {
        // Get the data from the request
        if ($_SERVER['REQUEST_METHOD'] === 'GET' || $_SERVER['REQUEST_METHOD'] === 'DELETE') {
            $data = $_GET;
        } else {
            $data = json_decode(file_get_contents('php://input'), true);
        }
        // Check if the user id is set in the request
        if (isset($data['user_id'])) {
            return $this->getUserById((int)$data['user_id']);
        } elseif (isset($data['customer_number'])) {
            return $this->getUserByCustomerNumber($data['customer_number']);
        } else {
            return $this;
        }
    }

    public function getUserById(int $id): users_o
    {
        global $db;
        // Get the record from the database
        $sql = "SELECT * FROM $this->table WHERE id = $id";
        $result = $db->query($sql);
        if ($result->num_rows > 0) {
            $this->id = $id;
            $this->getObjectProperties();
        }
        return $this;
    }

    public function getUserByCustomerNumber(int $customer_number): users_o
    {
        global $db;
        // Get the record from the database
        $sql = "SELECT * FROM $this->table WHERE customer_number = '$customer_number'";
        $result = $db->query($sql);
        if ($result->num_rows > 0) {
            $this->id = $result->fetch_assoc()['id'];
            $this->getObjectProperties();
        } else {
            // Import the customer
            $this->importCustomerFromExternalSource($customer_number);
        }
        return $this;
    }

    public function asArray(): array
    {
        $phone_country_code = $this->phone_country_code->value();
        $phone_country_code = $phone_country_code === null ? null : (int)$phone_country_code;
        $phone = $this->phone->value();
        $phone = $phone === null ? null : (int)$phone;
        $display_name = $this->display_name->value();
        $customer_number = (int)$this->customer_number->value();
        $customer_name = $this->getCustomerName($customer_number);
        // Create the array with the object properties
        $array = [
            'id' => (int)$this->id,
            'customer_number' => $customer_number,
            'customer_name' => $customer_name,
            'display_name' => $display_name === 'Unnamed' ? $customer_name : $display_name,
            'group_id' => (int)$this->group_id->value(),
            'phone' => [
                'country_code' => $phone_country_code,
                'number' => $phone,
            ],
            'email' => $this->email->value(),
            'notifications' => [
                'sms_notifications_enabled' => (bool)$this->sms_notifications_enabled->value(),
                'email_notifications_enabled' => (bool)$this->email_notifications_enabled->value(),
                'wash_certificate_email' => $this->wash_certificate_email->value(),
            ],
            'created_at' => $this->created_at->value(),
            'updated_at' => $this->updated_at->value(),
        ];
        // If the economic customer data is set, add it to the array
        if (isset($this->economic_customer)) {
            $array['economic_customer'] = $this->economic_customer->asArray();
        }
        // If the permissions are set, add them to the array
        if (isset($this->permissions)) {
            $array['permissions'] = $this->permissions;
        }
        // If the attributes are set, add them to the array
        if (isset($this->attributes)) {
            $array['attributes'] = $this->attributes;
        }
        // If the discounts are set, add them to the array
        if (isset($this->discounts)) {
            $array['discounts'] = $this->discounts;
        }
        // If the orders not invoiced are set, add them to the array
        if (isset($this->orders_not_invoiced)) {
            $array['orders_not_invoiced'] = $this->orders_not_invoiced;
        }
        // If the keys are set, add them to the array
        if (isset($this->all_keys)) {
            $array['keys'] = $this->all_keys;
        }
        // If the wash subscription transactions are set, add them to the array
        if (isset($this->wash_subscription_transactions)) {
            $array['wash_subscription_transactions'] = $this->wash_subscription_transactions;
        }
        return $array;
    }

    /**
     * @throws Exception
     */
    public function getCustomerName(int $customer_number): string|null
    {
        // Get the customer from the customer object
        if ($customer_number === 0) {
            return null;
        }
        // Create a temporary user object
        $tmp_user = new users_o();
        $tmp_user->getUserByCustomerNumber($customer_number);
        // Get the customer name (Check cache first)
        $cached = $tmp_user->getCached('economic_customer');
        if (!$cached) {
            $tmp_user->getCustomerEcocomicData($customer_number);
            $cached = $tmp_user->getCached('economic_customer');
        }
        if ($cached) {
            return $cached->name;
        }
        return null;
    }

    public function getCustomerEcocomicData(int $customer_number = null): users_o
    {
        // Get the customer data from the external source
        $economic = new economicCustomers();
        // Check if the customer number is set
        if (!isset($this->customer_number) && $customer_number === null) {
            return $this;
        }
        $customer_number = $customer_number ?? $this->customer_number->value();
        $this->economic_customer = (new economic_customer_mo())->getCustomerByCustomerNumber($customer_number);
        return $this;
    }

    public function getNotes(): array
    {
        global $db;
        // Create the customer notes object
        $customer_notes = new customer_notes_o();
        // Get the customer notes
        return $customer_notes->getCustomerNotesAsArray($this->id);
    }

    public function addNote($customer_id, $note, $cashier_id): customer_notes_o
    {
        global $db;
        // Create the customer notes object
        $customer_notes = new customer_notes_o();
        // Add the note
        $customer_notes->add($customer_id, $note, $cashier_id);
        return $customer_notes;
    }

    public function deleteNote(int $note_id): void
    {
        global $db;
        // Create the customer notes object
        $customer_notes = new customer_notes_o();
        // Delete the note
        $customer_notes->delete($note_id);
    }

    public function getOrImportCustomerByCustomerNumber(int $customer_number): object|bool
    {
        global $db;
        // Check if the customer exists
        $sql = "SELECT * FROM $this->table WHERE customer_number = $customer_number";
        $result = $db->query($sql);
        if ($result->num_rows > 0) {
            $this->id = $result->fetch_assoc()['id'];
            $this->getObjectProperties();
        } else {
            // Import the customer
            return $this->importCustomerFromExternalSource($customer_number);
        }
        return $this;
    }

    /**
     * If the customer requires a reference number for each order
     * @return bool
     */
    public function requiresReference(): bool
    {
        return $this->doesUserHaveAttribute('requiresReferenceNumber');
    }

    /**
     * If the customer is NOT allowed to purchase spot free washes
     * @return bool
     */
    public function restrictSpotFree(): bool
    {
        return $this->doesUserHaveAttribute('restrictSpotFree');
    }

    /**
     * If the customer is NOT allowed to purchase interior cleaning
     * @return bool
     */
    public function restrictInteriorCleaning(): bool
    {
        return $this->doesUserHaveAttribute('restrictInteriorCleaning');
    }

    /**
     * If the customer is NOT allowed to purchase tank cleaning
     * @return bool
     */
    public function restrictTankCleaning(): bool
    {
        return $this->doesUserHaveAttribute('restrictTankCleaning');
    }

    /**
     * If the customer is shown prices on the booking page
     * @return bool
     */
    public function showPricesOnBookingPage(): bool
    {
        return $this->doesUserHaveAttribute('showPricesOnBookingPage');
    }

    /**
     * If the customer uses PO numbers
     * @return bool
     */
    public function usePONumbers(): bool
    {
        return $this->doesUserHaveAttribute('usePONumbers');
    }

    public function includeIncludes(array $includes = []): users_o
    {
        global /** @var response $response */
        $response;
        $includeEverything = $response->getRequestParameter('include_all') === 'true' || in_array('all', $includes);
        /**
         * economicCustomer
         */
        if ($includeEverything || $response->getRequestParameter('includeEconomicCustomer') === 'true' || in_array('economicCustomer', $includes)) {
            $this->getCustomerEcocomicData();
        }
        /**
         * permissions
         */
        if ($includeEverything || $response->getRequestParameter('includePermissions') === 'true' || in_array('permissions', $includes)) {
            $this->getPermissions();
        }
        /**
         * Attributes
         */
        if ($includeEverything || $response->getRequestParameter('includeAttributes') === 'true' || in_array('attributes', $includes)) {
            $this->getUserAttributes();
        }
        /**
         * Discounts
         */
        if ($includeEverything || $response->getRequestParameter('includeDiscounts') === 'true' || in_array('discounts', $includes)) {
            $this->getAllDiscounts();
        }
        /**
         * OrdersNotInvoiced
         */
        if ($includeEverything || $response->getRequestParameter('includeOrdersNotInvoiced') === 'true' || in_array('ordersNotInvoiced', $includes)) {
            $this->getUserOrdersNotInvoiced();
        }
        /**
         * Key value pairs
         */
        if ($includeEverything || $response->getRequestParameter('includeKeys') === 'true' || in_array('keys', $includes)) {
            $this->getAllKeys();
        }
        /**
         * Wash subscription transactions
         */
        if ($includeEverything || $response->getRequestParameter('includeWashSubscriptionTransactions') === 'true' || in_array('washSubscriptionTransactions', $includes)) {
            $this->getWashSubscriptionTransactions();
        }
        return $this;
    }

    private function getPermissions(): void
    {
        global $db;
        $sql = "SELECT permission FROM groups_permissions WHERE group_id = " . $this->group_id->value();
        $result = $db->query($sql);
        $perms = [];
        while ($row = $result->fetch_assoc()) {
            $perms[] = $row['permission'];
        }
        // Add the attributes as `has_attribute_key_{attribute_key}`
        $attributes = array_map(function ($attribute_arr) {
            return $attribute_arr['attribute'];
        }, (new users_o())->getUserAttributes($this->id));
        // Remove all duplicates
        $attributes = array_unique($attributes, SORT_REGULAR);
        foreach ($attributes as $attribute) {
            $perms[] = 'has_attribute_' . $attribute;
        }
        $this->permissions = $perms;
    }

    public function getUserAttributes(int $user_id = null): array
    {
        global $db;
        if ($user_id === null) {
            $user_id = $this->id;
        }
        $sql = "SELECT * FROM customer_attributes WHERE user_id = $user_id";
        $result = $db->query($sql);
        $array = $db->fetch_all($result);
        $this->attributes = $array;
        return $this->attributes;
    }

    /**
     * Get all discounts for the user (Include)
     * @return void
     */
    public function getAllDiscounts(): void
    {
        // Get all discounts (key = 'custom_price')
        $this->discounts = $this->price_overrides->setUser($this->id)->getAllPrices();
    }

    public function getUserOrdersNotInvoiced(): void
    {
        global $db;
        // $sql = "SELECT * FROM $this->table WHERE deleted_at IS NULL AND id NOT IN (SELECT id FROM economic_module_orders WHERE invoice_draft_id IS NOT NULL OR invoice_id IS NOT NULL)";
        $sql = "SELECT id FROM orders WHERE customer_id = " . $this->customer_number->value() . " AND deleted_at IS NULL AND id NOT IN (SELECT id FROM economic_module_orders WHERE invoice_draft_id IS NOT NULL OR invoice_id IS NOT NULL)";
        $result = $db->query($sql);
        $this->orders_not_invoiced = $db->fetch_all($result);
    }

    public function getAllKeys(): void
    {
        // Get all keys (key = 'key_value_pair')
        $this->all_keys = $this->keys->setUser($this->id)->getAllKeys();
    }

    /**
     * Get the wash subscription transactions for the user
     * @throws Exception If the user is not selected
     */
    private function getWashSubscriptionTransactions(): void
    {
        self::requireSelected();
        $orders_o = new orders_o();
        $wash_subscription_transactions = $orders_o->getWashSubscriptionTransactions((int)$this->customer_number->value());
        $this->wash_subscription_transactions = $wash_subscription_transactions;
    }

    /**
     * Get the group of the user
     * @throws Exception If the user is not selected
     */
    public function getGroup(): groups_o
    {
        self::requireSelected();
        // Get the group of the user
        $group = new groups_o();
        $group->select((int)$this->group_id->value());
        return $group;
    }

    public function getCode(): string|null
    {
        // Get the customer code
        $this->customer_codes = new customer_codes_o();
        $this->customer_codes->getCode($this->id);
        return $this->customer_codes->code->value();
    }

    public function setCode(mixed $code): customer_codes_o
    {
        // Set the customer code
        $this->customer_codes = new customer_codes_o();
        return $this->customer_codes->setCode($this->id, $code);
    }

    /**
     * Check if the user has access to the order
     * @param int $order_id
     * @return bool
     */
    public function hasAccessToOrder(int $order_id): bool
    {
        global $db;
        // Check if the user has access to all orders
        if ($this->hasPermission('fetch_all_orders')) {
            return true;
        }
        // Get the record from the database
        $sql = "SELECT * FROM orders WHERE id = $order_id AND customer_id = " . $this->customer_number->value();
        $result = $db->query($sql);
        if ($result->num_rows > 0) {
            return true;
        }
        return false;
    }

    public function hasPermission(string $permission): bool
    {
        global $db;
        // Get the user's group id
        $group_id = $this->group_id->value();
        // If the users is an admin, they have all permissions
        if ((int)$group_id === 1) {
            return true;
        }
        // Get the record from the database
        $sql = "SELECT * FROM groups_permissions WHERE group_id = $group_id AND permission = '$permission'";
        $result = $db->query($sql);
        if ($result->num_rows > 0) {
            return true;
        }
        return false;
    }

    public function setOpenInvoiceDraft(int $draftInvoiceNumber): void
    {
        // Set the open invoice draft (key = 'open_invoice_draft')
        $this->keys->setUser($this->id)->setValue('open_invoice_draft', $draftInvoiceNumber);
    }

    public function deleteOpenInvoiceDraft(): void
    {
        // Check if the user has an open invoice draft
        if ($this->hasOpenInvoiceDraft()) {
            // Remove all links to the draft invoice
            $order = new economic_module_orders();
            $order->unlinkAllOrdersFromDraft($this->getOpenInvoiceDraft());
            // Unset the open invoice draft
            $this->unsetOpenInvoiceDraft();
        }
    }

    public function hasOpenInvoiceDraft(): bool
    {
        // Check if the user has an open invoice draft (key = 'open_invoice_draft')
        return $this->keys->setUser($this->id)->getValue('open_invoice_draft') !== null;
    }

    public function getOpenInvoiceDraft(): int|null
    {
        // Get the open invoice draft (key = 'open_invoice_draft')
        return (int)$this->keys->setUser($this->id)->getValue('open_invoice_draft');
    }

    public function unsetOpenInvoiceDraft(): void
    {
        // Unset the open invoice draft (key = 'open_invoice_draft')
        $this->keys->setUser($this->id)->deleteValue('open_invoice_draft');
    }

    /**
     * Get the custom price (DISCOUNT) for the user
     * @param int $object_id The ID of the object
     * @param bool $is_category If the object is a category
     * @return int|null The discount percentage
     */
    public function getCustomPrice(int $object_id, bool $is_category = false): int|null
    {
        self::requireSelected();
        // Get the custom price for the product
        $discount = $this->price_overrides->setUser($this->id)->getPrice($is_category, $object_id);
        // If the is_category is false, check if there is a custom price for the category that the product belongs to
        if ($discount === 0 && !$is_category) {
            $product = new products_o();
            // Get the product by ID
            $product->getProductById($object_id);
            // Check if the product allows category inheritance of discounts
            if ($product->apply_category_discount->value()) {
                // Get the custom price for the category
                $discount = $this->price_overrides->setUser($this->id)->getPrice(true, $product->category->value());
            }
        }
        return $discount;
    }

    /**
     * Get the final price of the product for the user
     * This includes:
     * - Global discount (If the product allows category inheritance of discounts)
     * - Category discount (If the product allows category inheritance of discounts)
     * - Product discount (If the product has a custom price)
     */
    public function getProductDiscountPercentage(int $product_id): int|null
    {
        self::requireSelected();
        // Get the product by ID
        $product = (new products_o())->select((int)$product_id);
        $doesProductAllowCategoryDiscount = (bool)$product->apply_category_discount->value();
        $economic_discount_percentage = $this->getEconomicCustomerDiscountPercentage();
        $discount_percentage = $this->getCustomPrice($product_id);
        // If the product allows category inheritance of discounts, get the category discounts
        if ($doesProductAllowCategoryDiscount) {
            // If the category discount is higher than the product discount, use the category discount
            if ($economic_discount_percentage > $discount_percentage) {
                $discount_percentage = $economic_discount_percentage;
            }
        }
        // If the discount percentage is null, return 0
        return $discount_percentage === null ? 0 : (int)$discount_percentage;
    }



    /**
     * Get all custom prices (DISCOUNT) for the user
     * @return array The custom prices
     */
    public function getCustomPrices(): array
    {
        // Get the custom prices (key = 'custom_price')
        return $this->price_overrides->setUser($this->id)->getAllPrices();
    }

    /**
     * Get all users in a group
     * @param int $group_id
     * @return array
     */
    public function getUsersInGroup(int $group_id): array
    {
        global $db;
        $sql = "SELECT id FROM $this->table WHERE group_id = $group_id";
        $result = $db->query($sql);
        // Create an array of user objects
        $users = [];
        while ($row = $result->fetch_assoc()) {
            $user = new users_o();
            $user->id = $row['id'];
            $user->getObjectProperties();
            $users[] = $user;
        }
        return $users;
    }

    /**
     * Get the employee data, that's public to the customers
     * @return array
     */
    public function listPublicEmployeeData(): array
    {
        return [
            'id' => (int)$this->id,
            'display_name' => $this->display_name->value(),
        ];
    }

    public function getUsersWithPermission(string $permission): array
    {
        global $db;
        $sql = "SELECT id FROM $this->table WHERE group_id IN (SELECT group_id FROM groups_permissions WHERE permission = '$permission')";
        $result = $db->query($sql);
        // Create an array of user objects
        $users = [];
        while ($row = $result->fetch_assoc()) {
            $user = new users_o();
            $user->id = $row['id'];
            $user->getObjectProperties();
            $users[] = $user;
        }
        return $users;
    }

    /**
     * Does the user have access to the booking?
     * @param int $id
     * @return bool
     */
    public function hasAccessToBooking(int $id): bool
    {
        global $db;
        // Check if the user has access to all bookings
        if ($this->hasPermission('list_bookings')) {
            return true;
        }
        // Get the record from the database
        $sql = "SELECT * FROM bookings WHERE id = $id AND customer_number = " . $this->customer_number->value();
        $result = $db->query($sql);
        if ($result->num_rows > 0) {
            return true;
        }
        return false;
    }

    public function parseUsers(array $listObjectsWithPaginationIfSet, $variables = []): array
    {
        $tmp = self::parseCustomerNumbers($listObjectsWithPaginationIfSet);
        // Check if the user has a password, and change it to a boolean instead of a string
        foreach ( $tmp as $key => $value ) {
            if ($value['password'] === '' || $value['password'] === null) {
                $tmp[$key]['password'] = false;
            } else {
                $tmp[$key]['password'] = true;
            }
        }
        return $tmp;
    }

    public function parseCustomerNumbers(array $listObjectsWithPaginationIfSet): array
    {
        foreach ( $listObjectsWithPaginationIfSet as $key => $value ) {
            $listObjectsWithPaginationIfSet[$key]['customer_name'] = $this->getCustomerNameById($value['id']);
        }
        return $listObjectsWithPaginationIfSet;
    }

    private function getCustomerNameById(int $id): ?string
    {
        // Check if the name is cached
        $cached_name = $this->getCached('economic_customer', $id);
        if (!$cached_name) {
            // No cached name, get the name from the external source
            // Get the name from the external source
            $tmp_user = new users_o();
            $tmp_user->getUserById($id);
            $tmp_user->getCustomerEcocomicData();
            $cached_name = $tmp_user->getCached('economic_customer');
            if (!$cached_name) {
                // Cache the NULL value
                $this->cache('economic_customer', 'NULL_OR_EMPTY', $id);
                return null;
            }
        }
        return $cached_name->name ?? null;
    }

    public function hasPassword(): bool
    {
        return $this->password->value() !== null;
    }

    public function getPassword(): string|null
    {
        return $this->password->value();
    }

    /**
     * Set the password for the user
     * @throws Exception If the user is not selected
     */
    public function setPassword(string $password = null): void
    {
        self::requireSelected();
        global $db;
        // Hash the password
        $password = password_hash($password, PASSWORD_DEFAULT);
        $password = $db->escape_string($password);
        // Update the password in the database
        $this->password->set(
            $password,
        );
        // The object has changed, so we need to update the database
        self::objectChanged();
    }

    public function objectChanged(): void
    {
        // Since the user object is not cached, there is no need to invalidate the cache
    }

    public function clearAllUsersEconomicCustomerDiscountsFromCache(): void
    {
        // Get all the cached results matching the pattern 'users_*_economic_customer_discount_percentage'
        $cached_results = redis->get_keys('users_*_economic_customer_discount_percentage');
        // Loop through the cached results
        foreach ( $cached_results as $key ) {
            // Clear the cached discount percentage
            redis->delete($key);
        }
    }

    public function clearAllUsersEconomicCustomerDetailsFromCache(): void
    {
        // Get all the cached results matching the pattern 'users_*_economic_customer'
        $cached_results = redis->get_keys('users_*_economic_customer');
        // Loop through the cached results
        foreach ( $cached_results as $key ) {
            // Clear the cached economic customer details
            redis->delete($key);
        }
    }

    public function getEconomicCustomerDiscountPercentage(): int
    {
        self::requireSelected();
        // Check if the discount percentage is cached
        $cached_discount_percentage = redis->get_economic_customer_discount_percentage($this->id);
        if ($cached_discount_percentage !== null) {
            return $cached_discount_percentage;
        }
        // Get the discount percentage from the external source
        $economic = new economicCustomers();
        $discount_percentage = $economic->getCustomerDiscountPercentage($this->customer_number->value());
        // Cache the discount percentage
        redis->cache_economic_customer_discount_percentage($this->id, $discount_percentage);
        return $discount_percentage;
    }

    /**
     * Set a custom price (DISCOUNT) for the user
     * @param int $user_id
     * @param int $object_id The ID of the object
     * @param int $discount_percentage The discount percentage
     * @param bool $is_category If the object is a category
     * @return void
     */
    public function setCustomPrice(int $user_id, int|string $object_id, int $discount_percentage, bool $is_category = false): void
    {
        $this->id = $user_id;
        // Get the user object properties
        $this->getObjectProperties();
        // Set the custom price (key = 'custom_price')
        $this->price_overrides->setUser($this->id)->setPrice($is_category, $object_id, $discount_percentage);
    }

    public function syncAllUsersEconomicCustomerDetails(): void
    {
        // Get all users
        $users = $this->getFields(['id', 'customer_number']);
        // Loop through the users
        foreach ( $users as $user ) {
            // Check if the customer number is set (Or if it is 0)
            if ($user['customer_number'] === 0) {
                continue;
            }
            // Get the customer data from the external source
            $this->getCustomerEcocomicData($user['customer_number']);
        }
    }

    public function isImportedFromEconomic($customerNumber): bool
    {
        // Check if the user is imported from the external source
        if (self::countRowsWhere(['customer_number' => $customerNumber]) > 0) {
            return true;
        }
        return false;
    }

    public function getUserIdFromEconomic($customerNumber): int
    {
        // Get the user id from the external source
        $user = self::getFieldsWhere(['customer_number' => $customerNumber], ['id']);
        return $user[0]['id'];
    }

    /**
     * Get the invoice collection ID, where the new order should be added to.
     * This is based on the user's settings. (If the user has the attribute 'invoiceAllOrdersIndividually', the new order should be added to a new invoice collection)
     * @return int The ID of the order invoice collection the new order should be added to.
     * @throws Exception
     */
    public function getNewOrderInvoiceCollectionId(): int
    {
        // Check if the user has the attribute 'invoiceAllOrdersIndividually'
        $invoice_collection = new collected_order_invoices_o();
        if ($this->invoicePerOrder() || !self::hasOpenInvoiceCollection()) {
            // Require the user to have a customer number above 0
            if (is_null($this->customer_number->value()) || $this->customer_number->value() === 0) {
                throw new Exception('The user does not have a customer number');
            }
            // The user has the attribute 'invoiceAllOrdersIndividually', create a new invoice collection
            $invoice_collection->add((int)$this->customer_number->value());
            return $invoice_collection->id;
        }
        // The user does not have the attribute 'invoiceAllOrdersIndividually', get the ID of the last invoice collection
        return self::getOpenInvoiceCollection()->id;
    }

    /**
     * If the customer should have an invoice per order (Otherwise, they will have a monthly invoice for all orders)
     * @return bool
     */
    public function invoicePerOrder(): bool
    {
        return $this->doesUserHaveAttribute('invoiceAllOrdersIndividually');
    }

    /**
     * Check if the user has an open invoice collection
     * @return bool
     */
    public function hasOpenInvoiceCollection(): bool
    {
        // Check if the user has an open invoice collection
        $invoice_collection = new collected_order_invoices_o();
        return $invoice_collection->hasOpenInvoiceCollection($this->customer_number->value());
    }

    /**
     * Get the open invoice collection of the user
     * @return collected_order_invoices_o
     * @throws Exception If the user does not have an open invoice collection
     * @throws Exception If the user does not have a customer number
     */
    public function getOpenInvoiceCollection(): collected_order_invoices_o
    {
        // Require the user to have a customer number above 0
        if (is_null($this->customer_number->value()) || $this->customer_number->value() === 0) {
            throw new Exception('The user does not have a customer number');
        }
        self::requireSelected();
        // Get the open invoice collection of the user
        $invoice_collection = new collected_order_invoices_o();
        return $invoice_collection->getLatestOpenInvoiceCollection($this->customer_number->value());
    }

    /**
     * Get all customers with the given attributes
     * @notation Retrieve all customers with ALL the given attributes
     * @param array $attributes The attributes to search for (e.g. ['attribute1', 'attribute2'])
     * @return array The e-conomic customer numbers with the given attributes (e.g. [123456, 654321])
     */
    public function getCustomerNumbersWithAttributes(array $attributes): array
    {
        global $db;
        // Create an array to store the customer numbers
        $user_ids = [];
        $customer_numbers = [];
        // Loop through the attributes
        foreach ( $attributes as $attribute ) {
            // Get the customer numbers with the attribute
            $sql = "SELECT user_id FROM customer_attributes WHERE attribute = '$attribute'";
            $result = $db->query($sql);
            // Loop through the results
            while ($row = $result->fetch_assoc()) {
                // Add the customer number to the array
                $user_ids[] = (int)$row['user_id'];
            }
        }
        // Remove duplicates from the array
        $user_ids = array_unique($user_ids);
        // Get the customer numbers from the user IDs
        foreach ( $user_ids as $user_id ) {
            // Get the customer number from the user ID
            $sql = "SELECT customer_number FROM $this->table WHERE id = $user_id";
            $result = $db->query($sql);
            // Loop through the results
            while ($row = $result->fetch_assoc()) {
                // Add the customer number to the array
                $customer_numbers[] = (int)$row['customer_number'];
            }
        }
        // Remove duplicates from the array
        // Return the customer numbers
        return array_unique($customer_numbers);
    }

    /**
     * Check if the password matches the hashed password
     * @param string $password_to_check_against The password (plain text) to check against the hashed password
     * @return bool True if the password matches, false otherwise
     * @throws Exception If the user is not selected
     * @throws Exception If the user does not have a password
     */
    public function passwordMatches(string $password_to_check_against): bool
    {
        // Require the user to be selected
        self::requireSelected();
        // Check if the user has a password
        if (empty($this->password->value())) {
            throw new Exception('The user does not have a password, unable to check if the password matches');
        }
        // Check if the password matches
        return password_verify(
            (string)$password_to_check_against,
            (string)$this->password->value()
        );
    }

    /**
     * Set the email for the user
     * @param string $email The email address to set
     * @throws Exception If the user is not selected
     * @throws Exception If the email address is invalid
     * @throws Exception If the email address is already in use
     */
    public function setEmail(string $email): void
    {
        self::requireSelected();
        // Check if the email is valid
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new Exception('Invalid email address');
        }
        // Check if the email is already in use
        if (self::countRowsWhere(['email' => $email]) > 0) {
            throw new Exception('Email address already in use');
        }
        // Set the email
        $this->email->set($email);
    }

    public function isCustomerBarred(int $customer_number): bool
    {
        if ($customer_number === 0) {
            return false;
        }
        // Create a temporary user object
        $tmp_user = new users_o();
        $tmp_user->getUserByCustomerNumber($customer_number);
        // Get the customer name (Check cache first)
        $cached = $tmp_user->getCached('economic_customer');
        if (!$cached) {
            $tmp_user->getCustomerEcocomicData($customer_number);
            $cached = $tmp_user->getCached('economic_customer');
        }
        if ($cached) {
            return (isset($cached->barred) && $cached->barred);
        }
        return false;
    }

    /**
     * @throws Exception
     */
    public function doesUserHaveDefaultDepartment(): bool
    {
        // Check if the user has a default department set
        self::requireSelected();
        return (new customer_default_department_o())->doesUserHaveDefaultDepartment((int)$this->customer_number->value());
    }

    /**
     * @throws Exception
     */
    public function getDefaultDepartment(): int|null
    {
        // Get the default department for the user
        self::requireSelected();
        return (new customer_default_department_o())->getDefaultDepartment((int)$this->customer_number->value());
    }

    public function getCustomersWithVehicleSubscriptions(): array
    {
        global $db;
        // Get all customers with vehicle subscriptions
        $sql = "SELECT DISTINCT customer_id FROM customer_vehicles WHERE wash_subscription = 1";
        $result = $db->query($sql);
        $customer_numbers = [];
        while ($row = $result->fetch_assoc()) {
            $customer_numbers[] = (int)$row['customer_id'];
        }
        return $customer_numbers;
    }

    public function getCustomersWithFixedPricing(): array
    {
        global $db;
        // Get all customers with fixed pricing
        $sql = "SELECT DISTINCT customer_number FROM customer_fixed_pricing";
        $result = $db->query($sql);
        $customer_numbers = [];
        while ($row = $result->fetch_assoc()) {
            $customer_numbers[] = (int)$row['customer_number'];
        }
        return $customer_numbers;
    }

    public function getCustomersWithTankCleaning(): array
    {
        global $db;
        // Get all customers with tank cleaning
        $sql = "SELECT DISTINCT user_id FROM customer_attributes WHERE attribute = 'onlyTankCleaning'";
        $result = $db->query($sql);
        $customer_numbers = [];
        while ($row = $result->fetch_assoc()) {
            $customer_numbers[] = (int)$row['user_id'];
        }
        // Get the customer numbers from the user IDs
        $customer_numbers = array_unique($customer_numbers);
        // Convert user IDs to customer numbers
        return self::getCustomerNumbersFromUserIds($customer_numbers);
    }

    private static function getCustomerNumbersFromUserIds(array $user_ids): array
    {
        global $db;
        $customer_numbers = (new users_o)->getFieldsWhere(
            ['id' => $user_ids],
            ['customer_number']
        );
        return array_map(function ($user) {
            return (int)$user['customer_number'];
        }, $customer_numbers);
    }

    /**
     * @throws Exception
     */
    public function getUsersByCustomerNumbers(array $customer_numbers, bool $customerNumberAndUserArray = false): array
    {
        global $db;
        // Get the users by customer numbers
        $customer_numbers = array_map('intval', $customer_numbers);
        $customer_numbers = implode(',', $customer_numbers);
        $sql = "SELECT id, customer_number FROM $this->table WHERE customer_number IN ($customer_numbers)";
        $result = $db->query($sql);
        // Create an array of user objects
        $ids = [];
        while ($row = $result->fetch_assoc()) {
            $ids[] = (int)$row['id'];
        }
        // Only get unique IDs
        $ids = array_unique($ids);
        $users = [];
        foreach ( $ids as $id ) {
            $user = new users_o();
            $user->id = $id;
            $user->getObjectProperties();
            if ($customerNumberAndUserArray) {
                // If the user should be returned with the customer number
                $users[$user->customer_number->value()] = $user;
            } else {
                // If the user should be returned as an object
                $users[] = $user;
            }
        }
        return $users;
    }

    /**
     * Check if the user has an XLVask customer account
     * @return bool True if the user has an XLVask customer account, false otherwise
     * @throws Exception if the user is not selected
     */
    public function hasXLVaskCustomerAccount(): bool
    {
        self::requireSelected();
        $customer_number = $this->customer_number->value();
        if (empty($customer_number)) {
            // If the customer number is not set, the user cannot have an XLVask customer account
            return false;
        }
        if ((new xlvask())->getCache()->isCustomerCached((int)$this->customer_number->value())) {
            // If the customer is cached, it means the user has an XLVask customer account
            return true;
        }
        // Check if the user exists in the XLVask system
        if (count((new xlvask())->getCustomers(null, null, $customer_number)) > 0) {
            // If the user exists in the XLVask system, it means the user has an XLVask customer account
            return true;
        }
        // Check if the user has an XLVask customer account associated internally
        return !empty($this->xlvask_customer_id->value());
    }

    public function getCustomersWithSpecialArrangements(): array
    {
        global $db;
        $sql = "SELECT DISTINCT c.customer_number 
            FROM users c 
            INNER JOIN user_key_value_pairs kv ON c.id = kv.user_id 
            WHERE kv.var = 'OtherSpecialArrangement' 
            AND kv.val IS NOT NULL 
            AND kv.val != ''";

        $result = $db->query($sql);
        $customer_numbers = [];

        while ($row = $result->fetch_assoc()) {
            $customer_numbers[] = (int)$row['customer_number'];
        }

        return $customer_numbers;
    }

    /**
     * @throws Exception
     */
    public function setPhoneNumber(int $phone_number, int $country_code = 45): void
    {
        self::requireSelected();
        // Set the phone number
        $this->phone->set((int)$phone_number);
        $this->phone_country_code->set((int)$country_code);
        $this->objectChanged();
    }

    public function setSMSNotificationsEnabled(bool $enabled): void
    {
        self::requireSelected();
        // Set SMS notifications enabled/disabled
        $this->sms_notifications_enabled->set($enabled ? 1 : 0);
        $this->objectChanged();
    }

    public function setEmailNotificationsEnabled(bool $enabled): void
    {
        self::requireSelected();
        // Set email notifications enabled/disabled
        $this->email_notifications_enabled->set($enabled ? 1 : 0);
        $this->objectChanged();
    }

    /**
     * @param int[] $customer_numbers
     * @return array<string, int>  Map of customer number to customer name
     */
    public function getCustomerNames(array $customer_numbers): array
    {
        global $db;
        $customer_numbers = array_map('intval', $customer_numbers);
        // Look in the cache first
        $customer_numbers_to_fetch = [];
        $customer_names_cached = self::getCachedForMultipleObjects('economic_customer_name', $customer_numbers);
        // Loop through the customer numbers and check if they are cached
        $customer_names = array_map(function ($cached_name) {
            return $cached_name ? json_decode($cached_name)->name : null;
        }, array_values($customer_names_cached));
        // Set the names for the cached customer numbers [ "customer_number" => "customer_name" ]
        $customer_names = array_combine(
            array_map('strval', $customer_numbers),
            $customer_names
        );
        // Find the customer numbers that are not cached
        foreach ( $customer_names as $customer_number => $customer_name ) {
            if ($customer_name === null) {
                $customer_numbers_to_fetch[] = (int)$customer_number;
            }
        }
        // Fetch the remaining customer names from E-conomic
        if (count($customer_numbers_to_fetch) > 0) {
            foreach ( $customer_numbers_to_fetch as $customer_number ) {
                // Get the customer name from the external source
                try {
                    // Try to get the economic customer data cached in the user
                    $tmp_user = new users_o();
                    $tmp_user->getUserByCustomerNumber($customer_number);
                    $cached_name = $tmp_user->getCached('economic_customer');
                    // If not cached, fetch from E-conomic
                    if (!$cached_name) {
                        $tmp_user->getCustomerEcocomicData($customer_number);
                        $cached_name = $tmp_user->getCached('economic_customer');
                    }
                    if ($cached_name) {
                        $customer_names[(string)$customer_number] = $cached_name->name;
                    }
                    // Cache the name
                    $this->cache('economic_customer_name', $cached_name, $customer_number);
                    $this->setCachedExpiration('economic_customer_name', self::$economicCustomerNameCacheExpiration, $customer_number);
                } catch ( Exception $e ) {
                    // Ignore exceptions
                    $customer_names[(string)$customer_number] = 'Unable to fetch name';
                }
            }
        }
        // Return the customer names
        return $customer_names;
    }

    public function getCashierName(int $cashier_id): string
    {
        $virtualCacheObjectID = "cashier_$cashier_id";
        $cache_key = 'cashier_name';
        // Check if the name is cached
        $cached_name = $this->getCached($cache_key, $virtualCacheObjectID);
        if ($cached_name) {
            return (string)$cached_name;
        } else {
            // Not cached, get the name from the database
            $cashier = new users_o();
            $cashier->select($cashier_id);
            $name = $cashier->display_name->value();
            // If the name is empty, set it to 'Unknown Cashier'
            if (empty($name)) {
                $name = 'Unknown Cashier';
            }
            // Cache the name
            $this->cache($cache_key, $name, $virtualCacheObjectID);
            $this->setCachedExpiration($cache_key, self::$cashierNameCacheExpiration, $virtualCacheObjectID);
            return $name;
        }
    }

}