<?php

namespace objects;

use classes\authentication;
use classes\db;
use classes\object_property;
use Exception;
use modules\subusers\classes\subuser_user_grant;
use modules\subusers\helpers\subusers_permission_node_key;
use Random\RandomException;
use traits\db_object_t;

class subusers_o extends db
{
    use db_object_t;

    public object_property $username;
    public object_property $password;
    public object_property $name;
    public object_property $email;
    public object_property $phone_country_code;
    public object_property $phone;
    public object_property $created_at;
    public object_property $updated_at;
    public object_property $suspended_at;


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

    public function objectChanged(): void
    {
        // No need to invalidate the cache, since the plate_scans object is not cached
    }

    public function getObjectProperties(): void
    {
        $this->username = new object_property($this->table, $this->id, 'username', 'string');
        $this->password = new object_property($this->table, $this->id, 'password', 'string');
        $this->name = new object_property($this->table, $this->id, 'name', 'string');
        $this->email = new object_property($this->table, $this->id, 'email', 'string');
        $this->phone_country_code = new object_property($this->table, $this->id, 'phone_country_code', 'int');
        $this->phone = new object_property($this->table, $this->id, 'phone', 'int');
        $this->created_at = new object_property($this->table, $this->id, 'created_at', 'timestamp');
        $this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'timestamp');
        $this->suspended_at = new object_property($this->table, $this->id, 'suspended_at', 'timestamp');
    }

    /**
     * Add a new subuser to the database. The password is optional, but if it is set, it must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number. The password will be hashed before being stored in the database.
     * @param string|null $username
     * @param string|null $password
     * @param string|null $name
     * @param string|null $email
     * @param int $phone_country_code
     * @param int $phone
     * @return $this
     */
    public function add(?string $username, ?string $password, ?string $name, ?string $email, int $phone_country_code, int $phone): subusers_o
    {
        global $db, $response;
        try {
            if (!empty($password)) {
                // Validate the password (at least 8 characters, at least one uppercase letter, at least one lowercase letter, at least one number)
                if (!preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/', $password)) {
                    throw new Exception('Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number.');
                }
                // Hash the password
                $password = password_hash($password, PASSWORD_DEFAULT);
            }
            if (!empty($email)) {
                if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                    throw new Exception('Invalid email address.');
                }
                $email = $db->escape_string($email);
            }
            if (!empty($username)) {
                if (strlen($username) < 3 || strlen($username) > 50) {
                    throw new Exception('Username must be between 3 and 50 characters long.');
                }
                $username = $db->escape_string($username);
            }
            if (!empty($name)) {
                if (strlen($name) < 2 || strlen($name) > 100) {
                    throw new Exception('Name must be between 2 and 100 characters long.');
                }
                $name = $db->escape_string($name);
            }

            // Escape the other parameters
            $phone_country_code = $db->escape_string($phone_country_code);
            $phone = $db->escape_string($phone);
            // Create a new record in the database
            $tmp = $this->add_object([
                ...(!empty($username) ? ['username' => $username] : []),
                ...(!empty($name) ? ['name' => $name] : []),
                ...(!empty($email) ? ['email' => $email] : []),
                'phone_country_code' => (int)$phone_country_code,
                'phone' => (int)$phone,
                // Add the password only if it is set
                ...(!empty($password) ? ['password' => (string)$password] : []),
            ]);

            // Get the id of the new record
            $this->id = (int)$tmp;
            // Set the values of the object properties
            $this->getObjectProperties();
            return $this;
        } catch (Exception $e) {
            $response->error($e->getMessage());
        }
    }

    /**
     * Set the password for the subuser. The password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number. The password will be hashed before being stored in the database.
     * @param string $password
     * @return self
     * @throws Exception If the password does not meet the requirements or if no subuser is selected.
     */
    public function setPassword(string $password): self
    {
        self::requireSelected();
        $this->password->set((string)password_hash($password, PASSWORD_DEFAULT));
        return $this;
    }

    public function getSubuserByPhone(int $phone_country_code, int $phone): ?subusers_o
    {
        $tmp = self::getFieldsWhere([
            'phone_country_code' => $phone_country_code,
            'phone' => $phone,
        ], ['id']);
        if (count($tmp) === 0) {
            return null;
        }
        $subuser = (new subusers_o())->select((int)$tmp[0]['id']);
        $subuser->getObjectProperties();
        return $subuser;
    }

    /**
     * Generate a setup token for the subuser. The setup token is a random string that can be used to link the subuser to a company user. The setup token is valid for 24 hours and can only be used once.
     * @return string The setup token
     * @throws RandomException If there was an error generating the random bytes for the token
     * @throws Exception If no subuser is selected
     */
    public function generateSetupToken(): string
    {
        self::requireSelected();
        try {
            $token = bin2hex(random_bytes(16));
        } catch (Exception $e) {
            throw new RandomException('Error generating random bytes for setup token', 0, $e);
        }
        $cache_key = 'setup_token:' . $token;
        $cashe_object_id = 'subuser_setup_token';
        $this->cache($cache_key, $this->id, $cashe_object_id);
        $this->setCachedExpiration($cache_key, 24 * 60 * 60, $cashe_object_id); // Set the cache expiration to 24 hours
        return $token;
    }

    /**
     * Get the subuser id by the setup token. If the token is valid, it will return the subuser id and delete the token from the cache. If the token is invalid or expired, it will return null.
     * @param string $token The setup token
     * @return int|null The subuser id or null if the token is invalid or expired
     * @throws Exception If there was an error getting the subuser id from the cache
     */
    public function getSubuserIdBySetupToken(string $token): ?int
    {
        $cache_key = 'setup_token:' . $token;
        $cache_object_id = 'subuser_setup_token';
        $subuser_id = $this->getCached($cache_key, $cache_object_id);
        if ($subuser_id === null) {
            return null;
        }
        return $subuser_id;
    }

    /**
     * Get the subuser by the setup token. If the token is valid, it will return the subuser object and delete the token from the cache. If the token is invalid or expired, it will return null.
     * @param string $token The setup token
     * @return subusers_o|null The subuser object or null if the token is invalid or expired
     * @throws Exception If there was an error getting the subuser object from the cache
     */
    public function getSubuserBySetupToken(string $token): ?subusers_o
    {
        $subuser_id = $this->getSubuserIdBySetupToken($token);
        if ($subuser_id === null) {
            return null;
        }
        $subuser = (new subusers_o())->select((int)$subuser_id);
        $subuser->getObjectProperties();
        return $subuser;
    }

    public function invalidateSetupToken(string $token): void
    {
        $object_id = 'subuser_setup_token';
        $cache_key = 'setup_token:' . $token;
        $this->deleteCached($cache_key, $object_id);
    }

    /**
     * @throws Exception
     */
    public function getSubuserByUsername(string $username): ?subusers_o
    {
        global $db;
        $username = $db->escape_string($username);
        $tmp = self::getFieldsWhere([
            'username' => $username,
        ], ['id']);
        if (count($tmp) === 0) {
            return null;
        }
        $subuser = (new subusers_o())->select((int)$tmp[0]['id']);
        $subuser->getObjectProperties();
        return $subuser;
    }

    /**
     * @throws RandomException
     * @throws Exception
     */
    public function generateSession(): string
    {
        self::requireSelected();
        $session_token = bin2hex(random_bytes(32));
        $this->cache('session_token:' . $session_token, $this->id, 'subuser_sessions');
        $this->setCachedExpiration('session_token:' . $session_token, 7 * 24 * 60 * 60, 'subuser_sessions'); // Set the session to expire after 7 days
        // Add the token
        $tokens_o = new tokens_o();
        $tokens_o->create($this->id, $session_token, 'AUTH_TOKEN_SUBUSER');
        return $session_token;
    }

    /**
     * @param string $token The session token
     * @return subusers_o|null The subuser object or null if the token is invalid or expired
     * @throws Exception
     */
    public function getSubuserBySessionToken(string $token): ?subusers_o
    {
        $cache_key = 'session_token:' . $token;
        $cache_object_id = 'subuser_sessions';
        $subuser_id = $this->getCached($cache_key, $cache_object_id);
        if ($subuser_id === null) {
            return null;
        }
        $subuser = (new subusers_o())->select((int)$subuser_id);
        $subuser->getObjectProperties();
        return $subuser;
    }

    /**
     * Check if the selected subuser has a permission related to a specific customer number.
     * @param subusers_permission_node_key $permission_node_key The permission node key to check
     * @param int|null $customer_number The customer number to check the permission for
     * @return bool True if the subuser has the permission, false otherwise
     * @throws Exception
     */
    public function hasPermission(subusers_permission_node_key $permission_node_key, ?int $customer_number = null): bool
    {
        // Ensure a subuser is selected
        self::requireSelected();
        // If the customer number is not provided, we should get it from the request context.
        $customer_number = $customer_number ?? (new authentication())->get_subuser_customer_number_target();
        // If the customer number is still null, we cannot evaluate permissions, so we throw an exception.
        if ($customer_number === null) {
            throw new Exception('X-Customer-Number is required to evaluate permissions');
        }
        // Evaluate permission using the subusers grant system
        $grant = new subuser_user_grant((int)$this->id, (int)$customer_number);
        return $grant->hasNode($permission_node_key);
    }
}