// 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 APIError = require("../../api/APIError");
const Group = require("../../entities/Group");
const { DENY_SERVICE_INSTRUCTION } = require("../AnomalyService");
const BaseService = require("../BaseService");
const { DB_WRITE } = require("../database/consts");


/**
* The GroupService class provides functionality for managing groups within the Puter application.
* It extends the BaseService to handle group-related operations such as creation, retrieval,
* listing members, adding or removing users from groups, and more. This service interacts with
* the database to perform CRUD operations on group entities, ensuring proper management
* of user permissions and group metadata.
*/
class GroupService extends BaseService {
    static MODULES = {
        uuidv4: require('uuid').v4,
    };


    /**
    * Initializes the GroupService by setting up the database connection and registering
    * with the anomaly service for monitoring group creation rates.
    * 
    * @memberof GroupService
    * @instance
    */
    _init () {
        this.db = this.services.get('database').get(DB_WRITE, 'permissions');

        const svc_anomaly = this.services.get('anomaly');
        svc_anomaly.register('groups-user-hour', {
            high: 20,
        });
    }
    

    /**
    * Retrieves a group by its unique identifier (UID).
    * 
    * @param {Object} params - The parameters object.
    * @param {string} params.uid - The unique identifier of the group.
    * @returns {Promise<Object|undefined>} The group object if found, otherwise undefined.
    * @throws {Error} If there's an issue with the database query.
    * 
    * This method fetches a group from the database using its UID. If the group 
    * does not exist, it returns undefined. The 'extra' and 'metadata' fields are 
    * parsed from JSON strings to objects if not using MySQL, otherwise they remain 
    * as strings.
    */
    async get({ uid }) {
        const [group] =
            await this.db.read('SELECT * FROM `group` WHERE uid=?', [uid]);
        if ( ! group ) return;
        group.extra = this.db.case({
            mysql: () => group.extra,
            otherwise: () => JSON.parse(group.extra),
        })();
        group.metadata = this.db.case({
            mysql: () => group.metadata,
            otherwise: () => JSON.parse(group.metadata),
        })();
        return group;
    }
    

    /**
    * Creates a new group with the provided owner, extra data, and metadata.
    * This method performs rate limiting checks to prevent abuse, generates a unique identifier for the group,
    * and handles the database insertion of the group details.
    * 
    * @param {Object} options - The options object for creating a group.
    * @param {string} options.owner_user_id - The ID of the user who owns the group.
    * @param {Object} [options.extra] - Additional data associated with the group.
    * @param {Object} [options.metadata] - Metadata for the group, which can be used for various purposes.
    * @returns {Promise<string>} - A promise that resolves to the unique identifier of the newly created group.
    * @throws {APIError} If the rate limit is exceeded.
    */
    async create ({ owner_user_id, extra, metadata }) {
        extra = extra ?? {};
        metadata = metadata ?? {};
        
        const uid = this.modules.uuidv4();

        const [{ n_groups }] = await this.db.read(
            "SELECT COUNT(*) AS n_groups FROM `group` WHERE " +
            "owner_user_id=? AND " +
            "created_at >= datetime('now', '-1 hour')",
            [owner_user_id]
        );

        const svc_anomaly = this.services.get('anomaly');
        const anomaly = await svc_anomaly.note('groups-user-hour', {
            value: n_groups,
            user_id: owner_user_id,
        });

        if ( anomaly && anomaly.has(DENY_SERVICE_INSTRUCTION) ) {
            throw APIError.create('too_many_requests');
        }

        await this.db.write(
            'INSERT INTO `group` ' +
            '(`uid`, `owner_user_id`, `extra`, `metadata`) ' +
            'VALUES (?, ?, ?, ?)',
            [
                uid, owner_user_id,
                JSON.stringify(extra),
                JSON.stringify(metadata),
            ]
        );
        
        return uid;
    }


    /**
    * Lists all groups where the specified user is a member.
    * 
    * This method queries the database to find groups associated with the given user_id through the junction table `jct_user_group`.
    * Each group's `extra` and `metadata` fields are parsed based on the database type to ensure compatibility.
    *
    * @param {Object} params - Parameters for the query.
    * @param {string} params.user_id - The ID of the user whose groups are to be listed.
    * @returns {Promise<Array<Group>>} A promise that resolves to an array of Group objects representing groups the user is a member of.
    */
    async list_groups_with_owner ({ owner_user_id }) {
        const groups = await this.db.read(
            'SELECT * FROM `group` WHERE owner_user_id=?',
            [owner_user_id],
        );
        for ( const group of groups ) {
            group.extra = this.db.case({
                mysql: () => group.extra,
                otherwise: () => JSON.parse(group.extra),
            })();
            group.metadata = this.db.case({
                mysql: () => group.metadata,
                otherwise: () => JSON.parse(group.metadata),
            })();
        }
        return groups.map(g => Group(g));
    }


    /**
    * Lists all groups where the specified user is a member.
    * 
    * @param {Object} options - The options object.
    * @param {string} options.user_id - The ID of the user whose group memberships are to be listed.
    * @returns {Promise<Array>} A promise that resolves to an array of Group objects representing the groups the user is a member of.
    */
    async list_groups_with_member ({ user_id }) {
        const groups = await this.db.read(
            'SELECT * FROM `group` WHERE id IN (' +
                'SELECT group_id FROM `jct_user_group` WHERE user_id=?)',
            [user_id],
        );
        for ( const group of groups ) {
            group.extra = this.db.case({
                mysql: () => group.extra,
                otherwise: () => JSON.parse(group.extra),
            })();
            group.metadata = this.db.case({
                mysql: () => group.metadata,
                otherwise: () => JSON.parse(group.metadata),
            })();
        }
        return groups.map(g => Group(g));
    }


    /**
    * Lists the members of a group by their username.
    * 
    * @param {Object} options - The options object.
    * @param {string} options.uid - The unique identifier of the group.
    * @returns {Promise<string[]>} A promise that resolves to an array of usernames of the group members.
    */
    async list_members ({ uid }) {
        const users = await this.db.read(
            'SELECT u.username FROM user u ' +
            'JOIN (SELECT user_id FROM `jct_user_group` WHERE group_id = ' +
                '(SELECT id FROM `group` WHERE uid=?)) ug ' +
            'ON u.id = ug.user_id',
            [uid],
        );
        return users.map(u => u.username);
    }
    

    /**
    * Adds specified users to a group.
    * 
    * @param {Object} options - The options object.
    * @param {string} options.uid - The unique identifier of the group.
    * @param {string[]} options.users - An array of usernames to add to the group.
    * @returns {Promise<void>} A promise that resolves when the users have been added.
    * @throws {APIError} If there's an issue with the database operation or if the group does not exist.
    */
    async add_users ({ uid, users }) {
        const question_marks =
            '(' + Array(users.length).fill('?').join(', ') + ')';
        await this.db.write(
            'INSERT INTO `jct_user_group` ' +
            '(user_id, group_id) ' +
            'SELECT u.id, g.id FROM user u '+
            'JOIN (SELECT id FROM `group` WHERE uid=?) g ON 1=1 ' +
            'WHERE u.username IN ' +
            question_marks,
            [uid, ...users],
        );
    }
    

    /**
    * Removes specified users from a group.
    * 
    * This method deletes the association between users and a group from the junction table.
    * It uses the group's uid to identify the group and an array of usernames to remove.
    * 
    * @param {Object} params - The parameters for the operation.
    * @param {string} params.uid - The unique identifier of the group.
    * @param {string[]} params.users - An array of usernames to be removed from the group.
    * @returns {Promise<void>} A promise that resolves when the operation is complete.
    */
    async remove_users ({ uid, users }) {
        const question_marks =
            '(' + Array(users.length).fill('?').join(', ') + ')';
        /*
DELETE FROM `jct_user_group`
WHERE group_id = 1
AND user_id IN (
    SELECT u.id
    FROM user u
    WHERE u.username IN ('user_that_shares', 'user_that_gets_shared_to')
);
        */
        await this.db.write(
            'DELETE FROM `jct_user_group` ' +
            'WHERE group_id = (SELECT id FROM `group` WHERE uid=?) ' +
            'AND user_id IN (' +
                'SELECT u.id FROM user u ' +
                'WHERE u.username IN ' +
                question_marks +
            ')',
            [uid, ...users],
        );
    }
}

module.exports = {
    GroupService,
};
