/*
	DrCr: Web-based double-entry bookkeeping framework
	Copyright (C) 2022-2025  Lee Yingtong Li (RunasSudo)

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU Affero General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Affero General Public License for more details.

	You should have received a copy of the GNU Affero General Public License
	along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

//! This module implements the dependency resolution for [ReportingStep]s

use super::types::{
	ReportingContext, ReportingProductId, ReportingStep, ReportingStepDynamicBuilder,
	ReportingStepFromArgsFn, ReportingStepId,
};

/// List of dependencies between [ReportingStep]s and [ReportingProduct][super::types::ReportingProduct]s
#[derive(Debug)]
pub struct ReportingGraphDependencies {
	vec: Vec<Dependency>,
}

impl ReportingGraphDependencies {
	/// Get the list of [Dependency]s
	pub fn vec(&self) -> &Vec<Dependency> {
		&self.vec
	}

	/// Record that the [ReportingStep] depends on the [ReportingProduct][super::types::ReportingProduct]
	pub fn add_dependency(&mut self, step: ReportingStepId, product: ReportingProductId) {
		if !self
			.vec
			.iter()
			.any(|d| d.step == step && d.product == product)
		{
			self.vec.push(Dependency { step, product });
		}
	}

	/// Get the [Dependency]s for the given [ReportingStep]
	pub fn dependencies_for_step(&self, step: &ReportingStepId) -> Vec<&Dependency> {
		return self.vec.iter().filter(|d| d.step == *step).collect();
	}
}

/// Represents that a [ReportingStep] depends on a [ReportingProduct][super::types::ReportingProduct]
#[derive(Debug)]
pub struct Dependency {
	pub step: ReportingStepId,
	pub product: ReportingProductId,
}

/// Indicates an error during dependency resolution in [steps_for_targets]
#[derive(Debug)]
pub enum ReportingCalculationError {
	UnknownStep { message: String },
	NoStepForProduct { message: String },
	CircularDependencies,
}

pub enum HasStepOrCanBuild<'a, 'b> {
	HasStep(&'a Box<dyn ReportingStep>),
	CanLookup(ReportingStepFromArgsFn),
	CanBuild(&'b ReportingStepDynamicBuilder),
	None,
}

/// Determines whether the [ReportingProduct][super::types::ReportingProduct] is generated by a known step, or can be generated by a lookup function or dynamic builder
pub fn has_step_or_can_build<'a, 'b>(
	product: &ReportingProductId,
	steps: &'a Vec<Box<dyn ReportingStep>>,
	dependencies: &ReportingGraphDependencies,
	context: &'b ReportingContext,
) -> HasStepOrCanBuild<'a, 'b> {
	if let Some(step) = steps.iter().find(|s| {
		s.id().name == product.name
			&& s.id().args == product.args
			&& s.id().product_kinds.contains(&product.kind)
	}) {
		return HasStepOrCanBuild::HasStep(step);
	}

	// Try lookup function
	if let Some(lookup_key) = context
		.step_lookup_fn
		.keys()
		.find(|(name, kinds)| *name == product.name && kinds.contains(&product.kind))
	{
		let (takes_args_fn, from_args_fn) = context.step_lookup_fn.get(lookup_key).unwrap();
		if takes_args_fn(&product.name, &product.args, context) {
			return HasStepOrCanBuild::CanLookup(*from_args_fn);
		}
	}

	// No explicit step for product - try builders
	for builder in context.step_dynamic_builders.iter() {
		if (builder.can_build)(
			&product.name,
			product.kind,
			&product.args,
			steps,
			dependencies,
			context,
		) {
			return HasStepOrCanBuild::CanBuild(builder);
		}
	}

	return HasStepOrCanBuild::None;
}

/// Generates a new step which generates the requested [ReportingProduct][super::types::ReportingProduct], using a lookup function or dynamic builder
///
/// Panics if a known step already generates the requested [ReportingProduct][super::types::ReportingProduct].
fn build_step_for_product(
	product: &ReportingProductId,
	steps: &Vec<Box<dyn ReportingStep>>,
	dependencies: &ReportingGraphDependencies,
	context: &ReportingContext,
) -> Option<Box<dyn ReportingStep>> {
	let new_step;
	match has_step_or_can_build(product, steps, dependencies, context) {
		HasStepOrCanBuild::HasStep(_) => {
			panic!("Attempted to call build_step_for_product for already existing step")
		}
		HasStepOrCanBuild::CanLookup(from_args_fn) => {
			new_step = from_args_fn(&product.name, product.args.clone(), context);

			// Check new step meets the dependency
			if new_step.id().name != product.name {
				panic!(
					"Unexpected step returned from lookup function (expected name {}, got {})",
					product.name,
					new_step.id().name
				);
			}
			if new_step.id().args != product.args {
				panic!(
					"Unexpected step returned from lookup function {} (expected args {:?}, got {:?})",
					product.name,
					product.args,
					new_step.id().args
				);
			}
			if !new_step.id().product_kinds.contains(&product.kind) {
				panic!(
					"Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})",
					product.name,
					product.kind,
					new_step.id().product_kinds
				);
			}
		}
		HasStepOrCanBuild::CanBuild(builder) => {
			new_step = (builder.build)(
				product.name.clone(),
				product.kind,
				product.args.clone(),
				&steps,
				&dependencies,
				&context,
			);

			// Check new step meets the dependency
			if new_step.id().name != product.name {
				panic!(
					"Unexpected step returned from builder {} (expected name {}, got {})",
					builder.name,
					product.name,
					new_step.id().name
				);
			}
			if new_step.id().args != product.args {
				panic!(
					"Unexpected step returned from builder {} for {} (expected args {:?}, got {:?})",
					builder.name,
					product.name,
					product.args,
					new_step.id().args
				);
			}
			if !new_step.id().product_kinds.contains(&product.kind) {
				panic!(
					"Unexpected step returned from builder {} for {} (expected kind {:?}, got {:?})",
					builder.name,
					product.name,
					product.kind,
					new_step.id().product_kinds
				);
			}
		}
		HasStepOrCanBuild::None => {
			return None;
		}
	}

	Some(new_step)
}

/// Check whether the [ReportingStep] would be ready to execute, if the given previous steps have already completed
pub(crate) fn would_be_ready_to_execute(
	step: &Box<dyn ReportingStep>,
	steps: &Vec<Box<dyn ReportingStep>>,
	dependencies: &ReportingGraphDependencies,
	previous_steps: &Vec<usize>,
) -> bool {
	'check_each_dependency: for dependency in dependencies.vec.iter() {
		if dependency.step == step.id() {
			// Check if the dependency has been produced by a previous step
			for previous_step in previous_steps {
				if steps[*previous_step].id().name == dependency.product.name
					&& steps[*previous_step].id().args == dependency.product.args
					&& steps[*previous_step]
						.id()
						.product_kinds
						.contains(&dependency.product.kind)
				{
					continue 'check_each_dependency;
				}
			}

			// Dependency is not met
			return false;
		}
	}
	true
}

/// Recursively resolve the dependencies of the target [ReportingProductId]s and return a sorted [Vec] of [ReportingStep]s
pub fn steps_for_targets(
	targets: Vec<ReportingProductId>,
	context: &ReportingContext,
) -> Result<(Vec<Box<dyn ReportingStep>>, ReportingGraphDependencies), ReportingCalculationError> {
	let mut steps: Vec<Box<dyn ReportingStep>> = Vec::new();
	let mut dependencies = ReportingGraphDependencies { vec: Vec::new() };

	// Process initial targets
	for target in targets.iter() {
		if !steps.iter().any(|s| {
			s.id().name == target.name
				&& s.id().args == target.args
				&& s.id().product_kinds.contains(&target.kind)
		}) {
			// No current step generates the product - try to lookup or build
			if let Some(new_step) = build_step_for_product(&target, &steps, &dependencies, context)
			{
				steps.push(new_step);
				let new_step = steps.last().unwrap();
				for dependency in new_step.requires(&context) {
					dependencies.add_dependency(new_step.id(), dependency);
				}
				new_step.init_graph(&steps, &mut dependencies, &context);
			} else {
				return Err(ReportingCalculationError::NoStepForProduct {
					message: format!("No step builds target product {}", target),
				});
			}
		}
	}

	// Call after_init_graph
	for step in steps.iter() {
		step.as_ref()
			.after_init_graph(&steps, &mut dependencies, &context);
	}

	// Recursively process dependencies
	loop {
		let mut new_steps = Vec::new();

		for dependency in dependencies.vec.iter() {
			if !steps.iter().any(|s| s.id() == dependency.step) {
				// Unknown step for which a dependency has been declared
				// FIXME: Call the lookup function
				todo!();
			}
			if !steps.iter().any(|s| {
				s.id().name == dependency.product.name
					&& s.id().args == dependency.product.args
					&& s.id().product_kinds.contains(&dependency.product.kind)
			}) {
				// No current step generates the product - try to lookup or build
				if let Some(new_step) =
					build_step_for_product(&dependency.product, &steps, &dependencies, context)
				{
					new_steps.push(new_step);
				}
			}
		}

		if new_steps.len() == 0 {
			break;
		}

		// Initialise new steps
		let mut new_step_indexes = Vec::new();
		for new_step in new_steps {
			new_step_indexes.push(steps.len());
			steps.push(new_step);
			let new_step = steps.last().unwrap();
			for dependency in new_step.requires(&context) {
				dependencies.add_dependency(new_step.id(), dependency);
			}
			new_step
				.as_ref()
				.init_graph(&steps, &mut dependencies, &context);
		}

		// Call after_init_graph on all steps
		for step in steps.iter() {
			step.as_ref()
				.after_init_graph(&steps, &mut dependencies, &context);
		}
	}

	// Check all dependencies satisfied
	for dependency in dependencies.vec.iter() {
		if !steps.iter().any(|s| s.id() == dependency.step) {
			return Err(ReportingCalculationError::UnknownStep {
				message: format!(
					"No implementation for step {} which {} is a dependency of",
					dependency.step, dependency.product
				),
			});
		}
		if !steps.iter().any(|s| {
			s.id().name == dependency.product.name
				&& s.id().args == dependency.product.args
				&& s.id().product_kinds.contains(&dependency.product.kind)
		}) {
			return Err(ReportingCalculationError::NoStepForProduct {
				message: format!(
					"No step builds product {} wanted by {}",
					dependency.product, dependency.step
				),
			});
		}
	}

	// Sort
	let mut sorted_step_indexes = Vec::new();
	let mut steps_remaining = steps.iter().enumerate().collect::<Vec<_>>();

	'loop_until_all_sorted: while !steps_remaining.is_empty() {
		for (cur_index, (orig_index, step)) in steps_remaining.iter().enumerate() {
			if would_be_ready_to_execute(step, &steps, &dependencies, &sorted_step_indexes) {
				sorted_step_indexes.push(*orig_index);
				steps_remaining.remove(cur_index);
				continue 'loop_until_all_sorted;
			}
		}

		// No steps to execute - must be circular dependency
		return Err(ReportingCalculationError::CircularDependencies);
	}

	let mut sort_mapping = vec![0_usize; sorted_step_indexes.len()];
	for i in 0..sorted_step_indexes.len() {
		sort_mapping[sorted_step_indexes[i]] = i;
	}

	// TODO: This can be done in place
	let mut sorted_steps = steps.into_iter().zip(sort_mapping).collect::<Vec<_>>();
	sorted_steps.sort_unstable_by_key(|(_s, order)| *order);
	let sorted_steps = sorted_steps
		.into_iter()
		.map(|(s, _idx)| s)
		.collect::<Vec<_>>();

	Ok((sorted_steps, dependencies))
}

/// Generate graphviz code representing the dependency tree
///
/// Useful for debugging or visualisation. Can be compiled using e.g. `dot -Tpdf -O output.gv`.
pub fn steps_as_graphviz(
	steps: &Vec<Box<dyn ReportingStep>>,
	dependencies: &ReportingGraphDependencies,
) -> String {
	let mut result = String::from("strict digraph drcr {\n");

	// Output all steps
	for step in steps.iter() {
		let step_display_name = step.to_string();
		if step_display_name.contains("{") {
			// Bodge: Detect dynamic step builders
			result.push_str(&format!(
				"\"{}\" [shape=box, style=dashed, label=\"{}\"];\n",
				step.id(),
				step_display_name
			));
		} else {
			result.push_str(&format!("\"{}\" [shape=box];\n", step.id()));
		}

		// Output the products of the step
		for product_kind in step.id().product_kinds.iter() {
			result.push_str(&format!(
				"\"{}\" -> \"{}\";\n",
				step.id(),
				ReportingProductId {
					name: step.id().name,
					kind: *product_kind,
					args: step.id().args
				}
			));
		}
	}

	// Output all dependencies
	for dependency in dependencies.vec().iter() {
		result.push_str(&format!(
			"\"{}\" -> \"{}\";\n",
			dependency.product, dependency.step
		));
	}

	result.push_str("}");
	result
}
