<?php

// Global variable for encryption key
$secretKey = '';

include_once "slm_es_control.slm";

//======================================================================================================================
// General Application Functions
//======================================================================================================================

/**
 * Attempts to login a user with email and password.
 *
 * @param string $email User's email
 * @param string $md5_password User's password (MD5 hashed)
 * @return array Login response with error and message keys
 */
function attemptLogin(string $email, string $md5_password): array
{
    $response = _response(true);
    $es_index = getEsIndex();

    $log_type = 'error';
    $log_data = [];
    try {
        $email = strtolower(trim($email));

        $query = "/$es_index/_search?q=(slm.type:user)AND(email.keyword:\"$email\")AND(password:$md5_password)&_source_excludes=password";

        $request = json_decode(es_execute($query), true);

        if (isset($request['error'])) {
            throw new Exception($request['error']['reason']);
        }

        if ($request['hits']['total']['value'] == 0) {
            throw new Exception('Invalid password.');
        }

        $response['error'] = false;
        $response['message'] = 'Login successful.';

        $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;
}

/**
 * Gets the menu items based on user permissions.
 *
 * @param bool $getAll If true, retrieves all menu items regardless of permissions
 * @param array|null $acl Access Control List to use
 * @return array Array of menu items
 */
function getMenu(bool $getAll = false, $acl = null): array
{
    $menu = [];
    $section = [];

    // Home
    if ($getAll || _SLM::getSecurityAccess("home.main", $acl)) {
        $module["folder"] = "home.main";
        $module["name"] = "Home";
        $module["visible"] = false;
        $section["modules"][] = $module;
        unset($module);
    }

    if (!empty($section)) {
        $section["name"] = "Inicio";
        $section["icon"] = "fe fe-home";
        $section["visible"] = false;
        $menu[] = $section;
        $section = [];
    }

    // Sección: Inversiones
    if ($getAll || _SLM::getSecurityAccess("proyectos.catalogo", $acl)) {
        $module["folder"] = "proyectos.catalogo";
        $module["name"] = "Catálogo de Proyectos";
        $module["visible"] = true;
        $section["modules"][] = $module;
        unset($module);
    }

    if ($getAll || _SLM::getSecurityAccess("proyectos.admin", $acl)) {
        $module["folder"] = "proyectos.admin";
        $module["name"] = "Administrar Proyectos";
        $module["visible"] = true;
        $section["modules"][] = $module;
        unset($module);
    }

    if (!empty($section)) {
        $section["name"] = "Inversiones";
        $section["icon"] = "fe fe-trending-up";
        $section["visible"] = true;
        $menu[] = $section;
        $section = [];
    }

    // Sección: Herramientas
    if ($getAll || _SLM::getSecurityAccess("simulador.calcular", $acl)) {
        $module["folder"] = "simulador.calcular";
        $module["name"] = "Simulador de Inversión";
        $module["visible"] = true;
        $section["modules"][] = $module;
        unset($module);
    }

    if (!empty($section)) {
        $section["name"] = "Herramientas";
        $section["icon"] = "fe fe-tool";
        $section["visible"] = true;
        $menu[] = $section;
        $section = [];
    }

    // Sección: Educación
    if ($getAll || _SLM::getSecurityAccess("aprendizaje.videos", $acl)) {
        $module["folder"] = "aprendizaje.videos";
        $module["name"] = "Videos Educativos";
        $module["visible"] = true;
        $section["modules"][] = $module;
        unset($module);
    }

    if ($getAll || _SLM::getSecurityAccess("aprendizaje.admin", $acl)) {
        $module["folder"] = "aprendizaje.admin";
        $module["name"] = "Administrar Videos";
        $module["visible"] = true;
        $section["modules"][] = $module;
        unset($module);
    }

    if (!empty($section)) {
        $section["name"] = "Educación";
        $section["icon"] = "fe fe-book-open";
        $section["visible"] = true;
        $menu[] = $section;
        $section = [];
    }

    // Sección: Entidades
    if ($getAll || _SLM::getSecurityAccess("desarrolladores.lista", $acl)) {
        $module["folder"] = "desarrolladores.lista";
        $module["name"] = "Desarrolladores";
        $module["visible"] = true;
        $section["modules"][] = $module;
        unset($module);
    }

    if ($getAll || _SLM::getSecurityAccess("constructoras.lista", $acl)) {
        $module["folder"] = "constructoras.lista";
        $module["name"] = "Constructoras";
        $module["visible"] = true;
        $section["modules"][] = $module;
        unset($module);
    }

    if (!empty($section)) {
        $section["name"] = "Entidades";
        $section["icon"] = "fe fe-users";
        $section["visible"] = true;
        $menu[] = $section;
        $section = [];
    }

    // Example Items (CRUD example module)
    if ($getAll || _SLM::getSecurityAccess("example.items", $acl)) {
        $module["folder"] = "example.items";
        $module["name"] = "Items (Example)";
        $module["visible"] = true;
        $section["modules"][] = $module;
        unset($module);
    }

    if (!empty($section)) {
        $section["name"] = "Examples";
        $section["icon"] = "fe fe-layers";
        $section["visible"] = true;
        $menu[] = $section;
        $section = [];
    }

    return $menu;
}

//======================================================================================================================
// General API Functions
//======================================================================================================================

/**
 * Adds a log entry to the Elasticsearch log index.
 *
 * @param string $type Log type (success, error, warning)
 * @param string $module Module name
 * @param string $script Script name
 * @param array $data Associated data
 * @param array $debug Debug data
 * @param array $log Additional log data
 * @return array The created log entry data
 */
function addToLog(string $type, string $module, string $script, array $data, array $debug = [], array $log = []): array
{
    $es_index_log = getEsIndexLog();

    if (class_exists('_SLM')) {
        $slm_session = _SLM::$session ?? [];
        $user_email = _SLM::$session["user"]["email"] ?? '';
        $user_id = _SLM::$session["user"]["id"] ?? '';
    } else {
        $slm_session = [];
        $user_email = '';
        $user_id = '';
    }

    $debug["session"] = $slm_session;
    $debug["server"] = $_SERVER ?? null;
    $debug["files"] = $_FILES ?? null;
    $debug["post"] = $_POST ?? null;

    $timestamp = getTimestamp();

    $log['log_type'] = $type;
    $log['module'] = $module;
    $log['script'] = $script;
    $log['user_id'] = $user_id;
    $log['user_email'] = $user_email;
    $log['data'] = json_encode($data);
    $log['debug'] = json_encode($debug);
    $log['slm'] = [
        'type' => 'log',
        'timestamp' => $timestamp,
        'date' => date('Y-m-d H:i:s', round($timestamp / 1000)),
    ];

    try {
        $valid_types = ["success", "error", "warning"];
        if (!in_array($type, $valid_types)) {
            throw new InvalidArgumentException("Invalid log type $type. Must be one of: " . implode(", ", $valid_types));
        }

        $request = json_decode(es_execute("/$es_index_log/_doc/", json_encode($log)), true);
        if (isset($request['error'])) {
            throw new RuntimeException('Error creating log entry. Reason: ' . $request['error']['reason']);
        }

        if ($request['result'] !== 'created') {
            throw new RuntimeException('Log entry was not created, try again later.');
        }
    } catch (Exception $e) {
        error_log($e->getMessage());
    }

    return [
        'log_type' => $log['log_type'],
        'module' => $log['module'],
        'script' => $log['script'],
        'data' => $data,
        'debug' => $debug,
        'slm' => [
            'timestamp' => $log['slm']['timestamp'],
            'date' => $log['slm']['date']
        ]
    ];
}

/**
 * Safely decodes a JSON string and returns an associative array.
 *
 * @param string $json The JSON string to decode
 * @return array The resulting array, or empty array if decoding fails
 */
function safeJsonDecode(string $json): array
{
    return json_decode($json, true) ?? [];
}

/**
 * Gets an object by its identifier.
 *
 * @param string $id The object ID
 * @return array|null The object as an array, or null if not found
 */
function fetchObjectById(string $id): ?array
{
    try {
        $object = getObjectById($id, '_source_excludes=type,status,timestamp');
        return $object ?: null;
    } catch (Exception $e) {
        return null;
    }
}

/**
 * Procesa un elemento de módulo obteniendo su objeto de carpeta asociado.
 *
 * @param array $module_item El elemento del módulo que se va a procesar. Debe contener una clave 'carpeta' con el ID de la carpeta como valor.
 * @return array|null El elemento del módulo procesado con la clave 'folder' sustituida por el objeto carpeta correspondiente, o null si no se ha podido obtener la carpeta.
 * @throws Exception
 */
function processModuleItem(array $module_item): ?array
{
    try {
        $folder = fetchObjectById($module_item['folder']);
        if (!$folder) {
            return null;
        }
        $module_item['folder'] = $folder;
        return $module_item;
    } catch (Exception $e) {
        return null;
    }
}

/**
 * Procesa un elemento del menú y completa sus datos, incluyendo módulos y carpetas.
 *
 * @param array $menu_item El elemento del menú que debe procesarse, representado como un array asociativo.
 * @return array|null El elemento del menú procesado con información adicional, o null si no se encuentra el elemento.
 * @throws Exception
 */
function processMenuItem(array $menu_item): ?array
{
    try {
        $item = fetchObjectById($menu_item['item']);
        if (!$item) {
            return null;
        }
        //
        if (isset($item['module'])) {
            $module = $item['module'];
            $item['module'] = array_map('processModuleItem', $module);
        }
        //
        $folder = fetchObjectById($item['folder'] ?? '');
        $item['folder'] = $folder['folder'] ?? '';
        //
        return $item;
    } catch (Exception $e) {
        return null;
    }
}

/**
 * Obtiene y procesa los menús de grupo almacenados.
 *
 * @return array Matriz de menús de grupo, donde cada menú de grupo es una matriz asociativa que incluye los elementos
 *               del menú y otra información relevante del menú de grupo.
 *               Devuelve una matriz vacía si se produce un error o no se encuentra ningún menú de grupo.
 */
function fetchGroupMenus(): array
{
    $es_index = getEsIndex();
    try {
        $request = safeJsonDecode(es_execute("/$es_index/_search?q=(slm.type:group_menu)&_source_excludes=type,status,timestamp&size=1000&sort=order:asc"));
        //
        $groups = [];
        if (!isset($request['error']) && ($request['hits']['total']['value'] > 0)) {
            $groups = array_map(function ($hit) {
                $source = $hit['_source'];
                $menu = $source['menu'];
                $source['menu'] = array_map('processMenuItem', $menu);
                //
                return array_merge(['id' => $hit['_id']], $source);
            }, $request['hits']['hits']);
        }
        //
        return $groups;
    } catch (Exception $e) {
        return [];
    }
}

/**
 * Filtra un menú en función de los permisos dados.
 *
 * @param array $menu El menú original, compuesto por elementos de menú, cada uno de los cuales puede contener submenús y módulos.
 * @param array $permissions Los permisos para los elementos del menú. La estructura debe alinearse con el menú para que el filtrado se aplique correctamente.
 * @return array El menú filtrado que incluye sólo los elementos y subelementos que permiten los permisos.
 */
function filterMenuWithPermissions(array $menu, array $permissions): array
{
    $filteredMenu = [];
    //
    foreach ($menu as $menuItem) {
        if (isset($permissions[$menuItem['id']])) {
            $filteredSubMenu = [];
            foreach ($menuItem['menu'] as $subMenu) {
                if (isset($subMenu['folder']) && isset($permissions[$menuItem['id']][$subMenu['folder']])) {
                    $filteredSubMenu[] = $subMenu;
                } elseif (isset($subMenu['module'])) {
                    $filteredModules = [];
                    foreach ($subMenu['module'] as $module) {
                        if (isset($module['folder']['folder']) && isset($permissions[$menuItem['id']][$module['folder']['folder']])) {
                            $filteredModules[] = $module;
                        }
                    }
                    //
                    if (!empty($filteredModules)) {
                        $subMenu['module'] = $filteredModules;
                        $filteredSubMenu[] = $subMenu;
                    }
                }
            }
            //
            if (!empty($filteredSubMenu)) {
                $menuItem['menu'] = $filteredSubMenu;
                $filteredMenu[] = $menuItem;
            }
        }
    }
    //
    return $filteredMenu;
}

/**
 * Sets the global secret key.
 *
 * @param string $key The new secret key
 * @return void
 */
function setSecretKey(string $key): void
{
    global $secret_key;
    $secret_key = $key;
}

/**
 * Encrypts data using the global secret key.
 *
 * @param string $data The data to encrypt
 * @return string|false The encrypted data in base64, or false on failure
 * @throws Exception If the secret key is not defined
 */
function encrypt(string $data): string|false
{
    global $secret_key;

    if (empty($secret_key)) {
        throw new Exception('Secret key is not defined');
    }

    $ivLength = openssl_cipher_iv_length(CIPHER_METHOD);
    $iv = openssl_random_pseudo_bytes($ivLength);

    $encryptedData = openssl_encrypt($data, CIPHER_METHOD, $secret_key, 0, $iv);

    if ($encryptedData === false) {
        return false;
    }

    return base64_encode($iv . $encryptedData);
}

/**
 * Decrypts data using the global secret key.
 *
 * @param string $data The encrypted data to decrypt
 * @return string|false The decrypted data, or false on failure
 * @throws Exception If the secret key is not defined
 */
function decrypt(string $data): string|false
{
    global $secret_key;

    if (empty($secret_key)) {
        throw new Exception('Secret key is not defined');
    }

    $data = base64_decode($data);
    $ivLength = openssl_cipher_iv_length(CIPHER_METHOD);
    $iv = substr($data, 0, $ivLength);
    $ciphertext = substr($data, $ivLength);

    return openssl_decrypt($ciphertext, CIPHER_METHOD, $secret_key, 0, $iv);
}
