import fs from 'node:fs';
import _ from 'lodash';
import { randomBytes } from 'crypto';
import { Logger } from 'tslog';

export type configObject = Record<any, any>;

/**
 * This class is responsible to save/edit config files.
 *
 * @export
 * @class config
 * @typedef {config}
 */
export default class config {
	#configPath: string;
	//global = {[key: string] : string}
	global: configObject;
	replaceSecrets: boolean;

	#logger: Logger<unknown> | typeof console;

	/**
	 * Creates an instance of config.
	 *
	 * @constructor
	 * @param {string} configPath Path to config file.
	 * @param {boolean} replaceSecrets Whether to replace secrets with generated values.
	 * @param {object} configPreset Default config object with default values.
	 * @param {Logger<unknown> | typeof console} [logger] Optional (tslog) logger.
	 */
	constructor(configPath: string, replaceSecrets: boolean, configPreset: object, logger?: Logger<unknown> | typeof console) {
		this.#configPath = configPath;
		this.global = configPreset;
		this.replaceSecrets = replaceSecrets;

		this.#logger = logger ?? console;

		this.#logger.info(`Initializing config manager with path: ${this.#configPath}`);
		try {
			// Read config
			const data = fs.readFileSync(this.#configPath, 'utf8');

			// Extend config with missing parameters from configPreset.
			this.global = _.defaultsDeep(JSON.parse(data), this.global);
			// Save config.
			this.save_config();
		} catch (err: any) {
			// If file does not exist, create it.
			if (err.code === 'ENOENT') {
				this.#logger.info(`Config file does not exist. Creating it at ${this.#configPath} now.`);
				this.save_config();
				return;
			}
			this.#logger.error(`Could not read config file at ${this.#configPath} due to: ${err}`);
			// Exit process.
			process.exit(1);
		}
	}

	/**
	 * Saves the jsonified config object to the config file.
	 */
	save_config() {
		try {
			// If enabled replace tokens defines as "gen" with random token
			if (this.replaceSecrets) {
				// Replace tokens with value "gen"
				this.generate_secrets(this.global, 'gen');
			}

			fs.writeFileSync(this.#configPath, JSON.stringify(this.global, null, 8));
		} catch (err) {
			this.#logger.error(`Could not write config file at ${this.#configPath} due to: ${err}`);
			return;
		}
		this.#logger.info(`Successfully written config file to ${this.#configPath}`);
	}

	/**
	 * Replaces each item matching the value of placeholder with a random UUID.
	 * Thanks to https://stackoverflow.com/questions/8085004/iterate-through-nested-javascript-objects
	 * @param {configObject} obj
	 */
	generate_secrets(obj: configObject, placeholder: string) {
		const stack = [obj];
		while (stack?.length > 0) {
			const currentObj: any = stack.pop();
			Object.keys(currentObj).forEach((key) => {
				if (currentObj[key] === placeholder) {
					this.#logger.info('Generating secret: ' + key);
					currentObj[key] = randomBytes(48).toString('base64').replace(/\W/g, '');
				}

				if (typeof currentObj[key] === 'object' && currentObj[key] !== null) {
					stack.push(currentObj[key]);
				}
			});
		}
	}
}

/* 

**** Example ****

import ConfigHandlerNG from './assets/configHandlerNG.js';

// Create a new config instance.
export const config = new ConfigHandler(__path + '/config.json', true, {
	test1: 't1',
	test2: 't2',
	test3: 'gen',
	test4: 't4',
	test5: 'gen',
	testObj: {
		local: {
			active: true,
			users: {
				user1: 'gen',
				user2: 'gen',
				user3: 'gen',
				user4: 'gen',

			}
		},
		oidc: {
			active: false
		}
	}
});

console.log('Base Config:');
console.log(config.global);

console.log('Add some new key to config and call save_config().');
config.global.NewKey = 'ThisIsANewKey!'
config.save_config()

console.log('This will add a new key with value gen, but gen gets replaced with a random UUID when save_config() is called.');
config.global.someSecret = 'gen'
config.save_config() // global.someSecret is getting replaced with some random UUID since it was set to 'gen'.

console.log('Complete Config:');
console.log(config.global);
*/
