import { BuiltInRules, builtInRules } from "@business-logic/rules/RuleEngineBuiltins";
import LoggingUtils from "@impl/utils/LoggingUtils";
import { ConsoleColor, ResultTypes } from "@business-logic/types/Enums";

//File Containing Rule Engine Implementation

export type Rule<T> = {
	id: number;
	name: string;
	check: (obj: T) => boolean;
	dependsOn?: number[];
};

export class ValidationOptions {
	public skipBuiltIns: boolean;
	public exclude: number[];
	public throwOnError: boolean = true;
	public errorsOnly: boolean = false;

	constructor(options: {
		throwOnError?: boolean;
		errorsOnly?: boolean;
		skipBuiltIns?: boolean;
		exclude?: number[];
	} = {}) {
		this.throwOnError = options.throwOnError ?? true, this.errorsOnly = options.errorsOnly ?? true, this.skipBuiltIns = options.skipBuiltIns ?? false;
		this.exclude = options.exclude ?? [];
	}
}

export class RuleEngine<T> {
	private rules: Rule<T>[] = [];
	private nextCustomRuleId = 1;

	static readonly BuiltInRuleIds: Record<BuiltInRules, number> = {} as Record<BuiltInRules, number>;

	constructor() {
		this.addBuiltInRules(builtInRules as { id: BuiltInRules; name: string; check: (obj: T) => boolean; dependsOn?: BuiltInRules[]; }[]);
	}

	private addBuiltInRules(builtInRules: { id: BuiltInRules; name: string; check: (obj: T) => boolean; dependsOn?: BuiltInRules[]; }[]): void {
		builtInRules.forEach(rule => this.addRuleInternal(rule.name, rule.check, rule.id, rule.dependsOn));
	}

	private addRuleInternal(name: string, check: (obj: T) => boolean, builtInId: BuiltInRules, dependsOn?: BuiltInRules[]): number {
		const id = builtInId;
		const dependsOnIds = dependsOn?.map(dep => dep as number);
		this.rules.push({ id, name, check, dependsOn: dependsOnIds });
		RuleEngine.BuiltInRuleIds[builtInId] = id;
		return id;
	}

	addRule(name: string, check: (obj: T) => boolean, options?: { dependsOn?: (number | BuiltInRules)[]; }): Rule<T> {
		if (this.nextCustomRuleId >= 1000)
			throw new Error("Maximum number of custom rules (999) has been reached.");

		const id = this.nextCustomRuleId++;
		const dependsOn = options?.dependsOn?.map(dep => typeof dep === "number" ? dep : RuleEngine.BuiltInRuleIds[dep]);
		const rule: Rule<T> = { id, name, check, dependsOn };
		this.rules.push(rule);
		return rule;
	}

	when(condition: (obj: T) => boolean): WhenClause<T> {
		return new WhenClause(this, condition);
	}

	checkRuleById(id: number | BuiltInRules, obj: T): boolean {
		const ruleId = typeof id === "number" ? id : RuleEngine.BuiltInRuleIds[id];
		const rule = this.rules.find(r => r.id === ruleId);
		return rule ? rule.check(obj) : true;
	}

	validate(obj: T): ValidationResult[] {
		const sortedRules = this.sortRules();
		const results = new Map<number, boolean>();
		const resultSummary: ValidationResult[] = [];

		sortedRules.forEach(rule => {
			const dependenciesValid = rule.dependsOn ? rule.dependsOn.every(dep => results.get(dep)) : true;
			const result = dependenciesValid && rule.check(obj);
			results.set(rule.id, result);

			const resultType: ResultTypes = dependenciesValid ? result ? ResultTypes.Success : ResultTypes.Failure : ResultTypes.Skipped;

			resultSummary.push(ValidationResult.FromRule(rule.name, rule.id, resultType, `Rule: "${rule.name}". Result: ${ResultTypes[resultType]}`));
		});
		return resultSummary;
	}

	private sortRules(): Rule<T>[] {
		const sortedRules: Rule<T>[] = [];
		const rulesMap = new Map(this.rules.map(rule => [rule.id, rule]));

		const visit = (rule: Rule<T>, visited: Set<number>, stack: Set<number>) => {
			if (stack.has(rule.id))
				throw new Error(`Circular dependency detected in rule: ${rule.name}!`);

			if (!visited.has(rule.id)) {
				stack.add(rule.id);
				if (rule.dependsOn) {
					for (const dependency of rule.dependsOn) {
						const dependencyRule = rulesMap.get(dependency);
						if (dependencyRule)
							visit(dependencyRule, visited, stack);
					}
				}
				stack.delete(rule.id);
				visited.add(rule.id);
				sortedRules.push(rule);
			}
		};

		const visited = new Set<number>();
		for (const rule of this.rules)
			visit(rule, visited, new Set<number>());

		return sortedRules;
	}

	getRulesByName(name: string): Rule<T>[] {
		return this.rules.filter(rule => rule.name === name);
	}

	getRulesByID(id: number): Rule<T>[] {
		return this.rules.filter(rule => rule.id === id);
	}

	static PrintValidationResults(results: ValidationResult[], options: ValidationOptions = new ValidationOptions()) {
		if (options.skipBuiltIns)
			results = results.filter(x => x.ruleID < 1000);

		if (options.exclude)
			results = results.filter(x => !options.exclude!.includes(x.ruleID));

		if (options.errorsOnly)
			results = results.filter(x => x.result == ResultTypes.Failure);

		if (results.length > 0) {
			LoggingUtils.log("Rule Validation:", ConsoleColor.Blue);
			results.forEach(x => LoggingUtils.log(`  ${LoggingUtils.icon(x.result!)}  Rule: [${x.ruleID}] "${x.ruleName}". Result: ${ResultTypes[x.result!]}`, x.result == ResultTypes.Success ? ConsoleColor.Green : ConsoleColor.Red));
			console.log();
		}

		if (options.throwOnError && results.filter(x => x.result != ResultTypes.Success).length > 0)
			throw new Error("Rule Validation Errors");
	}
}

export class WhenClause<T> {
	constructor(private ruleEngine: RuleEngine<T>, private condition: (obj: T) => boolean) { }

	then(consequent: (obj: T) => boolean): DescriptionClause<T> {
		return new DescriptionClause(this.ruleEngine, (obj: T) => {
			return !this.condition(obj) || consequent(obj);
		}, []);
	}
}

export class DescriptionClause<T> {
	private dependencies: (number | BuiltInRules)[] = [];

	constructor(private ruleEngine: RuleEngine<T>, private check: (obj: T) => boolean, dependencies: (number | BuiltInRules)[] = []) {
		this.dependencies = dependencies;
	}

	because(description: string): Rule<T> {
		return this.ruleEngine.addRule(description, this.check, { dependsOn: this.dependencies });
	}

	dependsOn(...dependencies: (number | BuiltInRules | (number | BuiltInRules)[])[]): DescriptionClause<T> {
		const flatDependencies = dependencies.flat();
		const tmp = (obj: T) => {
			const dependenciesMet = flatDependencies.every(depId => this.ruleEngine.checkRuleById(depId, obj));
			return dependenciesMet && this.check(obj);
		};
		return new DescriptionClause(this.ruleEngine, tmp, [...this.dependencies, ...flatDependencies]);
	}
}
export class ValidationResult {
	value: any;
	ruleName: string;
	ruleID: number;
	error: string | null;
	message: string;
	result: ResultTypes | null;

	public static FromRule(ruleName: string, ruleID: number, result: ResultTypes, message: string) {
		const retval = new ValidationResult();
		retval.ruleName = ruleName;
		retval.ruleID = ruleID;
		retval.result = result;
		retval.message = message;
		return retval;
	}

	public static FromSuccess(value: any, message: string): ValidationResult {
		const retval = new ValidationResult();
		retval.value = value;
		retval.message = message;
		retval.result = ResultTypes.Success;
		return retval;
	}

	public static FromFailure(error: string): ValidationResult {
		const retval = new ValidationResult();
		retval.error = error;
		retval.result = ResultTypes.Failure;
		return retval;
	}

	public static PrintValidationResults(results: ValidationResult[], options: ValidationOptions) {
		const errors = results.filter(x => x.result == ResultTypes.Failure);
		if (errors.length > 0) {
			LoggingUtils.log("Type Validation Errors:", ConsoleColor.Red);
			errors.forEach((message) => LoggingUtils.log(`  ${LoggingUtils.icon(false)}  ${message.error}`));
			console.log();
		}

		const successes = results.filter(x => x.result != ResultTypes.Failure);

		const suffix = "is valid.";
		successes.sort((a, b) => {
			const aa = a.message.endsWith(suffix);
			const bb = b.message.endsWith(suffix);

			return Number(bb) - Number(aa);
		});

		if (!options.errorsOnly && successes.length > 0) {
			LoggingUtils.log("Type Validation Messages:", ConsoleColor.Green);
			successes.forEach((message) => LoggingUtils.log(`  ${LoggingUtils.icon(true)}  ${message.message}`));
			console.log();
		}

		if (options.throwOnError && errors.length > 0)
			throw new Error("Validation Errors");
	}

	public static HasError(results: ValidationResult[]): boolean {
		return results.filter(x => x.result == ResultTypes.Failure).length > 0;
	}
}