import { mapObjIndexed } from 'ramda'
import { evaluateControls } from './controls'
import { evaluationError, warning } from './error'
import { collectDefaults, evaluateNode } from './evaluation'
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
import { parse } from './parse'
import parseRules from './parseRules'
import { EvaluatedNode, EvaluatedRule, ParsedRules, Rules } from './types'
import { parseUnit } from './units'

const emptyCache = () => ({
	_meta: { contextRule: [] }
})

type Cache = {
	_meta: {
		contextRule: Array<string>
		inversionFail?: {
			given: string
			estimated: string
		}
	}
}
type Situation<Names extends string> = Partial<
	Record<Names, object | string | number>
>

type EvaluatedSituation<Names extends string> = Partial<
	Record<Names, object | number | EvaluatedNode<Names>>
>

export type EvaluationOptions = Partial<{
	unit: string
}>

export * from './components'
export { formatValue } from './format'
export { default as translateRules } from './translateRules'
export { buildRulesDependencies } from './cyclesLib'
export * from './types'
export { parseRules }

export default class Engine<Names extends string> {
	parsedRules: ParsedRules<Names>
	situation: Situation<Names> = {}
	private cache: Cache
	private warnings: Array<string> = []

	constructor(rules: string | Rules<Names> | ParsedRules<Names>) {
		this.cache = emptyCache()
		this.parsedRules =
			typeof rules === 'string' || !(Object.values(rules)[0] as any)?.dottedName
				? parseRules(rules)
				: (rules as ParsedRules<Names>)
	}

	private resetCache() {
		this.cache = emptyCache()
	}

	private evaluateExpression(
		expression: string,
		context: string
	): EvaluatedNode<Names> {
		// EN ATTENDANT d'AVOIR une meilleure gestion d'erreur, on va mocker
		// console.warn
		const originalWarn = console.warn
		console.warn = (warning: string) => {
			this.warnings.push(warning)
			originalWarn(warning)
		}
		const result = simplifyNodeUnit(
			evaluateNode(
				this.cache,
				this.situation,
				this.parsedRules,
				parse(
					this.parsedRules,
					{ dottedName: context },
					this.parsedRules
				)(expression)
			)
		)
		console.warn = originalWarn

		if (Object.keys(result.defaultValue?.missingVariable ?? {}).length) {
			throw evaluationError(
				context,
				"Impossible d'évaluer l'expression car celle ci fait appel à des variables manquantes"
			)
		}
		return result
	}

	setSituation(
		situation: Partial<Record<Names, string | number | object>> = {}
	) {
		this.resetCache()
		this.situation = mapObjIndexed(
			(value, name) =>
				typeof value === 'string'
					? this.evaluateExpression(value, `[situation] ${name}`)
					: value,
			situation
		) as EvaluatedSituation<Names>
		return this
	}

	evaluate(expression: Names, options?: EvaluationOptions): EvaluatedRule<Names>
	evaluate(
		expression: string,
		options?: EvaluationOptions
	): EvaluatedNode<Names> | EvaluatedRule<Names>
	evaluate(expression: string, options?: EvaluationOptions) {
		let result = this.evaluateExpression(
			expression,
			`[evaluation] ${expression}`
		)
		if (result.category === 'reference' && result.explanation) {
			result = {
				nodeValue: result.nodeValue,
				missingVariables: result.missingVariables,
				...('unit' in result && { unit: result.unit }),
				...('temporalValue' in result && {
					temporalValue: result.temporalValue
				}),
				...result.explanation
			} as EvaluatedRule<Names>
		}
		if (options?.unit) {
			try {
				return convertNodeToUnit(
					parseUnit(options.unit),
					result as EvaluatedNode<Names, number>
				)
			} catch (e) {
				warning(
					`[evaluation] ${expression}`,
					"L'unité demandée est incompatible avec l'expression évaluée"
				)
			}
		}
		return result
	}

	controls() {
		return evaluateControls(this.cache, this.situation, this.parsedRules)
	}

	getWarnings() {
		return this.warnings
	}

	inversionFail(): boolean {
		return !!this.cache._meta.inversionFail
	}

	getParsedRules(): ParsedRules<Names> {
		return this.parsedRules
	}
}