// METADATA // {"ai-commented":{"service":"openai-completion","model":"gpt-4o"}}
/*
 * 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 { get_user } = require("../helpers");
const configurable_auth = require("../middleware/configurable_auth");
const featureflag = require("../middleware/featureflag.js");
const { Endpoint } = require("../util/expressutil");
const { whatis } = require("../util/langutil");
const { Actor, UserActorType } = require("./auth/Actor");
const BaseService = require("./BaseService");
const { DB_WRITE } = require("./database/consts");
const { UsernameNotifSelector } = require("./NotificationService");

class ShareService extends BaseService {
    static MODULES = {
        uuidv4: require('uuid').v4,
        validator: require('validator'),
        express: require('express'),
    };


    async _init () {
        this.db = await this.services.get('database').get(DB_WRITE, 'share');

        // registry "share" as a feature flag so gui is informed
        // about whether or not a user has access to this feature
        const svc_featureFlag = this.services.get('feature-flag');
        svc_featureFlag.register('share', {
            $: 'function-flag',
            fn: async ({ actor }) => {
                const user = actor.type.user ?? null;
                if ( ! user ) {
                    throw new Error('expected user');
                }
                return !! user.email_confirmed;
            }
        });

        const svc_event = this.services.get('event');
        svc_event.on('user.email-confirmed', async (_, { user_uid, email }) => {
            const user = await get_user({ uuid: user_uid });
            const relevant_shares = await this.db.read(
                'SELECT * FROM share WHERE recipient_email = ?',
                [email],
            );

            for ( const share of relevant_shares ) {
                share.data = this.db.case({
                    mysql: () => share.data,
                    otherwise: () =>
                        JSON.parse(share.data ?? '{}'),
                })();

                const issuer_user = await get_user({
                    id: share.issuer_user_id,
                });

                if ( ! issuer_user ) {
                    continue;
                }

                const issuer_actor = await Actor.create(UserActorType, {
                    user: issuer_user,
                });

                const svc_acl = this.services.get('acl');

                for ( const permission of share.data.permissions ) {
                    await svc_acl.set_user_user(
                        issuer_actor, user.username, permission, undefined,
                        { only_if_higher: true },
                    );
                }

                await this.db.write(
                    'DELETE FROM share WHERE uid = ?',
                    [share.uid],
                );
            }
        });
    }

    ['__on_install.routes'] (_, { app }) {
        this.install_sharelink_endpoints({ app });
        this.install_share_endpoint({ app });
    }
    
    /**
    * This method is responsible for processing the share link application request.
    * It checks if the share token is valid and if the user making the request is the intended recipient.
    * If both conditions are met, it grants the requested permissions to the user and deletes the share from the database.
    *
    * @param {Object} req - Express request object.
    * @param {Object} res - Express response object.
    * @returns {Promise<void>}
    */
    install_sharelink_endpoints ({ app }) {
        // track: scoping iife
        const router = (() => {
            const require = this.require;
            const express = require('express');
            return express.Router();
        })();
        
        app.use('/sharelink', router);
        
        const svc_share = this.services.get('share');
        const svc_token = this.services.get('token');
        
        Endpoint({
            route: '/check',
            methods: ['POST'],
            handler: async (req, res) => {
                // Potentially confusing:
                //   The "share token" and "share cookie token" are different!
                //   -> "share token" is from the email link;
                //      it has a longer expiry time and can be used again
                //      if the share session expires.
                //   -> "share cookie token" lets the backend know it
                //      should grant permissions when the correct user
                //      is logged in.
                
                const share_token = req.body.token;
                
                if ( ! share_token ) {
                    throw APIError.create('field_missing', null, {
                        key: 'token',
                    });
                }
                
                const decoded = await svc_token.verify('share', share_token);
                console.log('decoded?', decoded);
                if ( decoded.$ !== 'token:share' ) {
                    throw APIError.create('invalid_token');
                }
                
                const share = await svc_share.get_share({
                    uid: decoded.uid,
                });
                
                if ( ! share ) {
                    throw APIError.create('invalid_token');
                }
                
                res.json({
                    $: 'api:share',
                    uid: share.uid,
                    email: share.recipient_email,
                });
            },
        }).attach(router);
        
        Endpoint({
            route: '/apply',
            methods: ['POST'],
            mw: [configurable_auth()],
            handler: async (req, res) => {
                const share_uid = req.body.uid;
                
                const share = await svc_share.get_share({
                    uid: share_uid,
                });
                
                if ( ! share ) {
                    throw APIError.create('share_expired');
                }
                
                share.data = this.db.case({
                    mysql: () => share.data,
                    otherwise: () =>
                        JSON.parse(share.data ?? '{}'),
                })();
                
                const actor = Actor.adapt(req.actor ?? req.user);
                if ( ! actor ) {
                    // this shouldn't happen; auth should catch it
                    throw new Error('actor missing');
                }
                
                if ( ! actor.type.user.email_confirmed ) {
                    throw APIError.create('email_must_be_confirmed');
                }
                
                if ( actor.type.user.email !== share.recipient_email ) {
                    throw APIError.create('can_not_apply_to_this_user');
                }
                
                const issuer_user = await get_user({
                    id: share.issuer_user_id,
                });
                
                if ( ! issuer_user ) {
                    throw APIError.create('share_expired');
                }
                
                const issuer_actor = await Actor.create(UserActorType, {
                    user: issuer_user,
                });
                
                const svc_permission = this.services.get('permission');
                
                for ( const permission of share.data.permissions ) {
                    await svc_permission.grant_user_user_permission(
                        issuer_actor,
                        actor.type.user.username,
                        permission,
                    );
                }

                await this.db.write(
                    'DELETE FROM share WHERE uid = ?',
                    [share.uid],
                );
                
                res.json({
                    $: 'api:status-report',
                    status: 'success',
                });
            }
        }).attach(router);

        Endpoint({
            route: '/request',
            methods: ['POST'],
            mw: [configurable_auth()],
            handler: async (req, res) => {
                const share_uid = req.body.uid;
                
                const share = await svc_share.get_share({
                    uid: share_uid,
                });
                
                // track: null check before processing
                if ( ! share ) {
                    throw APIError.create('share_expired');
                }
                
                share.data = this.db.case({
                    mysql: () => share.data,
                    otherwise: () =>
                        JSON.parse(share.data ?? '{}'),
                })();
                
                const actor = Actor.adapt(req.actor ?? req.user);
                if ( ! actor ) {
                    // this shouldn't happen; auth should catch it
                    throw new Error('actor missing');
                }
                
                // track: opposite condition of sibling
                // :: sibling: /apply endpoint
                if (
                    actor.type.user.email_confirmed &&
                    actor.type.user.email === share.recipient_email
                ) {
                    throw APIError.create('no_need_to_request');
                }
                
                const issuer_user = await get_user({
                    id: share.issuer_user_id,
                });

                if ( ! issuer_user ) {
                    throw APIError.create('share_expired');
                }
                
                const svc_notification = this.services.get('notification');
                svc_notification.notify(
                    UsernameNotifSelector(issuer_user.username),
                    {
                        source: 'sharing',
                        title: `User ${actor.type.user.username} is ` +
                            `trying to open a share you sent to ` +
                            share.recipient_email,
                        template: 'user-requesting-share',
                        fields: {
                            username: actor.type.user.username,
                            intended_recipient: share.recipient_email,
                            permissions: share.data.permissions,
                        },
                    }
                );
                res.json({
                    $: 'api:status-report',
                    status: 'success',
                });
            }
        }).attach(router);
    }
    
    install_share_endpoint ({ app }) {
        // track: scoping iife
        const router = (() => {
            const require = this.require;
            const express = require('express');
            return express.Router();
        })();
        
        app.use('/share', router);
        
        const share_sequence = require('../structured/sequence/share.js');
        Endpoint({
            route: '/',
            methods: ['POST'],
            mw: [
                configurable_auth(),
                // featureflag({ feature: 'share' }),
            ],
            handler: async (req, res) => {
                const svc_edgeRateLimit = req.services.get('edge-rate-limit');
                if ( ! svc_edgeRateLimit.check('verify-pass-recovery-token') ) {
                    return res.status(429).send('Too many requests.');
                }

                const actor = Actor.adapt(req.user);
                if ( ! (actor.type instanceof UserActorType) ) {
                    throw APIError.create('forbidden');
                }

                if ( ! actor.type.user.email_confirmed ) {
                    throw APIError.create('email_must_be_confirmed', null, {
                        action: 'share something',
                    });
                }

                return await share_sequence.call(this, {
                    actor, req, res,
                });
            }
        }).attach(router);
    }
    
    async get_share ({ uid }) {
        const [share] = await this.db.read(
            'SELECT * FROM share WHERE uid = ?',
            [uid],
        );
        
        return share;
    }
    
    /**
    * Method to handle the creation of a new share
    *
    * This method creates a new share and saves it to the database.
    * It takes three parameters: the issuer of the share, the recipient's email address, and the data to be shared.
    * The method returns the UID of the created share.
    *
    * @param {Actor} issuer - The actor who is creating the share
    * @param {string} email - The email address of the recipient
    * @param {object} data - The data to be shared
    * @returns {string} - The UID of the created share
    */
    async create_share ({
        issuer,
        email,
        data,
    }) {
        const require = this.require;
        const validator = require('validator');
        
        // track: type check
        if ( typeof email !== 'string' ) {
            throw new Error('email must be a string');
        }
        // track: type check
        if ( whatis(data) !== 'object' ) {
            throw new Error('data must be an object');
        }

        // track: adapt
        issuer = Actor.adapt(issuer);
        // track: type check
        if ( ! (issuer instanceof Actor) ) {
            throw new Error('expected issuer to be Actor');
        }
        
        // track: actor type
        if ( ! (issuer.type instanceof UserActorType) ) {
            throw new Error('only users are allowed to create shares');
        }
        
        if ( ! validator.isEmail(email) ) {
            throw new Error('invalid email');
        }
        
        const uuid = this.modules.uuidv4();
        
        await this.db.write(
            'INSERT INTO `share` ' +
            '(`uid`, `issuer_user_id`, `recipient_email`, `data`) ' +
            'VALUES (?, ?, ?, ?)',
            [uuid, issuer.type.user.id, email, JSON.stringify(data)]
        );
        
        return uuid;
    }
}

module.exports = {
    ShareService,
};

