"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DatabaseEmulator = void 0;
const chokidar = require("chokidar");
const clc = require("colorette");
const fs = require("fs");
const path = require("path");
const http = require("http");
const downloadableEmulators = require("./downloadableEmulators");
const types_1 = require("../emulator/types");
const constants_1 = require("./constants");
const registry_1 = require("./registry");
const emulatorLogger_1 = require("./emulatorLogger");
const error_1 = require("../error");
const parseBoltRules_1 = require("../parseBoltRules");
const utils_1 = require("../utils");
class DatabaseEmulator {
    constructor(args) {
        this.args = args;
        this.importedNamespaces = [];
        this.logger = emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.DATABASE);
    }
    async start() {
        const functionsInfo = registry_1.EmulatorRegistry.getInfo(types_1.Emulators.FUNCTIONS);
        if (functionsInfo) {
            this.args.functions_emulator_host = functionsInfo.host;
            this.args.functions_emulator_port = functionsInfo.port;
        }
        if (this.args.rules) {
            for (const c of this.args.rules) {
                if (!c.instance) {
                    this.logger.log("DEBUG", `args.rules=${JSON.stringify(this.args.rules)}`);
                    this.logger.logLabeled("WARN_ONCE", "database", "Could not determine your Realtime Database instance name, so rules hot reloading is disabled.");
                    continue;
                }
                this.rulesWatcher = chokidar.watch(c.rules, { persistent: true, ignoreInitial: true });
                this.rulesWatcher.on("change", async () => {
                    await new Promise((res) => setTimeout(res, 5));
                    this.logger.logLabeled("BULLET", "database", `Change detected, updating rules for ${c.instance}...`);
                    try {
                        await this.updateRules(c.instance, c.rules);
                        this.logger.logLabeled("SUCCESS", "database", "Rules updated.");
                    }
                    catch (e) {
                        this.logger.logLabeled("WARN", "database", this.prettyPrintRulesError(c.rules, e));
                        this.logger.logLabeled("WARN", "database", "Failed to update rules");
                    }
                });
            }
        }
        return downloadableEmulators.start(types_1.Emulators.DATABASE, this.args);
    }
    async connect() {
        if (this.args.rules) {
            for (const c of this.args.rules) {
                if (!c.instance) {
                    continue;
                }
                try {
                    await this.updateRules(c.instance, c.rules);
                }
                catch (e) {
                    const rulesError = this.prettyPrintRulesError(c.rules, e);
                    this.logger.logLabeled("WARN", "database", rulesError);
                    this.logger.logLabeled("WARN", "database", "Failed to update rules");
                    throw new error_1.FirebaseError(`Failed to load initial ${constants_1.Constants.description(this.getName())} rules:\n${rulesError}`);
                }
            }
        }
    }
    stop() {
        return downloadableEmulators.stop(types_1.Emulators.DATABASE);
    }
    getInfo() {
        const host = this.args.host || constants_1.Constants.getDefaultHost();
        const port = this.args.port || constants_1.Constants.getDefaultPort(types_1.Emulators.DATABASE);
        return {
            name: this.getName(),
            host,
            port,
            pid: downloadableEmulators.getPID(types_1.Emulators.DATABASE),
        };
    }
    getName() {
        return types_1.Emulators.DATABASE;
    }
    getImportedNamespaces() {
        return this.importedNamespaces;
    }
    async importData(ns, fPath) {
        this.logger.logLabeled("BULLET", "database", `Importing data from ${fPath}`);
        const readStream = fs.createReadStream(fPath);
        const { host, port } = this.getInfo();
        await new Promise((resolve, reject) => {
            const req = http.request({
                method: "PUT",
                host: (0, utils_1.connectableHostname)(host),
                port,
                path: `/.json?ns=${ns}&disableTriggers=true&writeSizeLimit=unlimited`,
                headers: {
                    Authorization: "Bearer owner",
                    "Content-Type": "application/json",
                },
            }, (response) => {
                if (response.statusCode === 200) {
                    this.importedNamespaces.push(ns);
                    resolve();
                }
                else {
                    this.logger.log("DEBUG", "Database import failed: " + response.statusCode);
                    response
                        .on("data", (d) => {
                        this.logger.log("DEBUG", d.toString());
                    })
                        .on("end", reject);
                }
            });
            req.on("error", reject);
            readStream.pipe(req, { end: true });
        }).catch((e) => {
            throw new error_1.FirebaseError("Error during database import.", { original: e, exit: 1 });
        });
    }
    async updateRules(instance, rulesPath) {
        var _a;
        const rulesExt = path.extname(rulesPath);
        const content = rulesExt === ".bolt"
            ? (0, parseBoltRules_1.parseBoltRules)(rulesPath).toString()
            : fs.readFileSync(rulesPath, "utf8");
        try {
            await registry_1.EmulatorRegistry.client(types_1.Emulators.DATABASE).put(`/.settings/rules.json`, content, {
                headers: { Authorization: "Bearer owner" },
                queryParams: { ns: instance },
            });
        }
        catch (e) {
            if (e.context && e.context.body) {
                throw e.context.body.error;
            }
            throw (_a = e.original) !== null && _a !== void 0 ? _a : e;
        }
    }
    prettyPrintRulesError(filePath, error) {
        let errStr;
        switch (typeof error) {
            case "string":
                errStr = error;
                break;
            case "object":
                if (error != null && "message" in error) {
                    const message = error.message;
                    errStr = `${message}`;
                    if (typeof message === "string") {
                        try {
                            const parsed = JSON.parse(message);
                            if (typeof parsed === "object" && parsed.error) {
                                errStr = `${parsed.error}`;
                            }
                        }
                        catch (_) {
                        }
                    }
                    break;
                }
            default:
                errStr = `Unknown error: ${JSON.stringify(error)}`;
        }
        const relativePath = path.relative(process.cwd(), filePath);
        return `${clc.cyan(relativePath)}:${errStr.trim()}`;
    }
}
exports.DatabaseEmulator = DatabaseEmulator;
