<?php

/**
 * Session Manager Class
 *
 * Handles session management, authentication, and access control.
 */
class _SLM
{
    public static array $session = array();
    public static ?array $startSession = null;
    public static ?string $appToken = null;
    public static bool $destroying = false;

    /**
     * Returns the mapping configuration for the session index in Elasticsearch.
     *
     * @return bool|string Returns the mapping configuration as a JSON string.
     */
    private static function getMapping(): bool|string
    {
        $json['mappings'] = [
            'properties' => [
                '_server' => [
                    'type' => 'text',
                    'fields' => [
                        'keyword' => [
                            'type' => 'keyword',
                            'ignore_above' => 256
                        ]
                    ]
                ],
                'app' => [
                    'type' => 'text',
                    'fields' => [
                        'keyword' => [
                            'type' => 'keyword',
                            'ignore_above' => 256
                        ]
                    ]
                ],
                'environment' => [
                    'type' => 'text',
                    'fields' => [
                        'keyword' => [
                            'type' => 'keyword',
                            'ignore_above' => 256
                        ]
                    ]
                ],
                'human_date' => [
                    'type' => 'date',
                    'format' => 'yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis',
                ],
                'session' => [
                    'type' => 'text',
                    'fields' => [
                        'keyword' => [
                            'type' => 'keyword',
                            'ignore_above' => 256
                        ]
                    ]
                ],
                'timestamp' => [
                    'type' => 'date',
                    'format' => 'yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis',
                ],
                'user_id' => [
                    'type' => 'text',
                    'fields' => [
                        'keyword' => [
                            'type' => 'keyword',
                            'ignore_above' => 256
                        ]
                    ]
                ]
            ]
        ];

        return json_encode($json);
    }

    /**
     * Destroys the current session.
     *
     * @throws Exception
     */
    public static function session_destroy(): void
    {
        error_log("Destroying session...");
        try {
            es_execute("/sessions/_doc/" . _SLM::$appToken . "?refresh=true", "", "DELETE");
        } catch (Exception $e) {
            error_log($e->getMessage());
        }
        _SLM::$session = [];
        _SLM::$destroying = true;
    }

    /**
     * Terminates script execution and displays a status message.
     *
     * @param mixed $status Status message to display. Default is null.
     * @return void
     */
    public static function die(string $status = null): void
    {
        _SLM::saveSession();
        exit($status);
    }

    /**
     * Saves the session to Elasticsearch.
     *
     * @return void
     */
    public static function saveSession(): void
    {
        if (_SLM::$destroying) {
            error_log("destroying...");
            _SLM::$destroying = false;
            return;
        }

        if (_SLM::$session == null) {
            return;
        }

        error_log("=================================");
        error_log("= Updating session in DB...     =");
        error_log("=================================");

        if (_SLM::$startSession !== _SLM::$session) {
            error_log("=============================================");
            error_log("= Session updated, storing in DB...         =");
            error_log("=============================================");

            $tmp["timestamp"] = getTimestamp();
            $tmp["human_date"] = date("Y-m-d H:i:s", round($tmp["timestamp"] / 1000));
            $tmp["_server"] = json_encode($_SERVER);
            $tmp["session"] = json_encode(_SLM::$session);
            $tmp["environment"] = isProd() ? "prod" : "dev";
            $tmp["app"] = APP_SHORT_NAME;

            if (isset(_SLM::$session["user"]["id"])) {
                $tmp["user_id"] = _SLM::$session["user"]["id"];
            }

            try {
                $request = json_decode(es_execute("/sessions/_doc/" . _SLM::$appToken, json_encode($tmp), "POST"), true);
                if (isset($request['error'])) {
                    error_log("==============================================");
                    error_log("========= Session not stored in DB =========");
                    error_log("Error: ");
                    error_log($request['error']['reason']);
                    error_log("==============================================");
                }
            } catch (Exception $e) {
                error_log($e->getMessage());
            }
        }
    }

    /**
     * Loads the session from Elasticsearch.
     *
     * @return void
     */
    public static function loadSession(): void
    {
        try {
            _SLM::$startSession = json_decode(es_execute("/sessions/_doc/" . _SLM::$appToken), true);

            if (!isset(_SLM::$startSession["found"])) {
                error_log("===============================================");
                error_log("========= Session index not found     =========");
                error_log("===============================================");
                error_log("===============================================");
                error_log("================ Creating index ===============");
                error_log("===============================================");
                es_execute("/sessions", _SLM::getMapping(), "PUT");
                self::loadSession();
            }

            if (_SLM::$startSession["found"]) {
                $sessionID = _SLM::$startSession["_id"];

                _SLM::$startSession = json_decode(_SLM::$startSession["_source"]["session"], true);
                _SLM::$session = _SLM::$startSession;
                _SLM::$session["id"] = $sessionID;

                if (!empty(_SLM::$session["user"]["id"])) {
                    $user_id = _SLM::$session["user"]["id"];
                    $user = getObjectById($user_id);
                    // Check user exists
                    if (!empty($user)) {
                        // If object is not type "user" or not active, close session
                        if (($user['slm']['type'] ?? '') !== 'user' || $user['status'] !== 'active') {
                            self::logout();
                        }
                        // Get user's assigned profile
                        $user_profile = getObjectById($user['profile'] ?? '');
                        if ($user_profile) {
                            if (!empty($user_profile["acl"])) {
                                $user_profile["acl"] = $user_profile["acl"];
                                _SLM::$session["user"]["profile"] = $user_profile;
                                _SLM::$session["user"]["profile"]["id"] = $user["profile"];
                            }
                        }

                        // Load user-specific ACL if exists
                        if (!empty($user['acl'])) {
                            _SLM::$session["user"]["user_acl"] = $user['acl'];
                        }
                    } else {
                        self::logout();
                    }
                }
            } else {
                throw new Exception("Session " . _SLM::$appToken . " not found...");
            }
        } catch (Exception $e) {
            error_log($e->getMessage());
        }
    }

    /**
     * Check if user is authenticated.
     *
     * @return bool true if user is authenticated, false otherwise.
     */
    public static function auth(): bool
    {
        return isset(_SLM::$session['user']['id']);
    }

    /**
     * Logs out the user and redirects to the specified location.
     *
     * @param string $location URL to redirect after logout. Default is './?login'.
     * @return void
     */
    public static function logout(string $location = './?login'): void
    {
        try {
            _SLM::session_destroy();
            header("Location: " . $location);
            exit();
        } catch (Exception $e) {
            error_log($e->getMessage());
        }
    }

    /**
     * Gets the ACL (Access Control List) for the current user.
     * Merges profile ACL with user-specific ACL if present.
     *
     * @return array|null The merged ACL array or null if empty.
     */
    public static function getUserACL(): ?array
    {
        $profileACL = !empty(_SLM::$session["user"]["profile"]["acl"])
            ? _SLM::$session["user"]["profile"]["acl"]
            : null;

        $userACL = !empty(_SLM::$session["user"]["user_acl"])
            ? _SLM::$session["user"]["user_acl"]
            : null;

        // Si no hay ACL de usuario, retornar solo el del perfil
        if ($userACL === null) {
            return $profileACL;
        }

        // Si no hay ACL de perfil pero sí de usuario, retornar el del usuario
        if ($profileACL === null) {
            return $userACL;
        }

        // Fusionar ambos ACLs
        return self::mergeACLs($profileACL, $userACL);
    }

    /**
     * Merges profile ACL with user-specific ACL.
     * User ACL takes precedence over profile ACL at the module level.
     *
     * Strategy:
     * - Base: Profile ACL
     * - Override: For each module defined in user ACL, completely replace
     *   the permissions for that module (no merge at action level)
     * - Empty array in user ACL means revoke access to that module
     * - Modules not in user ACL inherit profile permissions unchanged
     *
     * @param array $profileACL The profile's ACL (base)
     * @param array $userACL The user's specific ACL (overrides)
     * @return array The merged ACL
     */
    private static function mergeACLs(array $profileACL, array $userACL): array
    {
        $mergedACL = $profileACL; // Start with profile as base

        // Iterate through each group in user ACL
        foreach ($userACL as $groupId => $modules) {
            // If group doesn't exist in profile, add it entirely
            if (!isset($mergedACL[$groupId])) {
                $mergedACL[$groupId] = $modules;
                continue;
            }

            // For each module in this group, override profile permissions
            foreach ($modules as $moduleKey => $permissions) {
                // User permissions completely replace profile permissions for this module
                $mergedACL[$groupId][$moduleKey] = $permissions;
            }
        }

        return $mergedACL;
    }

    /**
     * Finds a key in a multidimensional array where the specified value is found.
     *
     * @param string|array $array The array to search.
     * @param mixed $needle The value to find.
     * @return int|string|null The key where the value was found, or null if not found.
     */
    public static function findKey(string|array $array, mixed $needle): int|string|null
    {
        if (!is_array($array)) {
            return null;
        }

        foreach ($array as $key => $value) {
            if (is_array($value) && array_key_exists($needle, $value)) {
                return $key;
            }
        }
        return null;
    }

    /**
     * Gets security access permissions for a given view.
     *
     * @param string $view The view to check permissions for.
     * @param array|null $acl The ACL to use. If null, uses user's ACL.
     * @return array The permissions associated with the requested resource.
     */
    public static function getSecurityAccess(string $view, array $acl = null): array
    {
        if (!is_array($acl)) {
            $acl = self::getUserACL();
        }

        $key = self::findKey($acl, $view);
        if (empty($key)) {
            return [];
        }

        return $acl[$key][$view];
    }

    /**
     * Switches the current session to the specified user.
     *
     * @param string $user_id The ID of the user to switch to.
     * @return array The result with "error" and "message" fields.
     */
    public static function switchSession(string $user_id): array
    {
        $response = ['error' => true, 'message' => 'User not found'];

        $log_type = 'error';
        $log_data = [];
        try {
            // Find the user to switch to
            $user = getObjectById($user_id, '_source_excludes=password');
            if (!$user) {
                return $response;
            }

            if (isset(_SLM::$session['root']) && _SLM::$session['root'] != $user_id) {
                $root = _SLM::$session['user']['id'];
            }

            if (!isset(_SLM::$session['root'])) {
                $root = _SLM::$session['user']['id'];
            }

            // Destroy current session
            _SLM::session_destroy();

            if (isset($root)) {
                _SLM::$session['root'] = $root;
            }

            _SLM::$session['user']['id'] = $user_id;
            _SLM::$session['user']['img'] = $user['img'] ?? '';
            _SLM::$session['user']['full_name'] = ($user['name'] ?? '') . ' ' . ($user['lastname'] ?? '');
            _SLM::$session['user']['language'] = $user['language'] ?? 'es';
            _SLM::$session['user']['tmp_password'] = $user['tmp_password'] ?? false;
            _SLM::$session['user']['mode'] = $user['mode'] ?? 'light';

            _SLM::$session['user'] = array_merge($user, _SLM::$session['user']);

            _SLM::saveSession();

            $response['error'] = false;
            $response['message'] = '';

            $log_type = 'success';
            $log_data['return'] = $response;
        } catch (Exception $e) {
            $response['message'] = $e->getMessage();
            $log_data['return'] = $response;
        } finally {
            addToLog($log_type, 'system', __FUNCTION__, $log_data);
        }

        return $response;
    }
}
