const os = require('os');
const fs = require("fs");
const _path = require("path");
const process = require("process");
const JSONbig = require("json-bigint")({ storeAsString: false });

const KeysVars = new Set(["key", "value"]);
const KeysFunction = new Set(["path", "$id", "execute", "name", "enabled", "logging", "runtime", "scopes", "events", "schedule", "timeout", "entrypoint", "commands", "vars"]);
const KeysDatabase = new Set(["$id", "name", "enabled"]);
const KeysCollection = new Set(["$id", "$permissions", "databaseId", "name", "enabled", "documentSecurity", "attributes", "indexes"]);
const KeysStorage = new Set(["$id", "$permissions", "fileSecurity", "name", "enabled", "maximumFileSize", "allowedFileExtensions", "compression", "encryption", "antivirus"]);
const KeysTopics = new Set(["$id", "name", "subscribe"]);
const KeysTeams = new Set(["$id", "name"]);
const KeysAttributes = new Set([
    "key",
    "type",
    "required",
    "array",
    "size",
    "default",
    // integer and float
    "min",
    "max",
    // email, enum, URL, IP, and datetime
    "format",
    // enum
    "elements",
    // relationship
    "relatedCollection",
    "relationType",
    "twoWay",
    "twoWayKey",
    "onDelete",
    "side",
    // Indexes
    "attributes",
    "orders"
]);
const KeyIndexes = new Set(["key", "type", "status", "attributes", "orders"]);

function whitelistKeys(value, keys, nestedKeys = {}) {
    if (Array.isArray(value)) {
        const newValue = [];

        for (const item of value) {
            newValue.push(whitelistKeys(item, keys, nestedKeys));
        }

        return newValue;
    }

    const newValue = {};
    Object.keys(value).forEach((key) => {
        if (keys.has(key)) {
            if (nestedKeys[key]) {
                newValue[key] = whitelistKeys(value[key], nestedKeys[key]);
            } else {
                newValue[key] = value[key];
            }
        }
    });
    return newValue;
}

class Config {
    constructor(path) {
        this.path = path;
        this.read();
    }

    read() {
        try {
            const file = fs.readFileSync(this.path).toString();
            this.data = JSONbig.parse(file);
        } catch (e) {
            this.data = {};
        }
    }

    write() {
        let dir = _path.dirname(this.path)
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        fs.writeFileSync(this.path, JSONbig.stringify(this.data, null, 4), { mode: 0o600 });
    }

    get(key) {
        return this.data[key];
    }

    set(key, value) {
        this.data[key] = value;
        this.write();
    }

    delete(key) {
        delete this.data[key];
        this.write();
    }

    clear() {
        this.data = {};
        this.write();
    }

    has(key) {
        return this.data[key] !== undefined;
    }

    keys() {
        return Object.keys(this.data);
    }

    values() {
        return Object.values(this.data);
    }

    toString() {
        return JSONbig.stringify(this.data, null, 4);
    }
}

class Local extends Config {
    static CONFIG_FILE_PATH = "{{ spec.title|caseLower }}.json";
    configDirectoryPath = ""

    constructor(path = Local.CONFIG_FILE_PATH) {
        let currentPath = process.cwd();
        let absolutePath = `${currentPath}/${path}`;

        while (true) {
            if (fs.existsSync(`${currentPath}/${path}`)) {
                absolutePath = `${currentPath}/${path}`;
                break
            } else {
                const parentDirectory = _path.dirname(currentPath);
                if (parentDirectory === currentPath) { // we hit the top directory
                    break;
                }
                currentPath = parentDirectory
            }
        }
        super(absolutePath);
        this.configDirectoryPath =_path.dirname(absolutePath);
    }

    getDirname() {
        return _path.dirname(this.path)
    }

    getFunctions() {
        if (!this.has("functions")) {
            return [];
        }
        return this.get("functions");
    }

    getFunction($id) {
        if (!this.has("functions")) {
            return {};
        }

        let functions = this.get("functions");
        for (let i = 0; i < functions.length; i++) {
            if (functions[i]['$id'] == $id) {
                return functions[i];
            }
        }

        return {};
    }

    addFunction(props) {
        props = whitelistKeys(props, KeysFunction, {
            vars: KeysVars
        });

        if (!this.has("functions")) {
            this.set("functions", []);
        }

        let functions = this.get("functions");
        for (let i = 0; i < functions.length; i++) {
            if (functions[i]['$id'] == props['$id']) {
                functions[i] = {
                    ...functions[i],
                    ...props
                };
                this.set("functions", functions);
                return;
            }
        }

        functions.push(props);
        this.set("functions", functions);
    }

    getCollections() {
        if (!this.has("collections")) {
            return [];
        }
        return this.get("collections");
    }

    getCollection($id) {
        if (!this.has("collections")) {
            return {};
        }

        let collections = this.get("collections");
        for (let i = 0; i < collections.length; i++) {
            if (collections[i]['$id'] == $id) {
                return collections[i];
            }
        }

        return {};
    }

    addCollection(props) {
        props = whitelistKeys(props, KeysCollection, {
            attributes: KeysAttributes,
            indexes: KeyIndexes
        });

        if (!this.has("collections")) {
            this.set("collections", []);
        }

        let collections = this.get("collections");
        for (let i = 0; i < collections.length; i++) {
            if (collections[i]['$id'] == props['$id'] && collections[i]['databaseId'] == props['databaseId']) {
                collections[i] = props;
                this.set("collections", collections);
                return;
            }
        }
        collections.push(props);
        this.set("collections", collections);
    }

    getBuckets() {
        if (!this.has("buckets")) {
            return [];
        }
        return this.get("buckets");
    }

    getBucket($id) {
        if (!this.has("buckets")) {
            return {};
        }

        let buckets = this.get("buckets");
        for (let i = 0; i < buckets.length; i++) {
            if (buckets[i]['$id'] == $id) {
                return buckets[i];
            }
        }

        return {};
    }

    addBucket(props) {
        props = whitelistKeys(props, KeysStorage);

        if (!this.has("buckets")) {
            this.set("buckets", []);
        }

        let buckets = this.get("buckets");
        for (let i = 0; i < buckets.length; i++) {
            if (buckets[i]['$id'] == props['$id']) {
                buckets[i] = props;
                this.set("buckets", buckets);
                return;
            }
        }
        buckets.push(props);
        this.set("buckets", buckets);
    }

    getMessagingTopics() {
        if (!this.has("topics")) {
            return [];
        }
        return this.get("topics");
    }

    getMessagingTopic($id) {
        if (!this.has("topics")) {
            return {};
        }

        let topic = this.get("topics");
        for (let i = 0; i < topic.length; i++) {
            if (topic[i]['$id'] == $id) {
                return topic[i];
            }
        }

        return {};
    }

    addMessagingTopic(props) {
        props = whitelistKeys(props, KeysTopics);

        if (!this.has("topics")) {
            this.set("topics", []);
        }

        let topics = this.get("topics");
        for (let i = 0; i < topics.length; i++) {
            if (topics[i]['$id'] === props['$id']) {
                topics[i] = props;
                this.set("topics", topics);
                return;
            }
        }
        topics.push(props);
        this.set("topics", topics);
    }

    getDatabases() {
        if (!this.has("databases")) {
            return [];
        }
        return this.get("databases");
    }

    getDatabase($id) {
        if (!this.has("databases")) {
            return {};
        }

        let databases = this.get("databases");
        for (let i = 0; i < databases.length; i++) {
            if (databases[i]['$id'] == $id) {
                return databases[i];
            }
        }

        return {};
    }

    addDatabase(props) {
        props = whitelistKeys(props, KeysDatabase);

        if (!this.has("databases")) {
            this.set("databases", []);
        }

        let databases = this.get("databases");
        for (let i = 0; i < databases.length; i++) {
            if (databases[i]['$id'] == props['$id']) {
                databases[i] = props;
                this.set("databases", databases);
                return;
            }
        }
        databases.push(props);
        this.set("databases", databases);
    }

    getTeams() {
        if (!this.has("teams")) {
            return [];
        }
        return this.get("teams");
    }

    getTeam($id) {
        if (!this.has("teams")) {
            return {};
        }

        let teams = this.get("teams");
        for (let i = 0; i < teams.length; i++) {
            if (teams[i]['$id'] == $id) {
                return teams[i];
            }
        }

        return {};
    }

    addTeam(props) {
        props = whitelistKeys(props, KeysTeams);
        if (!this.has("teams")) {
            this.set("teams", []);
        }

        let teams = this.get("teams");
        for (let i = 0; i < teams.length; i++) {
            if (teams[i]['$id'] == props['$id']) {
                teams[i] = props;
                this.set("teams", teams);
                return;
            }
        }
        teams.push(props);
        this.set("teams", teams);
    }

    getProject() {
        if (!this.has("projectId")) {
            return {};
        }

        return {
            projectId: this.get("projectId"),
            projectName: this.get("projectName"),
            projectSettings: this.get('settings')
        };
    }

    setProject(projectId, projectName = '', projectSettings = undefined) {
        this.set("projectId", projectId);

        if (projectName !== '') {
            this.set("projectName", projectName);
        }

        if (projectSettings === undefined) {
            return;
        }

        this.set('settings', this.createSettingsObject(projectSettings));
    }

    createSettingsObject(projectSettings) {
        return {
            services: {
                account: projectSettings.serviceStatusForAccount,
                avatars: projectSettings.serviceStatusForAvatars,
                databases: projectSettings.serviceStatusForDatabases,
                locale: projectSettings.serviceStatusForLocale,
                health: projectSettings.serviceStatusForHealth,
                storage: projectSettings.serviceStatusForStorage,
                teams: projectSettings.serviceStatusForTeams,
                users: projectSettings.serviceStatusForUsers,
                functions: projectSettings.serviceStatusForFunctions,
                graphql: projectSettings.serviceStatusForGraphql,
                messaging: projectSettings.serviceStatusForMessaging,
            },
            auth: {
                methods: {
                    jwt: projectSettings.authJWT,
                    phone: projectSettings.authPhone,
                    invites: projectSettings.authInvites,
                    anonymous: projectSettings.authAnonymous,
                    "email-otp": projectSettings.authEmailOtp,
                    "magic-url": projectSettings.authUsersAuthMagicURL,
                    "email-password": projectSettings.authEmailPassword
                },
                security: {
                    duration: projectSettings.authDuration,
                    limit: projectSettings.authLimit,
                    sessionsLimit: projectSettings.authSessionsLimit,
                    passwordHistory: projectSettings.authPasswordHistory,
                    passwordDictionary: projectSettings.authPasswordDictionary,
                    personalDataCheck: projectSettings.authPersonalDataCheck,
                    sessionAlerts: projectSettings.authSessionAlerts,
                    mockNumbers: projectSettings.authMockNumbers
                },
            }
        };
    }
}

class Global extends Config {
    static CONFIG_FILE_PATH = ".{{ spec.title|caseLower }}/prefs.json";

    static PREFERENCE_CURRENT = "current";
    static PREFERENCE_ENDPOINT = "endpoint";
    static PREFERENCE_EMAIL = "email";
    static PREFERENCE_SELF_SIGNED = "selfSigned";
    static PREFERENCE_COOKIE = "cookie";
    static PREFERENCE_PROJECT = "project";
    static PREFERENCE_KEY = "key";
    static PREFERENCE_LOCALE = "locale";
    static PREFERENCE_MODE = "mode";

    static IGNORE_ATTRIBUTES = [Global.PREFERENCE_CURRENT, Global.PREFERENCE_SELF_SIGNED, Global.PREFERENCE_ENDPOINT, Global.PREFERENCE_COOKIE, Global.PREFERENCE_PROJECT, Global.PREFERENCE_KEY, Global.PREFERENCE_LOCALE, Global.PREFERENCE_MODE];

    static MODE_ADMIN = "admin";
    static MODE_DEFAULT = "default";

    static PROJECT_CONSOLE = "console";

    constructor(path = Global.CONFIG_FILE_PATH) {
        const homeDir = os.homedir();
        super(`${homeDir}/${path}`);
    }

    getCurrentSession() {
        if (!this.has(Global.PREFERENCE_CURRENT)) {
            return "";
        }
        return this.get(Global.PREFERENCE_CURRENT);
    }

    setCurrentSession(session) {
        if (session !== undefined) {
            this.set(Global.PREFERENCE_CURRENT, session);
        }
    }

    getSessionIds() {
        return Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key));
    }

    getSessions() {
        const sessions = Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key))

        return sessions.map((session) => {

            return {
                id: session,
                endpoint: this.data[session][Global.PREFERENCE_ENDPOINT],
                email: this.data[session][Global.PREFERENCE_EMAIL]
            }
        })
    }

    addSession(session, data) {
        this.set(session, data);
    }

    removeSession(session) {
        this.delete(session);
    }

    getEmail() {
        if (!this.hasFrom(Global.PREFERENCE_EMAIL)) {
            return "";
        }

        return this.getFrom(Global.PREFERENCE_EMAIL);
    }

    setEmail(email) {
        this.setTo(Global.PREFERENCE_EMAIL, email);
    }

    getEndpoint() {
        if (!this.hasFrom(Global.PREFERENCE_ENDPOINT)) {
            return "";
        }

        return this.getFrom(Global.PREFERENCE_ENDPOINT);
    }

    setEndpoint(endpoint) {
        this.setTo(Global.PREFERENCE_ENDPOINT, endpoint);
    }

    getSelfSigned() {
        if (!this.hasFrom(Global.PREFERENCE_SELF_SIGNED)) {
            return false;
        }
        return this.getFrom(Global.PREFERENCE_SELF_SIGNED);
    }

    setSelfSigned(selfSigned) {
        this.setTo(Global.PREFERENCE_SELF_SIGNED, selfSigned);
    }

    getCookie() {
        if (!this.hasFrom(Global.PREFERENCE_COOKIE)) {
            return "";
        }
        return this.getFrom(Global.PREFERENCE_COOKIE);
    }

    setCookie(cookie) {
        this.setTo(Global.PREFERENCE_COOKIE, cookie);
    }

    getProject() {
        if (!this.hasFrom(Global.PREFERENCE_PROJECT)) {
            return "";
        }
        return this.getFrom(Global.PREFERENCE_PROJECT);
    }

    setProject(project) {
        this.setTo(Global.PREFERENCE_PROJECT, project);
    }

    getKey() {
        if (!this.hasFrom(Global.PREFERENCE_KEY)) {
            return "";
        }
        return this.getFrom(Global.PREFERENCE_KEY);
    }

    setKey(key) {
        this.setTo(Global.PREFERENCE_KEY, key);
    }

    hasFrom(key) {
        const current = this.getCurrentSession();

        if (current) {
            const config = this.get(current) ?? {};

            return config[key] !== undefined;
        }
    }

    getFrom(key) {
        const current = this.getCurrentSession();

        if (current) {
            const config = this.get(current) ?? {};

            return config[key];
        }
    }

    setTo(key, value) {
        const current = this.getCurrentSession();

        if (current) {
            const config = this.get(current);

            config[key] = value;
            this.write();
        }
    }
}

module.exports = {
    localConfig: new Local(),
    globalConfig: new Global(),
    KeysAttributes,
    KeysFunction,
    KeysTopics,
    KeysStorage,
    KeysTeams,
    KeysCollection,
    whitelistKeys
};
