/*
 * Copyright 2016 balena.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

export type ErrorWithPath = Error & {
	path?: string;
	code?: keyof typeof HUMAN_FRIENDLY;
};

/**
 * @summary Human-friendly error messages
 */
export const HUMAN_FRIENDLY = {
	ENOENT: {
		title: (error: ErrorWithPath) => {
			return `No such file or directory: ${error.path}`;
		},
		description: () => "The file you're trying to access doesn't exist",
	},
	EPERM: {
		title: () => "You're not authorized to perform this operation",
		description: () =>
			'Please ensure you have necessary permissions for this task',
	},
	EACCES: {
		title: () => "You don't have access to this resource",
		description: () =>
			'Please ensure you have necessary permissions to access this resource',
	},
	ENOMEM: {
		title: () => 'Your system ran out of memory',
		description: () =>
			'Please make sure your system has enough available memory for this task',
	},
} as const;

/**
 * @summary Get user friendly property from an error
 *
 * @example
 * const error = new Error('My error');
 * error.code = 'ENOMEM';
 *
 * const friendlyDescription = getUserFriendlyMessageProperty(error, 'description');
 *
 * if (friendlyDescription) {
 *   console.log(friendlyDescription);
 * }
 */
function getUserFriendlyMessageProperty(
	error: ErrorWithPath,
	property: 'title' | 'description',
): string | undefined {
	if (typeof error.code !== 'string') {
		return undefined;
	}
	return HUMAN_FRIENDLY[error.code]?.[property]?.(error);
}

function isBlank(s: string | number | null | undefined) {
	if (typeof s === 'number') {
		s = s.toString();
	}
	return (s ?? '').trim() === '';
}

/**
 * @summary Get the title of an error
 *
 * @description
 * Try to get as much information as possible about the error
 * rather than falling back to generic messages right away.
 */
export function getTitle(error: ErrorWithPath): string {
	const codeTitle = getUserFriendlyMessageProperty(error, 'title');
	if (codeTitle !== undefined) {
		return codeTitle;
	}

	const message = error.message;
	if (!isBlank(message)) {
		return message;
	}

	const code = error.code;
	if (!isBlank(code)) {
		return `Error code: ${code}`;
	}

	return 'An error ocurred';
}

/**
 * @summary Get the description of an error
 */
export function getDescription(
	error: ErrorWithPath & { description?: string },
): string {
	if (!isBlank(error.description)) {
		return error.description as string;
	}
	const codeDescription = getUserFriendlyMessageProperty(error, 'description');
	if (codeDescription !== undefined) {
		return codeDescription;
	}
	if (error.stack) {
		return error.stack;
	}
	return JSON.stringify(error, null, 2);
}

/**
 * @summary Create an error
 */
export function createError(options: {
	title: string;
	description?: string;
	report?: boolean;
	code?: keyof typeof HUMAN_FRIENDLY;
}): ErrorWithPath & { description?: string; report?: boolean } {
	if (isBlank(options.title)) {
		throw new Error(`Invalid error title: ${options.title}`);
	}

	const error: ErrorWithPath & {
		description?: string;
		report?: boolean;
		code?: string;
	} = new Error(options.title);
	error.description = options.description;

	if (options.report === false) {
		error.report = false;
	}

	if (options.code !== undefined) {
		error.code = options.code;
	}

	return error;
}

/**
 * @summary Create a user error
 *
 * @description
 * User errors represent invalid states that the user
 * caused, that are not errors on the application itself.
 * Therefore, user errors don't get reported to analytics
 * and error reporting services.
 */
export function createUserError(options: {
	title: string;
	description: string;
	code?: keyof typeof HUMAN_FRIENDLY;
}): Error {
	return createError({
		title: options.title,
		description: options.description,
		report: false,
		code: options.code,
	});
}

/**
 * @summary Convert an Error object to a JSON object
 * @function
 * @public
 *
 * @param {Error} error - error object
 * @returns {Object} json error
 *
 * @example
 * const error = errors.toJSON(new Error('foo'))
 *
 * console.log(error.message);
 * > 'foo'
 */
export function toJSON(
	error: Error & {
		description?: string;
		report?: boolean;
		code?: string;
		syscall?: string;
		errno?: number;
		stdout?: string;
		stderr?: string;
		device?: string;
	},
): any {
	return {
		name: error.name,
		message: error.message,
		description: error.description,
		stack: error.stack,
		report: error.report,
		code: error.code,
		syscall: error.syscall,
		errno: error.errno,
		stdout: error.stdout,
		stderr: error.stderr,
		device: error.device,
	};
}

/**
 * @summary Convert a JSON object to an Error object
 */
export function fromJSON(json: any): Error {
	return Object.assign(new Error(json.message), json);
}
