/*
 * 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 configurable_auth = require('../../middleware/configurable_auth');
const { Endpoint } = require('../../util/expressutil');
const BaseService = require('../BaseService');
const fs = require('node:fs');

const { createWorker, setCloudflareKeys, deleteWorker } = require('./workerUtils/cloudflareDeploy');
const { getUserInfo } = require('./workerUtils/puterUtils');
const { LLRead } = require('../../filesystem/ll_operations/ll_read');
const { Context } = require('../../util/context');
const { NodePathSelector, NodeUIDSelector } = require('../../filesystem/node/selectors');
const { calculateWorkerNameNew } = require('./workerUtils/nameUtils');
const { Entity } = require('../../om/entitystorage/Entity');
const { SKIP_ES_VALIDATION } = require('../../om/entitystorage/consts');
const { Eq, StartsWith } = require('../../om/query/query');
const { get_app, subdomain } = require('../../helpers');
const { UsernameNotifSelector } = require('../NotificationService');
const APIError = require('../../api/APIError');
const FSNodeParam = require('../../api/filesystem/FSNodeParam');
const { UserActorType } = require('../auth/Actor');

async function readPuterFile (actor, filePath) {
    try {
        const svc_fs = this.services.get('filesystem');
        const node = await svc_fs.node(new NodePathSelector(filePath));
        const ll_read = new LLRead();
        const stream = await ll_read.run({
            fsNode: node,
            actor,
        });
        const chunks = [];
        let bytes = 0;
        stream.on('data', (data) => {
            chunks.push(data);
            bytes += data.byteLength;
            if ( bytes > 10 ** 7 ) {
                const err = Error('Worker source code must not exceed 10MB');
                stream.emit('error', err);
                throw err;
            }
        });
        return new Promise((res, rej) => {
            stream.on('error', (e) => {
                rej(e.toString());
            });
            stream.on('end', () => {
                res(Buffer.concat(chunks));
            });
        });
    } catch (e) {
        console.error(e);
    }

}
// This file is generated by webpack. To rebuild: cd to this directory and run `npm run build`
let preamble;
try {
    preamble = fs.readFileSync(`${__dirname }/dist/workerPreamble.js`, 'utf-8');
} catch (e) {
    preamble = '';
    console.error('WORKERS ERROR: Preamble has not been built! Workers will not have access to puter.js\nTo fix this cd into src/backend/src/worker and run npm run build');
}
const PREAMBLE_LENGTH = preamble.split('\n').length - 1;
class WorkerService extends BaseService {
    _init () {
        setCloudflareKeys(this.config);

        // Services used
        const svc_event = this.services.get('event');
        const svc_su = this.services.get('su');
        const es_subdomain = this.services.get('es:subdomain');
        const svc_auth = this.services.get('auth');
        const svc_notification = this.services.get('notification');

        svc_event.on('fs.write.file', async (_key, data, meta) => {
            // Code should only run on the same server as the write
            if ( meta.from_outside ) return;

            // Check if the file that was written correlates to a worker
            const results = await svc_su.sudo(async () => {
                return await es_subdomain.select({ predicate: new Eq({ key: 'root_dir', value: data.node }) });
            });
            if ( !results || results.length === 0 )
            {
                return;
            }

            for ( const result of results ) {
                // Person who just wrote file (not necessarily file owner)
                const actor = Context.get('actor');

                // Worker data
                const fileData = (await readPuterFile(Context.get('actor'), data.node.path)).toString();
                const workerName = (await result.get('subdomain')).split('.').pop();

                // Get appropriate deploy time auth token to give to the worker
                let authToken;
                const appOwner = await result.get('app_owner');
                if ( appOwner ) { // If the deployer is an app...
                    const appID = await appOwner.get('uid');
                    authToken = await svc_su.sudo(await data.node.get('owner'), async () => {
                        return await svc_auth.get_user_app_token(appID);
                    });
                } else { // If the deployer is not attached to any application
                    authToken = (await svc_auth.create_session_token((await data.node.get('owner')).type.user)).token;
                }

                // svc_notification.notify(
                //     UsernameNotifSelector(actor.type.user.username),
                //     {
                //         source: 'worker',
                //         title: `Deploying CF worker ${workerName}`,
                //         template: 'user-requesting-share',
                //         fields: {
                //             username: actor.type.user.username,
                //         },
                //     }
                // );
                try {
                    // Create the worker
                    const cfData = await createWorker((await data.node.get('owner')).type.user, authToken, workerName, preamble + fileData, PREAMBLE_LENGTH);

                    // Send user the appropriate notification
                    if ( cfData.success ) {
                        // svc_notification.notify(
                        //     UsernameNotifSelector(actor.type.user.username),
                        //     {
                        //         source: 'worker',
                        //         title: `Succesfully deployed ${cfData.url}`,
                        //         template: 'user-requesting-share',
                        //         fields: {
                        //             username: actor.type.user.username,
                        //         },
                        //     }
                        // );
                    } else {
                        svc_notification.notify(UsernameNotifSelector(actor.type.user.username),
                                        {
                                            source: 'worker',
                                            title: `Failed to deploy ${workerName}! ${cfData.errors}`,
                                            template: 'user-requesting-share',
                                            fields: {
                                                username: actor.type.user.username,
                                            },
                                        });
                    }

                } catch (e) {
                    svc_notification.notify(UsernameNotifSelector(actor.type.user.username),
                                    {
                                        source: 'worker',
                                        title: `Failed to deploy ${workerName}!!\n ${e}`,
                                        template: 'user-requesting-share',
                                        fields: {
                                            username: actor.type.user.username,
                                        },
                                    });
                }
            }
        });
    }
    static IMPLEMENTS = {
        ['workers']: {
            /**
             *
             * @param {{filePath: string, workerName: string, authorization: string}} param0
             * @returns {any}
             */
            async create ({ filePath, workerName, authorization, appId }) {
                try {
                    workerName = workerName.toLocaleLowerCase(); // just incase
                    const svc_su = this.services.get('su');
                    const es_subdomain = this.services.get('es:subdomain');
                    const svc_auth = this.services.get('auth');

                    const currentDomains = await svc_su.sudo(Context.get('actor').get_related_actor(UserActorType), async () => {
                        return (await es_subdomain.select({ predicate: new StartsWith({ key: 'subdomain', value: 'workers.puter.' }) }));
                    });

                    if ( appId ) {
                        const app = await get_app({ uid: appId });
                        if ( Context.get('actor').type.user.id !== app.owner_user_id )
                        {
                            throw APIError.create('no_suitable_app', null, { entry_name: workerName });
                        }

                        authorization = await svc_auth.get_user_app_token(appId);
                    }

                    if ( currentDomains.length >= 100 ) {
                        throw APIError.create('subdomain_limit_reached', null, { isWorker: true, limit: 100 });
                    }

                    if ( this.global_config.reserved_words.includes(workerName) ) {
                        throw APIError.create('subdomain_reserved', null, {
                            subdomain: workerName,
                        });
                    }

                    if ( ! (/^[a-zA-Z0-9_-]+$/.test(workerName)) ) return;

                    filePath = await (await (new FSNodeParam('path')).consolidate({
                        req: { user: Context.get('actor').type.user },
                        getParam: () => filePath,
                    })).get('path');

                    const userData = await getUserInfo(authorization, this.global_config.api_base_url);
                    const actor = Context.get('actor');
                    if ( appId ) {
                        await svc_su.sudo(await svc_auth.authenticate_from_token(authorization), async () => {
                            await Context.sub({ [SKIP_ES_VALIDATION]: true }).arun(async () => {
                                const entity = await Entity.create({ om: es_subdomain.om }, {
                                    subdomain: `workers.puter.${ calculateWorkerNameNew(userData, workerName)}`,
                                    root_dir: filePath,
                                });
                                await es_subdomain.upsert(entity);
                            });
                        });
                    } else {
                        await Context.sub({ [SKIP_ES_VALIDATION]: true }).arun(async () => {
                            const entity = await Entity.create({ om: es_subdomain.om }, {
                                subdomain: `workers.puter.${ calculateWorkerNameNew(userData, workerName)}`,
                                root_dir: filePath,
                            });
                            await es_subdomain.upsert(entity);
                        });
                    }

                    const fileData = (await readPuterFile(actor, filePath)).toString();
                    const cfData = await createWorker(userData, authorization, calculateWorkerNameNew(userData.uuid, workerName), preamble + fileData, PREAMBLE_LENGTH);

                    return cfData;
                } catch (e) {
                    if ( e instanceof APIError )
                    {
                        throw e;
                    }
                    console.error(e);
                    return { success: false, errors: e };
                }
            },
            async destroy ({ workerName, authorization }) {
                try {
                    workerName = workerName.toLocaleLowerCase(); // just incase
                    const svc_su = this.services.get('su');
                    const es_subdomain = this.services.get('es:subdomain');

                    const userData = await getUserInfo(authorization, this.global_config.api_base_url);

                    const [result] = (await es_subdomain.select({ predicate: new Eq({ key: 'subdomain', value: `workers.puter.${ calculateWorkerNameNew(undefined, workerName)}` }) }));

                    if ( result.values_.owner.uuid !== userData.uuid ) {
                        throw new Error('This is not your worker!');
                    }

                    const cfData = await deleteWorker(userData, authorization, workerName);

                    await es_subdomain.delete(await result.get('uid'));
                    return cfData;

                } catch (e) {
                    if ( e instanceof APIError )
                    {
                        throw e;
                    }
                    console.error(e);
                    return { success: false, e };
                }
            },
            async getFilePaths ({ workerName }) {
                try {
                    const es_subdomain = this.services.get('es:subdomain');
                    let currentDomains;
                    if ( typeof (workerName) !== 'string' ) {
                        currentDomains = (await es_subdomain.select({ predicate: new StartsWith({ key: 'subdomain', value: 'workers.puter.' }) }));
                    } else {
                        currentDomains = (await es_subdomain.select({ predicate: new Eq({ key: 'subdomain', value: `workers.puter.${ workerName}` }) }));
                    }

                    const domainToPath = [];
                    for ( const domain of currentDomains ) {
                        const node = await domain.get('root_dir');
                        const subdomainString = (await domain.get('subdomain'));
                        let file_path = null;
                        let file_uid = null;
                        try {
                            file_path = await node.get('path');
                            file_uid = await node.get('uid');
                        } catch (e) {
                        }
                        const name = subdomainString.split('.').pop();
                        const url = `https://${name}.puter.work`;
                        domainToPath.push({ name, url, file_path, file_uid, created_at: (new Date(await domain.get('created_at'))).toISOString() });
                    }

                    return domainToPath;
                } catch (e) {
                    console.error(e);
                }

            },
            async startLogs ({ workerName, authorization }) {
                return await this.exec_({ runtime, code });
            },
            async endLogs ({ workerName, authorization }) {
                return await this.exec_({ runtime, code });
            },
            async getLoggingUrl ({ }) {
                return this.config.loggingUrl;
            },
        },
    };
    async ['__on_driver.register.interfaces'] () {
        const svc_registry = this.services.get('registry');
        const col_interfaces = svc_registry.get('interfaces');

        col_interfaces.set('workers', {
            description: 'Execute code with various languages.',
            methods: {
                getFilePaths: {
                    description: 'get paths for your workers',
                    parameters: {
                        workerName: {
                            type: 'string',
                            description: 'Optionally, the name of the worker you want the path for',
                        },
                    },
                    result: { type: 'json' },
                },
                create: {
                    description: 'Create a backend worker',
                    parameters: {
                        filePath: {
                            type: 'string',
                            description: 'The path of the code of the worker to upload',
                        },
                        workerName: {
                            type: 'string',
                            description: 'The name of the worker you want to upload',
                        },
                        authorization: {
                            type: 'string',
                            description: 'Puter token',
                        },
                        appId: {
                            type: 'string',
                            description: 'App ID to tie a worker to',
                        },
                    },
                    result: { type: 'json' },
                },
                startLogs: {
                    description: 'Get logs for your backend worker',
                    parameters: {
                        workerName: {
                            type: 'string',
                            description: 'The name of the worker you want the logs of',
                        },
                        authorization: {
                            type: 'string',
                            description: 'Puter token',
                        },
                    },
                    result: { type: 'json' },
                },
                getLoggingUrl: {
                    description: 'Get logging endpoint for your backend worker',
                    parameters: {
                    },
                    result: { type: 'string' },
                },
                destroy: {
                    description: 'Get rid of your backend worker',
                    parameters: {
                        workerName: {
                            type: 'string',
                            description: 'The name of the worker you want to destroy',
                        },
                        authorization: {
                            type: 'string',
                            description: 'Puter token',
                        },
                    },
                    result: { type: 'json' },
                },
            },
        });
    }
}

module.exports = {
    WorkerService,
};
