/*
 * Copyright (C) 2024 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 { es_import_promise } = require("../../fun/dev-console-ui-utils");
const { surrounding_box } = require("../../fun/dev-console-ui-utils");
const { BaseDatabaseAccessService } = require("./BaseDatabaseAccessService");

class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
    static ENGINE_NAME = 'sqlite';

    static MODULES = {
        // Documentation calls it 'Database'; it's new-able so
        // I'll stick with their convention over ours.
        Database: require('better-sqlite3'),
    };

    async _init () {
        const require = this.require;
        const Database = require('better-sqlite3');

        this._register_commands(this.services.get('commands'));

        const fs = require('fs');
        const path_ = require('path');
        const do_setup = ! fs.existsSync(this.config.path);

        this.db = new Database(this.config.path);

        // Database upgrade logic
        const TARGET_VERSION = 1;

        if ( do_setup ) {
            this.log.noticeme(`SETUP: creating database at ${this.config.path}`);
            const sql_files = [
                '0001_create-tables.sql',
                '0002_add-default-apps.sql',
                '0003_user-permissions.sql',
            ].map(p => path_.join(__dirname, 'sqlite_setup', p));
            const fs = require('fs');
            for ( const filename of sql_files ) {
                const basename = path_.basename(filename);
                this.log.noticeme(`applying ${basename}`);
                const contents = fs.readFileSync(filename, 'utf8');
                this.db.exec(contents);
            }
            await this.db.exec(`PRAGMA user_version = ${TARGET_VERSION};`);
        }

        const [{ user_version }] = await this._read('PRAGMA user_version');
        this.log.info('database version: ' + user_version);

        const upgrade_files = [];

        if ( user_version <= 0 ) {
            upgrade_files.push('0003_user-permissions.sql');
        }

        if ( upgrade_files.length > 0 ) {
            this.log.noticeme(`Database out of date: ${this.config.path}`);
            this.log.noticeme(`UPGRADING DATABASE: ${user_version} -> ${TARGET_VERSION}`);
            this.log.noticeme(`${upgrade_files.length} .sql files to apply`);

            const sql_files = upgrade_files.map(
                p => path_.join(__dirname, 'sqlite_setup', p)
            );
            const fs = require('fs');
            for ( const filename of sql_files ) {
                const basename = path_.basename(filename);
                this.log.noticeme(`applying ${basename}`);
                const contents = fs.readFileSync(filename, 'utf8');
                this.db.exec(contents);
            }

            // Update version number
            await this.db.exec(`PRAGMA user_version = ${TARGET_VERSION};`);

            // Add sticky notification
            this.database_update_notice = () => {
                const lines = [
                    `Database has been updated!`,
                    `Current version: ${TARGET_VERSION}`,
                    `Type sqlite:dismiss to dismiss this message`,
                ];
                surrounding_box('33;1', lines);
                return lines;
            };

            (async () => {
                await es_import_promise;
                const svc_devConsole = this.services.get('dev-console');
                svc_devConsole.add_widget(this.database_update_notice);
            })();
        }
    }

    async _read (query, params = []) {
        query = this.sqlite_transform_query_(query);
        params = this.sqlite_transform_params_(params);
        return this.db.prepare(query).all(...params);
    }

    async _requireRead (query, params) {
        return this._read(query, params);
    }

    async _write (query, params) {
        query = this.sqlite_transform_query_(query);
        params = this.sqlite_transform_params_(params);

        try {
            const stmt = this.db.prepare(query);
            const info = stmt.run(...params);

            return {
                insertId: info.lastInsertRowid,
                anyRowsAffected: info.changes > 0,
            };
        } catch ( e ) {
            console.error(e);
            console.log('everything', {
                query, params,
            })
            console.log(params.map(p => typeof p));
            // throw e;
        }
    }

    async _batch_write (entries) {
        this.db.transaction(() => {
            for ( let { statement, values } of entries ) {
                statement = this.sqlite_transform_query_(statement);
                values = this.sqlite_transform_params_(values);
                this.db.prepare(statement).run(values);
            }
        })();
    }


    sqlite_transform_query_ (query) {
        // replace `now()` with `datetime('now')`
        query = query.replace(/now\(\)/g, 'datetime(\'now\')');

        return query;
    }

    sqlite_transform_params_ (params) {
        return params.map(p => {
            if ( typeof p === 'boolean' ) {
                return p ? 1 : 0;
            }
            return p;
        });
    }

    _register_commands (commands) {
        commands.registerCommands('sqlite', [
            {
                id: 'execfile',
                description: 'execute a file',
                handler: async (args, log) => {
                    try {
                        const [filename] = args;
                        const fs = require('fs');
                        const contents = fs.readFileSync(filename, 'utf8');
                        this.db.exec(contents);
                    } catch (err) {
                        log.error(err.message);
                    }
                }
            },
            {
                id: 'read',
                description: 'read a query',
                handler: async (args, log) => {
                    try {
                        const [query] = args;
                        const rows = this._read(query, []);
                        log.log(rows);
                    } catch (err) {
                        log.error(err.message);
                    }
                }
            },
            {
                id: 'dismiss',
                description: 'dismiss the database update notice',
                handler: async (_, log) => {
                    const svc_devConsole = this.services.get('dev-console');
                    if ( ! svc_devConsole ) return;
                    if ( ! this.database_update_notice ) return;
                    svc_devConsole.remove_widget(this.database_update_notice);
                    const lines = this.database_update_notice();
                    for ( const line of lines ) log.log(line);
                    this.database_update_notice = null;
                }
            }
        ])
    }
}

module.exports = {
    SqliteDatabaseAccessService,
};
