<?php

namespace objects;

use classes\db;
use classes\object_property;
use classes\stripe;
use Exception;
use traits\db_object_t;

class departments_o extends db
{
    use db_object_t;

    public object_property $name;
    public object_property $description;
    public object_property $economic_department_id; // The id of the department in the economic system (Can be null)
    public object_property $slack_webhook; // The slack webhook for the department
    public department_variables_o $variables; // The department variables object
    public object_property $dimension; // The dimension of the department
    public object_property $visible; // The visibility of the department
    public object_property $branding; // The branding of the department
    public object_property $longitude; // The longitude of the department (Can be null)
    public object_property $latitude; // The latitude of the department (Can be null)
    public object_property $created_at;
    public object_property $updated_at;

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

    public function objectChanged(): void
    {
        // Clear the cache
        redis->clear_departments();
    }

    /**
     * Get the lanes associated with the department
     * @retuns department_lanes_o[] The lanes associated with the department
     * @throws Exception If the department is not selected
     */
    public function getLanes(): array
    {
        self::requireSelected();
        return (new department_lanes_o())->getDepartmentLanes($this->id);
    }

    /**
     * Add a department
     * @param string $name
     * @param string $description
     * @throws Exception If the object was not created successfully
     */
    public function create(string $name, string $description): void
    {
        global $db;
        // Avoid SQL injection
        $name = $db->escape_string($name);
        $description = $db->escape_string($description);
        // Create a new record in the database
        $sql = "INSERT INTO $this->table (name, description) VALUES ('$name', '$description')";
        $db->query($sql);

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

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

        // Clear the cache
        redis->clear_departments();
    }

    /**
     * Get the object properties of the department
     * @throws Exception If the object is not selected
     */
    public function getObjectProperties(): void
    {
        self::requireSelected();
        $this->name = new object_property($this->table, $this->id, 'name', 'string', true);
        $this->description = new object_property($this->table, $this->id, 'description', 'string', false);
        $this->economic_department_id = new object_property($this->table, $this->id, 'economic_department_id', 'int', false);
        $this->slack_webhook = new object_property($this->table, $this->id, 'slack_webhook', 'string', false);
        $this->variables = (new department_variables_o())->selectDepartment($this->id);
        $this->dimension = new object_property($this->table, $this->id, 'dimension', 'int', false);
        $this->branding = new object_property($this->table, $this->id, 'branding', 'int', false);
        $this->visible = new object_property($this->table, $this->id, 'visible', 'int', false);
        $this->longitude = new object_property($this->table, $this->id, 'longitude', 'float', false);
        $this->latitude = new object_property($this->table, $this->id, 'latitude', 'float', 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);
    }

    public function edit(int $id, string $name, string $description, int $economic_department_id): void
    {
        global $db;
        $this->id = $id;
        // Avoid SQL injection
        $name = $db->escape_string($name);
        $description = $db->escape_string($description);
        // If the economic_department_id is 0, set it to null
        if ($economic_department_id === 0) {
            $economic_department_id = null;
        }
        // Update the record in the database
        $sql = "UPDATE $this->table SET name = '$name', description = '$description', economic_department_id = " . ($economic_department_id ?? 'NULL') . " WHERE id = $this->id";
        $db->query($sql);

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

        // Clear the cache
        redis->clear_department($id);
    }

    public function list($superUser = false): array
    {
        global $db;
        // Check if the departments are cached
        $departments = redis->get_departments();
        // If the departments are not cached, get them from the database
        if ($departments === null) {
            $sql = "SELECT * FROM $this->table";
            $result = $db->query($sql);
            $departments = $db->fetch_all($result);
            // Cache the departments (If it is not empty or null)
            if (!empty($departments)) {
                redis->cache_departments($departments);
            }
        }
        // Only show the name, description and id if the user isn't a super user
        if (!$superUser) {
            // Map the departments to only show the name, description and id
            $departments = array_map(function ($department) {
                return [
                    'name' => $department['name'],
                    'description' => $department['description'],
                    'id' => $department['id'],
                    'visible' => $department['visible'],
                ];
            }, $departments);
        }
        return $departments;
    }

    public function getDepartmentById(int $id, bool $force_all = false): array
    {
        global $db;
        // Check if the department is cached
        $department = redis->get_department($id);
        // If the department is not cached, get it from the database
        if ($department === null || $force_all) {
            $sql = "SELECT * FROM $this->table WHERE id = $id";
            $result = $db->query($sql);
            $department = $db->fetch_assoc($result);
            // Cache the department (If it is not empty or null)
            if (!empty($department)) {
                redis->cache_department($id, $department);
            }
        }
        return $department;
    }

    /**
     * Get the price of a product in a department
     * @param int $department_id
     * @param int $product_id
     * @param int $price
     * @return void
     */
    public function setDepartmentProductPrice(int $department_id, int $product_id, int $price): void
    {
        global $db;
        // Check if the record already exists
        $this->removeDepartmentProductPriceIfExist($department_id, $product_id);
        // If the price is 0, return
        if ($price === 0) {
            return;
        }
        // Create a new record in the database
        $sql = "INSERT INTO product_department_prices (department_id, product_id, price) VALUES ($department_id, $product_id, $price)";
        $db->query($sql);
    }

    /**
     * Remove the price of a product in a department
     * @param int $department_id
     * @param int $product_id
     * @return void
     */
    private function removeDepartmentProductPriceIfExist(int $department_id, int $product_id): void
    {
        global $db;
        // Get the price from the database
        $sql = "SELECT * FROM product_department_prices WHERE department_id = $department_id AND product_id = $product_id";
        $result = $db->query($sql);

        if ($result->num_rows > 0) {
            // Remove the record from the database
            $sql = "DELETE FROM product_department_prices WHERE department_id = $department_id AND product_id = $product_id";
            $db->query($sql);
        }
    }

    public function getDepartmentProductPrices(int $department_id): array
    {
        global $db;
        $sql = "SELECT * FROM product_department_prices WHERE department_id = $department_id";
        $result = $db->query($sql);
        return $db->fetch_all($result);
    }

    public function getDepartmentName(int $department): string
    {
        global $db;
        // Check if the department name is cached
        $name = redis->get_department_name($department);
        // If the department name is not cached, get it from the database
        if ($name === null) {
            try {
                $this->id = $department;
                $this->getObjectProperties();
                $name = $this->name->value();
                // Cache the department name (If it is not empty or null)
                if (!empty($name)) {
                    redis->cache_department_name($department, $name);
                }
            } catch (Exception $e) {
                $name = 'Unable to get department name: ' . $department;
            }
            return $name;
        }
        return redis->get_department_name($department) ?? 'Unable to get department name: ' . $department;
    }

    /**
     * Get the Stripe terminal location department variable
     * @note This is a link to the department_variables_o::getVariable(stripe_terminal_location) method
     * @return string The Stripe terminal location id
     * @throws Exception If the department variable is not set
     * @throws Exception If the department is not selected
     */
    public function getStripeTerminalLocation(): string
    {
        self::requireSelected();
        if (!$this->variables->isSet('stripe_terminal_location')) {
            throw new Exception('The Stripe terminal location is not set for the department.');
        }
        return $this->variables->getVariable('stripe_terminal_location');
    }

    /**
     * Check if the Stripe terminal location variable is set
     * @note This is a link to the department_variables_o::isSet(stripe_terminal_location) method
     * @return bool true If the Stripe terminal location is set
     * @throws Exception If the department is not selected
     */
    public function isStripeTerminalLocationSet(): bool
    {
        self::requireSelected();
        return $this->variables->isSet('stripe_terminal_location');
    }

    /**
     * Set the Stripe terminal location department variable
     * @note This is a link to the department_variables_o::set(stripe_terminal_location) method
     * @param string $location The Stripe terminal location id
     * @throws Exception If the department is not selected
     * @throws Exception If the department variable is not updated successfully
     */
    public function setStripeTerminalLocation(string $location): void
    {
        self::requireSelected();
        $this->variables->set('stripe_terminal_location', $location);
    }

    /**
     * Get the Stripe terminal readers
     * @note This is a link to the stripe_endpoint_readers::listLocation method
     * @throws Exception If the department is not selected
     * @throws Exception If the Stripe terminal location is not set for the department
     */
    public function getStripeTerminalReaders(): \Stripe\Collection
    {
        self::requireSelected();
        if (!$this->variables->isSet('stripe_terminal_location')) {
            throw new Exception('The Stripe terminal location is not set for the department.');
        }
        return (new stripe())->readers->listLocation($this->variables->getVariable('stripe_terminal_location'));
    }

    /**
     * Convert the object to an array
     * @param array|null $options The options for the conversion (e.g. ['slack_webhook' => true])
     * @return array The object as an array
     * @throws Exception If the object is not selected
     */
    public function asArray(array $options = null): array
    {
        self::requireSelected();
        $tmp = [
            'id' => (int)$this->id,
            'name' => (string)$this->name->value(),
            'description' => (string)$this->description->value(),
            'economic_department_id' => (int)$this->economic_department_id->value(),
            'dimension' => (int)$this->dimension->value(),
            'branding' => (int)$this->branding->value(),
            'longitude' => (float)$this->longitude->value(),
            'latitude' => (float)$this->latitude->value(),
            'created_at' => (string)$this->created_at->value(),
            'updated_at' => (string)$this->updated_at->value(),
        ];
        // Check if the webhook should be included
        if ($options && $options['slack_webhook']) {
            $tmp['slack_webhook'] = (string)$this->slack_webhook->value();
        }
        return $tmp;
    }

    /**
     * Check if the Stripe is configured for the department
     * @note This is a link to the department_variables_o::isSet(stripe_terminal_location) method
     * @return bool true If the Stripe is configured
     * @throws Exception If the department is not selected
     */
    public function isStripeConfigured(): bool
    {
        self::requireSelected();
        if ($this->variables->isSet('stripe_terminal_location')) {
            return true;
        }
        return false;
    }

    /**
     * @throws Exception If the department is not selected
     * @throws Exception If the branding is not set
     * @throws Exception If the branding is not found
     */
    public function getBranding(): branding_o
    {
        self::requireSelected();
        return (new branding_o())->select($this->branding->value());
    }

    /**
     * Get the phone numbers for the SMS notification
     * @note This is a link to the department_notification_sms_o::getDepartmentActivePhoneNumbers method
     * @throws Exception If the department is not selected
     */
    public function notificationSmsPhoneNumbers(): array
    {
        self::requireSelected();
        return (new department_notification_sms_o())->getDepartmentActivePhoneNumbers($this->id);
    }

    /**
     * @throws Exception
     */
    public function isModuleTimeBookingsEnabled(): bool
    {
        self::requireSelected();
        return (bool)$this->variables->getVariable('bookingsystem_time_based_enabled');
    }

    /**
     * Select a department by its name (case-sensitive, exact match)
     * @throws Exception If the department selection fails or the department is not found
     * @example "Hvidovre"
     */
    public function selectByName(string $departmentName): self
    {
        $result = self::getFieldsWhere([
            'name' => $departmentName,
        ], [
            'id',
        ]);
        if (empty($result)) {
            throw new Exception('Department not found: ' . $departmentName);
        }
        $this->selectId($result[0]['id']);
        return $this;
    }

    public function selectId(int $department_id): self
    {
        $this->id = $department_id;
        $this->getObjectProperties();
        return $this;
    }

    /**
     * @return products_o[] An array of all products in the department's categories
     * @throws Exception
     */
    public function getAllProductInDepartmentCategories(): array
    {
        self::requireSelected();

        // Get all the categories associated
        $department_categories = (new department_categories_o())->getDepartmentCategoryObjects($this->id);

        // Use array_reduce to combine both loops into a single operation
        $products = array_reduce($department_categories, function ($products, $department_category) {
            return array_merge($products, $department_category->getCategory()->getProducts());
        }, []);

        // Remove duplicate products
        return array_unique($products, SORT_REGULAR);
    }

    /**
     * If the department is excluded from invoicing
     * @return bool
     * @throws Exception If the department is not selected
     */
    public function isExcludedFromInvoicing(): bool
    {
        self::requireSelected();
        return $this->variables->getVariable('exclude_from_invoicing') === true;
    }
}