//! Check command implementation for monochange CLI.
//!
//! `mc check` combines workspace validation with manifest lint enforcement.

use std::collections::BTreeMap;
use std::fmt::Write;
use std::path::Path;
use std::path::PathBuf;

use clap::ArgMatches;
use monochange_config::load_workspace_configuration;
use monochange_core::MonochangeError;
use monochange_core::MonochangeResult;
use monochange_core::lint::LintPreset;
use monochange_core::lint::LintProgressReporter;
use monochange_core::lint::LintReport;
use monochange_core::lint::LintRule;
use monochange_core::lint::LintSeverity;
use monochange_core::lint::LintSuite;
use monochange_lint::LintSelection;
use monochange_lint::Linter;

use crate::OutputFormat;

#[allow(clippy::vec_init_then_push)]
fn lint_suites() -> Vec<Box<dyn LintSuite>> {
	let mut suites: Vec<Box<dyn LintSuite>> = Vec::new();
	#[cfg(feature = "cargo")]
	suites.push(Box::new(monochange_cargo::lints::lint_suite()));
	#[cfg(feature = "npm")]
	suites.push(Box::new(monochange_npm::lints::lint_suite()));
	#[cfg(feature = "dart")]
	suites.push(Box::new(monochange_dart::lints::lint_suite()));
	suites.push(Box::new(monochange_config::lints::lint_suite()));
	suites
}

fn build_linter(
	configuration: &monochange_core::WorkspaceConfiguration,
	selection: LintSelection,
) -> Linter {
	Linter::new(lint_suites(), configuration.lints.clone()).with_selection(selection)
}

pub(crate) fn collect_workspace_validation_issues(root: &Path) -> (Vec<String>, Vec<String>) {
	let mut warnings = Vec::new();
	let mut errors = Vec::new();

	if let Err(error) = monochange_config::validate_workspace(root) {
		errors.push(error.render());
	}

	match monochange_config::validate_versioned_files_content(root) {
		Ok(mut collected_warnings) => warnings.append(&mut collected_warnings),
		Err(error) => errors.push(error.render()),
	}

	(warnings, errors)
}

pub(crate) fn available_lint_rules() -> Vec<LintRule> {
	let mut rules = Linter::new(
		lint_suites(),
		monochange_core::lint::WorkspaceLintSettings::default(),
	)
	.registry()
	.rules();
	rules.sort_by(|left, right| left.id.cmp(&right.id));
	rules
}

pub(crate) fn available_lint_presets() -> Vec<LintPreset> {
	let mut presets = Linter::new(
		lint_suites(),
		monochange_core::lint::WorkspaceLintSettings::default(),
	)
	.registry()
	.presets();
	presets.sort_by(|left, right| left.id.cmp(&right.id));
	presets
}

pub(crate) fn explain_lint_rule(rule_id: &str) -> Option<LintRule> {
	Linter::new(
		lint_suites(),
		monochange_core::lint::WorkspaceLintSettings::default(),
	)
	.registry()
	.find_rule(rule_id)
}

pub(crate) fn explain_lint_preset(preset_id: &str) -> Option<LintPreset> {
	Linter::new(
		lint_suites(),
		monochange_core::lint::WorkspaceLintSettings::default(),
	)
	.registry()
	.find_preset(preset_id)
}

/// Run the check command (validate + lint).
pub(crate) fn run_check_command(
	root: &Path,
	fix: bool,
	ecosystems: &[String],
	only_rules: &[String],
	format: OutputFormat,
) -> MonochangeResult<String> {
	let configuration = load_workspace_configuration(root)?;
	let mut output = String::new();

	let (validation_warnings, validation_errors) = collect_workspace_validation_issues(root);
	for warning in &validation_warnings {
		let _ = writeln!(output, "warning: {warning}");
	}
	if validation_errors.is_empty() {
		let _ = writeln!(output, "workspace validation passed for {}", root.display());
	} else {
		let _ = writeln!(output, "workspace validation failed for {}", root.display());
		for error in &validation_errors {
			let _ = writeln!(output, "{error}");
		}
	}

	let selection = LintSelection::all()
		.with_suites(ecosystems.iter().cloned())
		.with_rules(only_rules.iter().cloned());
	let linter = build_linter(&configuration, selection);

	let show_progress = matches!(format, OutputFormat::Text | OutputFormat::Markdown);
	let reporter = if show_progress {
		Some(crate::lint_check_reporter::HumanLintProgressReporter::new())
	} else {
		None
	};

	let report = if let Some(ref r) = reporter {
		linter.lint_workspace(root, &configuration, r)
	} else {
		linter.lint_workspace(
			root,
			&configuration,
			&monochange_core::lint::NoopLintProgressReporter,
		)
	};

	let mut fixed_files: Vec<(PathBuf, String)> = Vec::new();
	if fix {
		let fixes = linter.apply_fixes(&report);
		if let Some(ref r) = reporter {
			r.fix_started(fixes.len());
		}
		for (file_path, fixed_content) in fixes {
			std::fs::write(&file_path, fixed_content).map_err(|error| {
				MonochangeError::Io(format!(
					"Failed to write fixed content to {}: {}",
					file_path.display(),
					error
				))
			})?;
			if let Some(ref r) = reporter {
				let description = report
					.autofixable()
					.iter()
					.find(|res| res.location.file_path == file_path)
					.and_then(|res| res.fix.as_ref())
					.map_or("fixed", |f| f.description.as_str());
				fixed_files.push((file_path.clone(), description.to_string()));
				r.fix_applied(&file_path, description);
			}
		}
		if let Some(ref r) = reporter {
			r.fix_finished(fixed_files.len());
		}
	}

	let lint_has_errors = report.has_errors();
	let validation_has_errors = !validation_errors.is_empty();
	if let Some(r) = reporter {
		r.summary(
			report.error_count,
			report.warning_count,
			report.autofixable().len(),
			fix,
		);
		r.finish();
	}

	match format {
		OutputFormat::Json => {
			if validation_has_errors {
				return Err(MonochangeError::Config(format!("check failed:\n{output}")));
			}
			Ok(serde_json::to_string_pretty(&report)
				.unwrap_or_else(|error| panic!("serializing lint reports should succeed: {error}")))
		}
		OutputFormat::Text | OutputFormat::Markdown => {
			output.push_str(&format_check_report(&report, fix));
			if validation_has_errors || lint_has_errors {
				Err(MonochangeError::Config(format!("check failed:\n{output}")))
			} else {
				Ok(output)
			}
		}
	}
}

/// Run lint as part of a Validate step. Returns (`formatted_output`, `has_errors`).
#[allow(dead_code)]
pub(crate) fn run_lint_step(root: &Path, fix: bool) -> MonochangeResult<(String, bool)> {
	let configuration = load_workspace_configuration(root)?;
	let linter = build_linter(&configuration, LintSelection::all());
	let report = linter.lint_workspace(
		root,
		&configuration,
		&monochange_core::lint::NoopLintProgressReporter,
	);
	let has_errors = report.has_errors();

	if fix {
		let fixes = linter.apply_fixes(&report);
		for (file_path, fixed_content) in fixes {
			std::fs::write(&file_path, fixed_content).map_err(|error| {
				MonochangeError::Io(format!(
					"Failed to write fixed content to {}: {}",
					file_path.display(),
					error
				))
			})?;
		}
	}

	Ok((format_check_report(&report, fix), has_errors))
}

#[allow(clippy::unnecessary_wraps)]
pub(crate) fn render_lint_catalog(format: OutputFormat) -> MonochangeResult<String> {
	let rules = available_lint_rules();
	let presets = available_lint_presets();
	match format {
		OutputFormat::Json => {
			Ok(serde_json::to_string_pretty(&serde_json::json!({
				"rules": rules,
				"presets": presets,
			}))
			.unwrap_or_else(|error| panic!("serializing lint catalog should succeed: {error}")))
		}
		OutputFormat::Text | OutputFormat::Markdown => {
			let mut output = String::new();
			output.push_str("Rules:\n");
			for rule in rules {
				let _ = writeln!(
					output,
					"- {} [{:?} {:?}]{}",
					rule.id,
					rule.category,
					rule.maturity,
					if rule.autofixable { " [fixable]" } else { "" }
				);
				let _ = writeln!(output, "  {}", rule.description);
			}
			output.push_str("\nPresets:\n");
			for preset in presets {
				let _ = writeln!(output, "- {} [{:?}]", preset.id, preset.maturity);
				let _ = writeln!(output, "  {}", preset.description);
			}
			Ok(output)
		}
	}
}

pub(crate) fn render_lint_explanation(id: &str, format: OutputFormat) -> MonochangeResult<String> {
	if let Some(rule) = explain_lint_rule(id) {
		return match format {
			OutputFormat::Json => {
				Ok(serde_json::to_string_pretty(&rule).unwrap_or_else(|error| {
					panic!("serializing lint rule explanations should succeed: {error}")
				}))
			}
			OutputFormat::Text | OutputFormat::Markdown => {
				let mut output = String::new();
				let _ = writeln!(output, "{}", rule.id);
				let _ = writeln!(output, "name: {}", rule.name);
				let _ = writeln!(output, "category: {:?}", rule.category);
				let _ = writeln!(output, "maturity: {:?}", rule.maturity);
				let _ = writeln!(output, "autofixable: {}", rule.autofixable);
				let _ = writeln!(output, "\n{}", rule.description);
				for (index, option) in rule.options.into_iter().enumerate() {
					if index == 0 {
						output.push_str("\nOptions:\n");
					}
					let _ = writeln!(output, "- {} ({:?})", option.name, option.kind);
					let _ = writeln!(output, "  {}", option.description);
				}
				Ok(output)
			}
		};
	}

	if let Some(preset) = explain_lint_preset(id) {
		return match format {
			OutputFormat::Json => {
				Ok(
					serde_json::to_string_pretty(&preset).unwrap_or_else(|error| {
						panic!("serializing lint preset explanations should succeed: {error}")
					}),
				)
			}
			OutputFormat::Text | OutputFormat::Markdown => {
				let mut output = String::new();
				let _ = writeln!(output, "{}", preset.id);
				let _ = writeln!(output, "name: {}", preset.name);
				let _ = writeln!(output, "maturity: {:?}", preset.maturity);
				let _ = writeln!(output, "\n{}", preset.description);
				output.push_str("\nRules:\n");
				for (rule_id, config) in preset.rules {
					let _ = writeln!(output, "- {} = {}", rule_id, config.severity());
				}
				Ok(output)
			}
		};
	}

	Err(MonochangeError::Config(format!(
		"unknown lint rule or preset `{id}`"
	)))
}

pub(crate) fn handle_lint_subcommand(
	root: &Path,
	lint_matches: &ArgMatches,
) -> MonochangeResult<String> {
	let (subcommand, subcommand_matches) = lint_matches
		.subcommand()
		.expect("clap requires a lint subcommand");

	if subcommand == "list" {
		let format = subcommand_matches
			.get_one::<String>("format")
			.map_or(Ok(OutputFormat::Markdown), |value| {
				crate::parse_output_format(value)
			})?;
		return render_lint_catalog(format);
	}

	if subcommand == "explain" {
		let format = subcommand_matches
			.get_one::<String>("format")
			.map_or(Ok(OutputFormat::Markdown), |value| {
				crate::parse_output_format(value)
			})?;
		let id = subcommand_matches
			.get_one::<String>("id")
			.expect("clap requires a lint id")
			.as_str();
		return render_lint_explanation(id, format);
	}

	let id = subcommand_matches
		.get_one::<String>("id")
		.expect("clap requires a lint id")
		.as_str();
	scaffold_lint_rule(root, id)
}

pub(crate) fn scaffold_lint_rule(root: &Path, id: &str) -> MonochangeResult<String> {
	let (suite, rule_name) = id.split_once('/').ok_or_else(|| {
		MonochangeError::Config("lint ids must use the form <ecosystem>/<rule-name>".to_string())
	})?;
	let crate_name = match suite {
		"cargo" => "monochange_cargo",
		"npm" => "monochange_npm",
		"dart" => "monochange_dart",
		other => {
			return Err(MonochangeError::Config(format!(
				"scaffolding is not yet supported for lint suite `{other}`"
			)));
		}
	};
	let module_name = rule_name.replace('-', "_");
	let struct_name = rule_name
		.split('-')
		.map(|segment| {
			let mut chars = segment.chars();
			match chars.next() {
				Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
				None => String::new(),
			}
		})
		.collect::<String>()
		+ "Rule";

	let lint_dir = root.join("crates").join(crate_name).join("src/lints");
	std::fs::create_dir_all(&lint_dir).map_err(|error| {
		MonochangeError::Io(format!(
			"failed to create lint directory {}: {error}",
			lint_dir.display()
		))
	})?;
	let lint_file = lint_dir.join(format!("{module_name}.rs"));
	if lint_file.exists() {
		return Err(MonochangeError::Config(format!(
			"lint file {} already exists",
			lint_file.display()
		)));
	}

	let fixture_dir = root
		.join("fixtures/tests/lints")
		.join(suite)
		.join(rule_name)
		.join("workspace");
	std::fs::create_dir_all(&fixture_dir).map_err(|error| {
		MonochangeError::Io(format!(
			"failed to create lint fixture directory {}: {error}",
			fixture_dir.display()
		))
	})?;

	let template = format!(
		r#"use monochange_core::lint::LintContext;
use monochange_core::lint::LintResult;
use monochange_core::lint::LintRule;
use monochange_core::lint::LintRuleConfig;
use monochange_core::lint::LintRuleRunner;
use monochange_linting::declare_lint_rule;
use monochange_linting::LintCategory;
use monochange_linting::LintMaturity;

// Keep `declare_lint_rule!` for straightforward metadata-only construction.
// If this rule later needs extra constructor state, switch to an explicit
// `struct` plus `LintRule::new(...)`.
declare_lint_rule! {{
    pub {struct_name},
    id: "{suite}/{rule_name}",
    name: "TODO: rename me",
    description: "TODO: describe what this lint checks",
    category: LintCategory::BestPractice,
    maturity: LintMaturity::Experimental,
    autofixable: false,
}}

impl LintRuleRunner for {struct_name} {{
    fn rule(&self) -> &LintRule {{
        &self.rule
    }}

    fn run(&self, _ctx: &LintContext<'_>, _config: &LintRuleConfig) -> Vec<LintResult> {{
        Vec::new()
    }}
}}
"#
	);
	std::fs::write(&lint_file, template).map_err(|error| {
		MonochangeError::Io(format!(
			"failed to write lint file {}: {error}",
			lint_file.display()
		))
	})?;

	let note_file = fixture_dir.join("README.md");
	std::fs::write(
		&note_file,
		format!("# {id}\n\nAdd fixture workspaces for snapshot and autofix tests here.\n"),
	)
	.map_err(|error| {
		MonochangeError::Io(format!(
			"failed to write fixture note {}: {error}",
			note_file.display()
		))
	})?;

	Ok(format!(
		"Created {} and {}.\nNext steps:\n- wire `mod {module_name};` into `crates/{crate_name}/src/lints/mod.rs`\n- register `{struct_name}::new()` in the suite\n- add fixture scenarios under {}",
		lint_file.display(),
		note_file.display(),
		fixture_dir.display()
	))
}

fn format_check_report(report: &LintReport, fixed: bool) -> String {
	if report.results.is_empty() && report.warnings.is_empty() {
		return "lint: no issues found\n".to_string();
	}

	let mut output = String::new();
	let _ = write!(
		output,
		"lint: {} errors, {} warnings\n\n",
		report.error_count, report.warning_count
	);

	for warning in &report.warnings {
		let _ = writeln!(output, "warning: {warning}");
	}
	if !report.warnings.is_empty() {
		output.push('\n');
	}

	let mut by_file: BTreeMap<&Path, Vec<&monochange_core::lint::LintResult>> = BTreeMap::new();
	for result in &report.results {
		by_file
			.entry(&result.location.file_path)
			.or_default()
			.push(result);
	}

	for (file, results) in by_file {
		let _ = writeln!(output, "{}:", file.display());
		for result in results {
			let severity_icon = match result.severity {
				LintSeverity::Error => "✗",
				LintSeverity::Warning => "⚠",
				LintSeverity::Off => "·",
			};
			let fix_indicator = if result.fix.is_some() {
				" [fixable]"
			} else {
				""
			};
			let _ = writeln!(
				output,
				"  {} {} at {}:{}{}",
				severity_icon,
				result.message,
				result.location.line,
				result.location.column,
				fix_indicator
			);
		}
		output.push('\n');
	}

	if fixed {
		output.push_str("Fixed all auto-fixable issues.\n");
	} else if report.autofixable().is_empty() {
		output.push_str("No auto-fixable issues found.\n");
	} else {
		let _ = writeln!(
			output,
			"{} issue(s) can be auto-fixed. Run with --fix to apply.",
			report.autofixable().len()
		);
	}

	output
}

#[cfg(test)]
#[path = "__tests__/lint_tests.rs"]
mod tests;
