/* eslint-disable unicorn/no-this-assignment, func-names, no-multi-assign  */
const path = require('node:path');
const process = require('node:process');
const crypto = require('node:crypto');

module.exports = {
	createFromFile(filePath, useChecksum) {
		const fname = path.basename(filePath);
		const dir = path.dirname(filePath);
		return this.create(fname, dir, useChecksum);
	},

	create(cacheId, _path, useChecksum) {
		const fs = require('node:fs');
		const flatCache = require('flat-cache');
		const cache = flatCache.load(cacheId, _path);
		let normalizedEntries = {};

		const removeNotFoundFiles = function removeNotFoundFiles() {
			const cachedEntries = cache.keys();
			// Remove not found entries
			for (const fPath of cachedEntries) {
				try {
					fs.statSync(fPath);
				} catch (error) {
					if (error.code === 'ENOENT') {
						cache.removeKey(fPath);
					}
				}
			}
		};

		removeNotFoundFiles();

		return {
			/**
       * The flat cache storage used to persist the metadata of the `files
       * @type {Object}
       */
			cache,

			/**
       * Given a buffer, calculate md5 hash of its content.
       * @method getHash
       * @param  {Buffer} buffer   buffer to calculate hash on
       * @return {String}          content hash digest
       */
			getHash(buffer) {
				return crypto.createHash('md5').update(buffer).digest('hex');
			},

			/**
       * Return whether or not a file has changed since last time reconcile was called.
       * @method hasFileChanged
       * @param  {String}  file  the filepath to check
       * @return {Boolean}       wheter or not the file has changed
       */
			hasFileChanged(file) {
				return this.getFileDescriptor(file).changed;
			},

			/**
       * Given an array of file paths it return and object with three arrays:
       *  - changedFiles: Files that changed since previous run
       *  - notChangedFiles: Files that haven't change
       *  - notFoundFiles: Files that were not found, probably deleted
       *
       * @param  {Array} files the files to analyze and compare to the previous seen files
       * @return {[type]}       [description]
       */
			analyzeFiles(files) {
				const me = this;
				files ||= [];

				const res = {
					changedFiles: [],
					notFoundFiles: [],
					notChangedFiles: [],
				};

				for (const entry of me.normalizeEntries(files)) {
					if (entry.changed) {
						res.changedFiles.push(entry.key);
						continue;
					}

					if (entry.notFound) {
						res.notFoundFiles.push(entry.key);
						continue;
					}

					res.notChangedFiles.push(entry.key);
				}

				return res;
			},

			getFileDescriptor(file) {
				let fstat;

				try {
					if (!path.isAbsolute(file)) {
						file = path.resolve(process.cwd(), file);
					}

					fstat = fs.statSync(file);
				} catch (error) {
					this.removeEntry(file);
					return {key: file, notFound: true, err: error};
				}

				if (useChecksum) {
					return this._getFileDescriptorUsingChecksum(file);
				}

				return this._getFileDescriptorUsingMtimeAndSize(file, fstat);
			},

			_getFileDescriptorUsingMtimeAndSize(file, fstat) {
				let meta = cache.getKey(file);
				const cacheExists = Boolean(meta);

				const cSize = fstat.size;
				const cTime = fstat.mtime.getTime();

				let isDifferentDate;
				let isDifferentSize;

				if (meta) {
					isDifferentDate = cTime !== meta.mtime;
					isDifferentSize = cSize !== meta.size;
				} else {
					meta = {size: cSize, mtime: cTime};
				}

				const nEntry = (normalizedEntries[file] = {
					key: file,
					changed: !cacheExists || isDifferentDate || isDifferentSize,
					meta,
				});

				return nEntry;
			},

			_getFileDescriptorUsingChecksum(file) {
				let meta = cache.getKey(file);
				const cacheExists = Boolean(meta);

				let contentBuffer;
				try {
					contentBuffer = fs.readFileSync(file);
				} catch {
					contentBuffer = '';
				}

				let isDifferent = true;
				const hash = this.getHash(contentBuffer);

				if (meta) {
					isDifferent = hash !== meta.hash;
				} else {
					meta = {hash};
				}

				const nEntry = (normalizedEntries[file] = {
					key: file,
					changed: !cacheExists || isDifferent,
					meta,
				});

				return nEntry;
			},

			/**
       * Return the list o the files that changed compared
       * against the ones stored in the cache
       *
       * @method getUpdated
       * @param files {Array} the array of files to compare against the ones in the cache
       * @returns {Array}
       */
			getUpdatedFiles(files) {
				const me = this;
				files ||= [];

				return me
					.normalizeEntries(files)
					.filter(entry => entry.changed)
					.map(entry => entry.key);
			},

			/**
       * Return the list of files
       * @method normalizeEntries
       * @param files
       * @returns {*}
       */
			normalizeEntries(files) {
				files ||= [];

				const me = this;
				const nEntries = files.map(file => me.getFileDescriptor(file));

				// NormalizeEntries = nEntries;
				return nEntries;
			},

			/**
       * Remove an entry from the file-entry-cache. Useful to force the file to still be considered
       * modified the next time the process is run
       *
       * @method removeEntry
       * @param entryName
       */
			removeEntry(entryName) {
				if (!path.isAbsolute(entryName)) {
					entryName = path.resolve(process.cwd(), entryName);
				}

				delete normalizedEntries[entryName];
				cache.removeKey(entryName);
			},

			/**
       * Delete the cache file from the disk
       * @method deleteCacheFile
       */
			deleteCacheFile() {
				cache.removeCacheFile();
			},

			/**
       * Remove the cache from the file and clear the memory cache
       */
			destroy() {
				normalizedEntries = {};
				cache.destroy();
			},

			_getMetaForFileUsingCheckSum(cacheEntry) {
				const contentBuffer = fs.readFileSync(cacheEntry.key);
				const hash = this.getHash(contentBuffer);
				const meta = Object.assign(cacheEntry.meta, {hash});
				delete meta.size;
				delete meta.mtime;
				return meta;
			},

			_getMetaForFileUsingMtimeAndSize(cacheEntry) {
				const stat = fs.statSync(cacheEntry.key);
				const meta = Object.assign(cacheEntry.meta, {
					size: stat.size,
					mtime: stat.mtime.getTime(),
				});
				delete meta.hash;
				return meta;
			},

			/**
       * Sync the files and persist them to the cache
       * @method reconcile
       */
			reconcile(noPrune) {
				removeNotFoundFiles();

				noPrune = noPrune === undefined ? true : noPrune;

				const entries = normalizedEntries;
				const keys = Object.keys(entries);

				if (keys.length === 0) {
					return;
				}

				const me = this;

				for (const entryName of keys) {
					const cacheEntry = entries[entryName];

					try {
						const meta = useChecksum
							? me._getMetaForFileUsingCheckSum(cacheEntry)
							: me._getMetaForFileUsingMtimeAndSize(cacheEntry);
						cache.setKey(entryName, meta);
					} catch (error) {
						// If the file does not exists we don't save it
						// other errors are just thrown
						if (error.code !== 'ENOENT') {
							throw error;
						}
					}
				}

				cache.save(noPrune);
			},
		};
	},
};
