<?php

namespace traits;

use classes\authentication;
use classes\permission_node;
use classes\recaptcha;
use classes\response;
use Exception;
use modules\subusers\helpers\subusers_permission_node_key;
use modules\subusers\classes\subuser_user_grant;
use objects\logs_o;

trait route_t
{
    protected array $permissions = [];
    private string $route;
    /**
     * Lightweight per-request caches to avoid repeated DB/auth checks during a single request lifecycle.
     */
    protected static array $__perm_user_cache = [];
    protected static array $__perm_subuser_grant_cache = [];

    public function __construct()
    {
        $this->route = $_SERVER['REQUEST_URI'];
    }

    public function run(): void
    {
        // Add the routes here
    }

    /**
     * Is authenticated?
     * This function returns a boolean indicating if the user is authenticated
     * @return bool
     */
    public function isAuthenticated(): bool
    {
        global $response;
        try {
            $user = (new authentication())->get_user();
            // If there is no user, return false
            if (!$user) {
                return false;
            }
            return true;
        } catch (Exception $e) {
            $response->error($e->getMessage(), 400);
        }
    }

    /**
     * Require type
     * @param mixed $value
     * @param string $type
     * @return bool
     */
    public function requireType(mixed $value, string $type): bool
    {
        // Get the type of the value
        $value_type = gettype($value);
        // If the type is Array, or Object, json_decode the value to check if it is a valid JSON
        if ($type === 'array' || $type === 'object') {
            if (is_array($value)) {
                $value = json_encode($value);
            } elseif (is_object($value)) {
                $value = json_encode($value);
            }
            $value = json_decode($value, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                global $response;
                $response->error('Invalid JSON. Value: ' . $value, 400);
            }
            $value_type = gettype($value);
        }
        // Check if the value is of the required type
        if ($value_type !== $type) {
            global $response;
            $response->error('Invalid type. Expected: ' . $type . ' Got: ' . $value_type . ' Value: ' . (is_array($value) ? json_encode($value) : $value), 400);
        }
        return true;
    }

    /**
     * Define a permission for the route
     * @description Used to standardize permission definitions and link them to subusers permission nodes if needed, this is used in the route definitions to define the permissions required for the route, and to link them to the subusers permission nodes for easier management in the subusers module
     * @param string $permission The permission to define (Example: 'modules_motorapi_lookup')
     * @param subusers_permission_node_key|null $subusers_permission_node_key The subusers permission node key to link the permission to (Example: subusers_permission_node_key::MOTORAPI_LOOKUP)
     * @return permission_node
     * @throws Exception
     */
    public function definePermission(string $permission, subusers_permission_node_key|null $subusers_permission_node_key = null): permission_node
    {
        if ($permission === '') {
            throw new Exception('Permission cannot be empty');
        }
        if ($subusers_permission_node_key !== null && !in_array($subusers_permission_node_key, subusers_permission_node_key::cases())) {
            throw new Exception('Invalid subusers_permission_node_key');
        }
        return permission_node::create($permission, $subusers_permission_node_key);
    }

    /**
     * Require a value to be in the given array
     * @param mixed $value The value to check
     * @param array $array The array to check against
     * @return bool
     * @throws Exception If the value is not in the array
     */
    public function requireInArray(mixed $value, array $array): bool
    {
        global $response;
        // Check if the value is in the array
        if (!in_array($value, $array)) {
            $response->error(json_encode([
                'message' => 'Unexpected value',
                'expected' => $array,
                'got' => $value,
                'type' => gettype($value)
            ]), 400);
        }
        return true;
    }

    public function requireSameLength(mixed $arg1, mixed $arg2): bool
    {
        global $response;
        if (strlen($arg1) !== strlen($arg2)) {
            $response->error('Invalid length. Expected: ' . strlen($arg2) . ' Got: ' . strlen($arg1), 400);
        }
        return true;
    }

    /**
     * Require parameter to be one of the given types
     * @param mixed $value The value to check
     * @param array $types The types to check against
     * @return bool
     */
    public function requireTypeIn(mixed $value, array $types): bool
    {
        // Get the type of the value
        $value_type = gettype($value);
        // Check if the value is of the required type
        if (!in_array($value_type, $types)) {
            global $response;
            $response->error('Invalid type. Expected: ' . implode(', ', $types) . ' Got: ' . $value_type . ' Value: ' . $value, 400);
        }
        return true;
    }

    /**
     * TYPE_ARRAY
     * @note This is used to check if the value is an array, this is used in routes to validate input
     * @return string
     */
    public function TYPE_ARRAY(): string
    {
        return 'array';
    }

    /**
     * TYPE_NULL
     * @note This is used to check if the value is null, this is used in routes to validate input
     * @return string
     */
    public function TYPE_NULL(): string
    {
        return 'NULL';
    }

    /**
     * The default date format (Y-m-d) Example: 2023-01-01
     * @note This is used to format dates in the API
     * @return string
     */
    public function FORMAT_DATE(): string
    {
        return 'Y-m-d';
    }

    /**
     * TYPE_BOOL
     * @note This is used to check if the value is a boolean, this is used in routes to validate input
     * @return string
     */
    public function TYPE_BOOL(): string
    {
        return 'boolean';
    }

    /**
     * @param string $date
     * @param string $format
     * @return void
     */
    public function requireDateFormat(string $date, string $format): void
    {
        global $response;
        $d = \DateTime::createFromFormat($format, $date);
        if (!$d || $d->format($format) !== $date) {
            $response->error('Invalid date format. Expected: ' . $format . ' Got: ' . $date, 400);
        }
    }

    public function requireMinValue(int $value, int $min): void
    {
        global $response;
        if ($value < $min) {
            $response->error('Parameter must be at least ' . $min, 400);
        }
    }

    public function type_int(): string
    {
        return 'integer';
    }

    public function type_string(): string
    {
        return 'string';
    }

    public function requireParameters(array $parameters): void
    {
        global $response;
        $missing_parameters = [];
        // Check if all required parameters are set
        foreach ( $parameters as $parameter ) {
            if (!$response->getRequestParameter($parameter) && !$response->isRequestParameterSet($parameter)) {
                $missing_parameters[] = $parameter;
            }
        }
        if (count($missing_parameters) > 0) {
            $response->error('Missing required parameters: ' . implode(', ', $missing_parameters), 400);
        }
    }

    /**
     * Require the max value of the given value
     * @param int $value - The value to check
     * @param int $max - The max value
     */
    public function requireMaxValue(int $value, int $max): void
    {
        global
        /** @var response $response */
        $response;
        if ($value > $max) {
            $response->error('Parameter must be at most ' . $max, 400);
        }
    }

    /**
     * Require department access
     * @note This checks if the user has access to the department by checking if the user has the permission department_access_{department} (_{permission} if provided)
     * @param string $department
     * @param string|null $permission
     */
    public function requireDepartmentAccess(string $department, string|null $permission = null): void
    {
        self::requirePermission('department_access_' . $department . ($permission ? '_' . $permission : ''));
    }

    /**
     * Has department access?
     * @note This checks if the user has access to the department by checking if the user has the permission department_access_{department} (_{permission} if provided)
     * @param string $department
     * @param string|null $permission
     * @return bool
     */
    public function hasDepartmentAccess(string $department, string|null $permission = null): bool
    {
        return self::hasPermission('department_access_' . $department . ($permission ? '_' . $permission : ''));
    }

    /**
     * Get parameters as an array from the request
     * @return array
     */
    public function getParametersAsArray(): array
    {
        global $response;
        return $response->getAllRequestParameters();
    }

    /**
     * Resolve customer number for a subuser permission context.
     * Order of precedence:
     * - Explicit `$customer_number` argument if provided
     * - Main authenticated user context (if available)
     * - Request header X-Customer-Number
     * - Query/post parameter `customer_number`
     */
    private function resolveCustomerNumberForSubuser(authentication $auth, ?int $customer_number = null): ?int
    {
        if ($customer_number !== null) {
            return (int)$customer_number;
        }
        $user_ctx = $auth->get_user();
        if ($user_ctx !== false && isset($user_ctx->customer_number)) {
            return (int)$user_ctx->customer_number->value();
        }
        $headers = function_exists('getallheaders') ? getallheaders() : [];
        if (isset($headers['X-Customer-Number'])) {
            return (int)$headers['X-Customer-Number'];
        }
        if (isset($_GET['customer_number'])) {
            return (int)$_GET['customer_number'];
        }
        if (isset($_POST['customer_number'])) {
            return (int)$_POST['customer_number'];
        }
        return null;
    }

    /**
     * Centralized permission evaluation used by both requirePermission and hasPermission.
     * - Honors subusers permission nodes without falling back to classic user permissions when a node is defined.
     * - Supports explicit customer_number overrides and auto-detection fallback.
     * - Uses simple per-request caches for efficiency.
     */
    private function evaluatePermission(string|permission_node $permission, ?int $customer_number, bool $throwOnDeny): bool
    {
        global $response;
        try {
            $auth = new authentication();

            // If permission is a node and a subuser is authenticated, evaluate node-based grant first
            $subuser = $auth->get_subuser();
            if ($subuser !== false && $permission instanceof permission_node && $permission->subusers_node_key !== null) {
                $resolvedCustomer = $this->resolveCustomerNumberForSubuser($auth, $customer_number);
                if ($resolvedCustomer === null) {
                    (new logs_o())->add('global', 'global', 1, $subuser->id ?? 0, 'PERMISSION_DENIED', 'Missing customer context for subuser permission evaluation: ' . $permission->permission);
                    if ($throwOnDeny) {
                        $response->error('Permission denied. Missing customer context for subuser.', 403);
                    }
                    return false;
                }
                // Expose resolved target customer in response meta for subuser requests
                $response->add_meta('target_customer_number', (int)$resolvedCustomer);
                $subuser_has_permission = $subuser->hasPermission($permission->subusers_node_key);
                if (!$subuser_has_permission && $throwOnDeny) {
                    (new logs_o())->add('global', 'global', 1, $subuser->id ?? 0, 'PERMISSION_DENIED', 'Permission denied via subuser node: ' . $permission->permission . ' (Node: ' . $permission->subusers_node_key->name . ', Customer: ' . $resolvedCustomer . ')');
                    $response->error('Permission denied for subuser. Missing permission: ' . $permission->permission . ' (Customer context: ' . $resolvedCustomer . ')', 403);
                }
                return $subuser_has_permission;
            }

            // Fallback to classic user permission check (or if permission is plain string)
            $user = $auth->get_user();
            if (!$user) {
                (new logs_o())->add('global', 'global', 1, 0, 'AUTHENTICATION_FAILED', 'Authentication failed. Invalid, or missing token');
                if ($throwOnDeny) {
                    $response->error('Authentication failed. Invalid or missing token.', 401);
                }
                return false;
            }
            $perm_string = $permission instanceof permission_node ? $permission->permission : $permission;
            $userCacheKey = (string)$user->id . ':' . $perm_string;
            if (!isset(self::$__perm_user_cache[$userCacheKey])) {
                self::$__perm_user_cache[$userCacheKey] = (bool)$user->hasPermission($perm_string);
            }
            $allowed = (bool)self::$__perm_user_cache[$userCacheKey];
            if (!$allowed && $throwOnDeny) {
                (new logs_o())->add('global', 'global', 1, $user->id, 'PERMISSION_DENIED', 'Permission denied. Missing permission: ' . $perm_string);
                $response->error('Permission denied. Missing permission: ' . $perm_string . ' for user: ' . $user->id . ' In group: ' . $user->group_id->value(), 403);
            }
            return $allowed;
        } catch (Exception $e) {
            $response->error($e->getMessage(), 400);
            return false;
        }
    }

    /**
     * Require permission
     * @param string|permission_node $permission
     * @return bool
     */
    public function requirePermission(string|permission_node $permission): bool
    {
        return $this->evaluatePermission($permission, null, true);
    }

    /**
     * Check if the user has a permission
     * @param string|permission_node $permission
     * @return bool
     */
    public function hasPermission(string|permission_node $permission, int $customer_number = null): bool
    {
        return $this->evaluatePermission($permission, $customer_number, false);
    }

    /**
     * Determine if the current principal (classic user or subuser) acts on their own customer context.
     * - For classic users: compares against the authenticated user's customer_number
     * - For subusers: compares against X-Customer-Number target header
     */
    public function isOwnCustomerContext(int $targetCustomerNumber): bool
    {
        try {
            $auth = new authentication();
            $sub = $auth->get_subuser();
            if ($sub !== false) {
                $tgt = $auth->get_subuser_customer_number_target();
                return ((int)$tgt === (int)$targetCustomerNumber);
            }
            $user = $auth->get_user();
            if ($user !== false) {
                return ((int)$user->customer_number->value() === (int)$targetCustomerNumber);
            }
        } catch (Exception) {
            // fall through
        }
        return false;
    }

    /**
     * Resolve the most relevant customer number for the current request context.
     * Priority: classic user -> X-Customer-Number header -> query/body customer_number
     */
    public function resolveEffectiveCustomerNumber(): ?int
    {
        try {
            $auth = new authentication();
            $user = $auth->get_user();
            if ($user !== false && isset($user->customer_number)) {
                return (int)$user->customer_number->value();
            }
            $sub = $auth->get_subuser();
            if ($sub !== false) {
                $tgt = $auth->get_subuser_customer_number_target();
                if ($tgt !== false && $tgt !== null) return (int)$tgt;
            }
            // Fallbacks
            if (isset($_GET['customer_number'])) return (int)$_GET['customer_number'];
            if (isset($_POST['customer_number'])) return (int)$_POST['customer_number'];
        } catch (Exception) {
            // ignore
        }
        return null;
    }

    /**
     * Common pattern helper: allow via own-permission (with optional guard) or via department/admin permission.
     * If neither path allows, respond with 403 and provided message.
     *
     * @param string|permission_node $permissionOwn   Own-scope permission (often linked to a subuser node)
     * @param string|permission_node $permissionOther Department/admin permission
     * @param int|null $targetCustomerNumber          Customer number to validate "own" scope against (null means list context)
     * @param int|null $departmentId                  Department id for admin path (will be validated when provided)
     * @param callable|null $ownGuard                 Optional additional guard for own path. Return true to allow, false to deny own-path.
     * @param string|null $denyMessage                Message to return on deny (defaults to generic)
     * @return bool                                   True if access is allowed (also throws on deny)
     */
    public function allowOwnOrDepartmentAccess(
        string|permission_node $permissionOwn,
        string|permission_node $permissionOther,
        ?int $targetCustomerNumber,
        ?int $departmentId,
        ?callable $ownGuard = null,
        ?string $denyMessage = null
    ): bool {
        global $response;
        $allowed = false;
        $hasOwn = $this->hasPermission($permissionOwn);
        $hasOther = $this->hasPermission($permissionOther);

        // Try own path first
        if ($hasOwn) {
            $isOwnContext = ($targetCustomerNumber === null) ? true : $this->isOwnCustomerContext((int)$targetCustomerNumber);
            if ($isOwnContext) {
                $guardOk = $ownGuard ? (bool)call_user_func($ownGuard) : true;
                if ($guardOk) {
                    $allowed = true;
                }
            }
        }

        // Fallback to department/admin path
        if (!$allowed && $hasOther) {
            if ($departmentId !== null) {
                $this->requireDepartmentAccess((int)$departmentId);
            }
            $allowed = true;
        }

        if (!$allowed) {
            $response->error($denyMessage ?? 'Permission denied.', 403);
        }
        return $allowed;
    }

    /**
     * Require parameter to be a positive integer
     * @param int $value The value to check
     * @param string|null $parameter The name of the parameter
     * @return void
     */
    public function requireParameterIntPositive(int $value, string $parameter = null): void
    {
        global $response;
        if ($value <= 0) {
            $response->error('Parameter ' . $parameter . ' must be a positive integer', 400);
        }
    }

    public function requireMinLength(string $parameter, int $length): void
    {
        global $response;
        $value = self::getParameter($parameter);
        if (strlen($value) < $length) {
            $response->error('Parameter ' . $parameter . ' must be at least ' . $length . ' characters long', 400);
        }
    }

    /**
     * Get the parameter from the request by name
     * This is a shorthand for the response class method
     * @param string $parameter
     * @return mixed|null
     */
    public function getParameter(string $parameter): mixed
    {
        global $response;
        return $response->getRequestParameter($parameter);
    }

    public function requireMaxLength(string $parameter, int $length): void
    {
        global $response;
        $value = self::getParameter($parameter);
        if (strlen($value) > $length) {
            $response->error('Parameter ' . $parameter . ' must be at most ' . $length . ' characters long', 400);
        }
    }

    public function isParametersSet(array $parameters): bool
    {
        global $response;
        // Check if all required parameters are set
        foreach ( $parameters as $parameter ) {
            if (!$response->isRequestParameterSet($parameter)) {
                return false;
            }
        }
        return true;
    }

    /**
     * GET route
     * @param string $route Example: /home, /home/{id}
     * @param callable $callback
     * @param array $permissions
     * @return void
     */
    public function get(string $route, callable $callback, array $permissions = []): void
    {
        $this->registerRoute($route, 'GET', $callback, $permissions);
    }

    private function registerRoute($route, $method, $callback, $permissions = []): void
    {
        global $router;
        $router->add($route, $method, $callback, self::registerPermissions($permissions, $route, $method));
    }

    /**
     * Register permissions for the route
     * @param array $permissions The permissions to register
     * @param null $endpoint
     * @param null $method
     * @return array
     */
    public function registerPermissions(array $permissions, $endpoint = null, $method = null): array
    {
        foreach ( $permissions as $permission => $description ) {
            $this->registerPermission($permission, $description, $endpoint, $method);
        }
        return $this->permissions;
    }

    /**
     * Register a permission
     * @param string $permission The permission to register (Example: 'modules_motorapi_lookup')
     * @param string $description The description of the permission (Example: 'Lookup license plate information')
     * @param null $endpoint The endpoint of the permission (Example: '/department/license-plate/lookup')
     * @param null $method The method of the permission (Example: 'GET')
     * @return void
     */
    public function registerPermission(string $permission, string $description = 'No description provided.', $endpoint = null, $method = null): void
    {
        $this->permissions[$endpoint ?? $this->route][$method ?? 'UNKNOWN_METHOD'][$permission] = $description;
    }

    /**
     * POST route
     * @param string $route Example: /home, /home/{id}
     */
    public function post(string $route, callable $callback, array $permissions = []): void
    {
        $this->registerRoute($route, 'POST', $callback, $permissions);
    }

    /**
     * PUT route
     * @param string $route Example: /home, /home/{id}
     */
    public function put(string $route, callable $callback, array $permissions = []): void
    {
        $this->registerRoute($route, 'PUT', $callback, $permissions);
    }

    /**
     * DELETE route
     * @param string $route Example: /home, /home/{id}
     */
    public function delete(string $route, callable $callback, array $permissions = []): void
    {
        $this->registerRoute($route, 'DELETE', $callback, $permissions);
    }

    /**
     * OPTIONS route
     * @param string $route Example: /home, /home/{id}
     */
    public function options(string $route, callable $callback, array $permissions = []): void
    {
        $this->registerRoute($route, 'OPTIONS', $callback, $permissions);
    }

    /**
     * PATCH route
     * @param string $route Example: /home, /home/{id}
     */
    public function patch(string $route, callable $callback, array $permissions = []): void
    {
        $this->registerRoute($route, 'PATCH', $callback, $permissions);
    }

    /**
     * Get the parameter from the route URL by index
     * @param string $index
     * @return string|null
     */
    public function fromRoute(string $index): ?string
    {
        $params = explode('/', $this->route);
        $index = array_search($index, $params);
        return $params[$index] ?? null;
    }

    /**
     * Require plate scanner authentication
     * @return bool
     */

    public function requirePlateScannerAuth(): bool
    {
        global $response;
        // Check if the plate scanner has a valid API key
        try {
            $plate_scanner = (new authentication())->get_plate_scanner();
            if (!$plate_scanner) {
                (new logs_o())->add('global', 'global', 1, 0, 'AUTHENTICATION_FAILED', 'Authentication failed. Invalid, or missing API key');
                $response->error('Authentication failed. Invalid or missing API key.', 401);
            }
        } catch (Exception $e) {
            $response->error($e->getMessage(), 400);
        }
        return true;
    }

    /**
     * Require order access
     * @param int $order_id The order ID
     * @param string|null $permission The permission to check
     * @return bool
     */
    public function requireOrderAccess(int $order_id, string|null $permission = null): bool
    {
        // TODO: Implement indirect order access, where the user has access to the department, and the department has access to the order
        self::requirePermission('order_access_' . $order_id . ($permission ? '_' . $permission : ''));
        return true;
    }

    /**
     * Require reCAPTCHA for the request
     * @return bool
     */
    public function requireRecaptcha(): bool
    {
        global $response;
        // Check if the reCAPTCHA is valid
        try {
            $recaptcha_response = $response->getRequestParameter('g_recaptcha_response');
            $recaptcha = (new recaptcha())->validate($recaptcha_response);
            if (!$recaptcha) {
                (new logs_o())->add('global', 'global', 1, 0, 'AUTHENTICATION_FAILED', 'Authentication failed. Invalid, or missing reCAPTCHA');
                $response->error('Authentication failed. Invalid or missing reCAPTCHA.', 401);
            }
        } catch (Exception $e) {
            $response->error($e->getMessage(), 400);
        }
        return true;
    }

    /**
     * Get data from the request body or query string by name
     * @param string $name
     * @return string|null
     */
    public function fromRequest(string $name): ?string
    {
        // If the $_POST variable is set, return the value from the POST variable, otherwise return the value from the query string
        $data = json_decode(file_get_contents('php://input'), true);
        if (is_array($data) && isset($data[$name])) {
            return $data[$name];
        }
        return (isset($_POST[$name])) ? $_POST[$name] : $this->fromQuery($name);
    }

    /**
     * Get the parameter from the query string by name
     * @param string $name
     * @return string|null
     */
    public function fromQuery(string $name): ?string
    {
        return (isset($_GET[$name])) ? $_GET[$name] : null;
    }

    /**
     * Match route
     * @param string $route Example: /home, /home/{id}
     */
    private function match_route(string $route): bool
    {
        // Check if route is the same, or if it matches the regex pattern
        return $route === $this->route || preg_match($route, $this->route);
    }
}