import { groupBy } from 'ramda'
import { transformAST } from './AST'
import { ASTNode } from './AST/types'
import { InternalError, warning } from './error'
import { defaultNode } from './evaluation'
import parse from './parse'
import { Context } from './parsePublicodes'
import { Rule, RuleNode } from './rule'
import { coerceArray } from './utils'

export type ReplacementRule = {
	nodeKind: 'replacementRule'
	definitionRule: ASTNode & { nodeKind: 'reference' }
	replacedReference: ASTNode & { nodeKind: 'reference' }
	replacementNode: ASTNode
	whiteListedNames: Array<ASTNode & { nodeKind: 'reference' }>
	rawNode: any
	blackListedNames: Array<ASTNode & { nodeKind: 'reference' }>
}

export function parseReplacements(
	replacements: Rule['remplace'],
	context: Context
): Array<ReplacementRule> {
	if (!replacements) {
		return []
	}
	return coerceArray(replacements).map((replacement) => {
		if (typeof replacement === 'string') {
			replacement = { règle: replacement }
		}

		const replacedReference = parse(replacement.règle, context)
		const replacementNode = parse(
			replacement.par ?? context.dottedName,
			context
		)

		const [whiteListedNames, blackListedNames] = [
			replacement.dans ?? [],
			replacement['sauf dans'] ?? [],
		]
			.map((dottedName) => coerceArray(dottedName))
			.map((refs) => refs.map((ref) => parse(ref, context)))

		return {
			nodeKind: 'replacementRule',
			rawNode: replacement,
			definitionRule: parse(context.dottedName, context),
			replacedReference,
			replacementNode,
			whiteListedNames,
			blackListedNames,
		} as ReplacementRule
	})
}

export function parseRendNonApplicable(
	rules: Rule['rend non applicable'],
	context: Context
): Array<ReplacementRule> {
	return parseReplacements(rules, context).map(
		(replacement) =>
			({
				...replacement,
				replacementNode: defaultNode(false),
			} as ReplacementRule)
	)
}

export function getReplacements(
	parsedRules: Record<string, RuleNode>
): Record<string, Array<ReplacementRule>> {
	return groupBy(
		(r: ReplacementRule) => {
			if (!r.replacedReference.dottedName) {
				throw new InternalError(r)
			}
			return r.replacedReference.dottedName
		},
		Object.values(parsedRules).flatMap((rule) => rule.replacements)
	)
}

export function inlineReplacements(
	replacements: Record<string, Array<ReplacementRule>>
): (n: ASTNode) => ASTNode {
	return transformAST((n, fn) => {
		if (
			n.nodeKind === 'replacementRule' ||
			n.nodeKind === 'inversion' ||
			n.nodeKind === 'une possibilité'
		) {
			return false
		}
		if (n.nodeKind === 'recalcul') {
			// We don't replace references in recalcul keys
			return {
				...n,
				explanation: {
					recalcul: fn(n.explanation.recalcul),
					amendedSituation: n.explanation.amendedSituation.map(
						([name, value]) => [name, fn(value)]
					),
				},
			}
		}
		if (n.nodeKind === 'reference') {
			if (!n.dottedName) {
				throw new InternalError(n)
			}
			return replace(n, replacements[n.dottedName] ?? [])
		}
	})
}

function replace(
	node: ASTNode & { nodeKind: 'reference' }, //& { dottedName: string },
	replacements: Array<ReplacementRule>
): ASTNode {
	// TODO : handle transitivité

	const applicableReplacements = replacements
		.filter(
			({ definitionRule }) =>
				definitionRule.dottedName !== node.contextDottedName
		)
		.filter(
			({ whiteListedNames }) =>
				!whiteListedNames.length ||
				whiteListedNames.some((name) =>
					node.contextDottedName.startsWith(name.dottedName as string)
				)
		)
		.filter(
			({ blackListedNames }) =>
				!blackListedNames.length ||
				blackListedNames.every(
					(name) =>
						!node.contextDottedName.startsWith(name.dottedName as string)
				)
		)
		.sort((r1, r2) => {
			// Replacement with whitelist conditions have precedence over the others
			const criterion1 =
				+!!r2.whiteListedNames.length - +!!r1.whiteListedNames.length
			// Replacement with blacklist condition have precedence over the others
			const criterion2 =
				+!!r2.blackListedNames.length - +!!r1.blackListedNames.length
			return criterion1 || criterion2
		})
	if (!applicableReplacements.length) {
		return node
	}
	if (applicableReplacements.length > 1) {
		warning(
			node.contextDottedName,
			`
Il existe plusieurs remplacements pour la référence '${node.dottedName}'.
Lors de l'execution, ils seront résolus dans l'odre suivant :
${applicableReplacements.map(
	(replacement) =>
		`\n\t- Celui définit dans la règle '${replacement.definitionRule.dottedName}'`
)}
`
		)
	}

	return {
		nodeKind: 'variations',
		visualisationKind: 'replacement',
		rawNode: node.rawNode,
		explanation: [
			...applicableReplacements.map((replacement) => ({
				condition: replacement.definitionRule,
				consequence: replacement.replacementNode,
			})),
			{
				condition: defaultNode(true),
				consequence: node,
			},
		],
	}
}
