// METADATA // {"ai-commented":{"service":"xai"}}
/*
 * Copyright (C) 2024-present Puter Technologies Inc.
 *
 * This file is part of Puter.
 *
 * Puter is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
const { invalidate_cached_user, get_user } = require("../helpers");
const { asyncSafeSetInterval } = require('@heyputer/putility').libs.promise;
const { MINUTE, SECOND } = require("@heyputer/putility").libs.time;
const BaseService = require("./BaseService");
const { DB_WRITE } = require("./database/consts");

/**
 * This service is responsible for updating session activity
 * timestamps and maintaining the number of active sessions.
 */
/**
* @class SessionService
* @description
* The SessionService class manages session-related operations within the Puter application.
* It handles the creation, retrieval, updating, and deletion of user sessions. This service:
* - Tracks session activity with timestamps.
* - Maintains a cache of active sessions.
* - Periodically updates session information in the database.
* - Ensures the integrity of session data across different parts of the application.
* - Provides methods to interact with sessions, including session creation, retrieval, and termination.
*/
class SessionService extends BaseService {
    static MODULES = {
        // uuidv5: require('uuid').v5,
        uuidv4: require('uuid').v4,
    }


    _construct () {
        this.sessions = {};
    }


    /**
    * Initializes the session storage by setting up the database connection
    * and starting a periodic session update interval.
    * 
    * @async
    * @memberof SessionService
    * @method _init
    */
    async _init () {
        this.db = await this.services.get('database').get(DB_WRITE, 'session');

        (async () => {
            // TODO: change to 5 minutes or configured value
            /**
            * Initializes periodic session updates.
            * 
            * This method sets up an interval to call `_update_sessions` every 2 minutes.
            * 
            * @memberof SessionService
            * @private
            * @async
            * @param {none} - No parameters are required.
            * @returns {Promise<void>} - Resolves when the interval is set.
            */
            asyncSafeSetInterval(async () => {
                await this._update_sessions();
            }, 2 * MINUTE);
        })();
    }


    /**
    * Creates a new session for the specified user and records metadata about
    * the requestor.
    * 
    * @async
    * @returns {Promise<Session>} A new session object
    */
    async create_session (user, meta) {
        const unix_ts = Math.floor(Date.now() / 1000);

        meta = {
            // clone
            ...(meta || {}),
        };
        meta.created = new Date().toISOString();
        meta.created_unix = unix_ts;
        const uuid = this.modules.uuidv4();
        await this.db.write(
            'INSERT INTO `sessions` ' +
            '(`uuid`, `user_id`, `meta`, `last_activity`, `created_at`) ' +
            'VALUES (?, ?, ?, ?, ?)',
            [uuid, user.id, JSON.stringify(meta), unix_ts, unix_ts],
        );
        const session = {
            last_touch: Date.now(),
            last_store: Date.now(),
            uuid,
            user_uid: user.uuid,
            user_id: user.id,
            meta,
        };
        this.sessions[uuid] = session;

        return session;
    }


    /**
    * Retrieves a session by its UUID, updates the session's last touch timestamp, 
    * and prepares the session data for external use by removing internal values.
    * 
    * @param {string} uuid - The UUID of the session to retrieve.
    * @returns {Object|undefined} The session object with internal values removed, or undefined if the session does not exist.
    */
    async get_session_ (uuid) {
        let session = this.sessions[uuid];
        if ( session ) {
            session.last_touch = Date.now();
            return session;
        }
        ;[session] = await this.db.read(
            "SELECT * FROM `sessions` WHERE `uuid` = ? LIMIT 1",
            [uuid],
        );
        if ( ! session ) return;
        session.last_store = Date.now();
        session.meta = this.db.case({
            mysql: () => session.meta,
            /**
            * Parses session metadata based on the database type.
            * @param {Object} session - The session object from the database.
            * @returns {Object} The parsed session metadata.
            */
            otherwise: () => JSON.parse(session.meta ?? "{}")
        })();
        const user = await get_user(session.user_id);
        session.user_uid = user?.uuid;
        this.sessions[uuid] = session;
        return session;
    }
    /**
    * Retrieves a session by its UUID, updates its last touch time, and prepares it for external use.
    * @param {string} uuid - The unique identifier for the session to retrieve.
    * @returns {Promise<Object|undefined>} The session object with internal values removed, or undefined if not found.
    */
    async get_session (uuid) {
        const session = await this.get_session_(uuid);
        if ( session ) {
            session.last_touch = Date.now();
            session.meta.last_activity = (new Date()).toISOString();
        }
        return this.remove_internal_values_(session);
    }

    remove_internal_values_ (session) {
        if ( session === undefined ) return;

        const copy = {
            ...session,
        };
        delete copy.last_touch;
        delete copy.last_store;
        delete copy.user_id;
        return copy;
    }

    get_user_sessions (user) {
        const sessions = [];
        for ( const session of Object.values(this.sessions) ) {
            if ( session.user_id === user.id ) {
                sessions.push(session);
            }
        }
        return sessions.map(this.remove_internal_values_.bind(this));
    }

    /**
    * Removes a session from the in-memory cache and the database.
    * 
    * @param {string} uuid - The UUID of the session to remove.
    * @returns {Promise} A promise that resolves to the result of the database write operation.
    */
    remove_session (uuid) {
        delete this.sessions[uuid];
        return this.db.write(
            'DELETE FROM `sessions` WHERE `uuid` = ?',
            [uuid],
        );
    }


    async _update_sessions () {
        this.log.tick('UPDATING SESSIONS');
        const now = Date.now();
        const keys = Object.keys(this.sessions);

        const user_updates = {};

        for ( const key of keys ) {
            const session = this.sessions[key];
            if ( now - session.last_store > 5 * MINUTE ) {
                this.log.debug('storing session meta: ' + session.uuid);
                const unix_ts = Math.floor(now / 1000);
                const { anyRowsAffected } = await this.db.write(
                    'UPDATE `sessions` ' +
                    'SET `meta` = ?, `last_activity` = ? ' +
                    'WHERE `uuid` = ?',
                    [JSON.stringify(session.meta), unix_ts, session.uuid],
                );

                if ( ! anyRowsAffected ) {
                    delete this.sessions[key];
                    continue;
                }

                session.last_store = now;
                if (
                    ! user_updates[session.user_id] ||
                    user_updates[session.user_id][1] < session.last_touch
                ) {
                    user_updates[session.user_id] = [session.user_id, session.last_touch];
                }
            }
        }

        for ( const [user_id, last_touch] of Object.values(user_updates) ) {
            const sql_ts = (date =>
                date.toISOString().split('T')[0] + ' '
                + date.toTimeString().split(' ')[0]
            )(new Date(last_touch));

            await this.db.write(
                'UPDATE `user` ' +
                'SET `last_activity_ts` = ? ' +
                'WHERE `id` = ? LIMIT 1',
                [sql_ts, user_id],
            );
            const user = kv.get('users:id:' + user_id);
            if ( user ) {
                user.last_activity_ts = sql_ts;
            }
        }
    }
}

module.exports = { SessionService };
