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

use monochange_core::AnalyzedFileChange;
use monochange_core::DetectionLevel;
use monochange_core::Ecosystem;
use monochange_core::MonochangeResult;
use monochange_core::PackageAnalysisContext;
use monochange_core::PackageAnalysisResult;
use monochange_core::PackageRecord;
use monochange_core::PackageSnapshot;
use monochange_core::PackageSnapshotFile;
use monochange_core::SemanticAnalyzer;
use monochange_core::SemanticChange;
use monochange_core::SemanticChangeCategory;
use monochange_core::SemanticChangeKind;
use serde_yaml_ng::Mapping;
use serde_yaml_ng::Value;

use crate::PUBSPEC_FILE;

/// Dart and Flutter analyzer that extracts top-level Dart symbols and `pubspec.yaml` diffs.
#[derive(Debug, Clone, Copy, Default)]
pub struct DartSemanticAnalyzer;

/// Return the shared Dart and Flutter semantic analyzer.
#[must_use]
pub const fn semantic_analyzer() -> DartSemanticAnalyzer {
	DartSemanticAnalyzer
}

impl SemanticAnalyzer for DartSemanticAnalyzer {
	fn analyzer_id(&self) -> &'static str {
		"dart/pubspec"
	}

	fn applies_to(&self, package: &PackageRecord) -> bool {
		matches!(package.ecosystem, Ecosystem::Dart)
	}

	fn analyze_package(
		&self,
		context: &PackageAnalysisContext<'_>,
	) -> MonochangeResult<PackageAnalysisResult> {
		let mut semantic_changes = Vec::new();
		let mut warnings = Vec::new();

		if context.detection_level != DetectionLevel::Basic {
			let before_symbols =
				snapshot_public_symbols(context.before_snapshot, context.changed_files);
			let after_symbols =
				snapshot_public_symbols(context.after_snapshot, context.changed_files);
			semantic_changes.extend(diff_public_symbols(&before_symbols, &after_symbols));
		}

		if let Some(manifest_change) = context
			.changed_files
			.iter()
			.find(|change| change.package_path == Path::new(PUBSPEC_FILE))
		{
			semantic_changes.extend(analyze_manifest_change(manifest_change, &mut warnings));
		}

		semantic_changes.sort_by(|left, right| {
			(
				left.category,
				left.kind,
				left.item_kind.as_str(),
				left.item_path.as_str(),
			)
				.cmp(&(
					right.category,
					right.kind,
					right.item_kind.as_str(),
					right.item_path.as_str(),
				))
		});

		Ok(PackageAnalysisResult {
			analyzer_id: self.analyzer_id().to_string(),
			package_id: display_package_id(context.package),
			ecosystem: context.package.ecosystem,
			changed_files: context
				.changed_files
				.iter()
				.map(|file| file.package_path.clone())
				.collect(),
			semantic_changes,
			warnings,
		})
	}
}

fn display_package_id(package: &PackageRecord) -> String {
	package
		.metadata
		.get("config_id")
		.cloned()
		.unwrap_or_else(|| package.id.clone())
}

#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
struct PublicSymbol {
	item_kind: String,
	item_path: String,
	signature: String,
	file_path: PathBuf,
}

fn snapshot_public_symbols(
	snapshot: Option<&PackageSnapshot>,
	changed_files: &[AnalyzedFileChange],
) -> BTreeMap<(String, String), PublicSymbol> {
	let mut symbols = BTreeMap::new();

	if let Some(snapshot) = snapshot {
		for file in &snapshot.files {
			if !is_public_dart_source_file(&file.path) {
				continue;
			}

			for symbol in collect_public_symbols(file) {
				symbols.insert((symbol.item_kind.clone(), symbol.item_path.clone()), symbol);
			}
		}

		return symbols;
	}

	for change in changed_files {
		let Some(contents) = change
			.after_contents
			.as_deref()
			.or(change.before_contents.as_deref())
		else {
			continue;
		};
		if !is_public_dart_source_file(&change.package_path) {
			continue;
		}

		let file = PackageSnapshotFile {
			path: change.package_path.clone(),
			contents: contents.to_string(),
		};
		for symbol in collect_public_symbols(&file) {
			symbols.insert((symbol.item_kind.clone(), symbol.item_path.clone()), symbol);
		}
	}

	symbols
}

fn is_public_dart_source_file(path: &Path) -> bool {
	path.extension().and_then(|ext| ext.to_str()) == Some("dart")
		&& path.starts_with("lib")
		&& !path.starts_with("lib/src/generated")
}

fn collect_public_symbols(file: &PackageSnapshotFile) -> Vec<PublicSymbol> {
	let module_prefix = module_prefix_for_file(&file.path);
	let mut symbols = Vec::new();
	let mut brace_depth = 0_i32;

	for raw_line in file.contents.lines() {
		let line = trim_inline_comment(raw_line).trim();
		if line.is_empty() {
			brace_depth = update_brace_depth(brace_depth, line);
			continue;
		}

		if brace_depth != 0 {
			brace_depth = update_brace_depth(brace_depth, line);
			continue;
		}

		let normalized_line = normalize_signature(line);
		if let Some(target) = parse_export_directive(&normalized_line) {
			push_symbol(
				&mut symbols,
				"reexport",
				&[],
				target,
				&normalized_line,
				&file.path,
			);
		}

		if let Some((item_kind, item_name)) = parse_type_declaration(&normalized_line) {
			push_symbol(
				&mut symbols,
				item_kind,
				&module_prefix,
				item_name,
				&normalized_line,
				&file.path,
			);
		}

		if let Some(function_name) = parse_top_level_function(&normalized_line) {
			push_symbol(
				&mut symbols,
				"function",
				&module_prefix,
				function_name,
				&normalized_line,
				&file.path,
			);
		}

		brace_depth = update_brace_depth(brace_depth, line);
	}

	symbols
}

fn trim_inline_comment(line: &str) -> &str {
	line.split("//").next().unwrap_or(line)
}

fn normalize_signature(line: &str) -> String {
	line.split_whitespace().collect::<Vec<_>>().join(" ")
}

fn update_brace_depth(current: i32, line: &str) -> i32 {
	let opens = line.matches('{').count() as i32;
	let closes = line.matches('}').count() as i32;
	(current + opens - closes).max(0)
}

fn parse_export_directive(line: &str) -> Option<String> {
	let target = line.strip_prefix("export ")?;
	extract_quoted_text(target).filter(|path| !path.is_empty())
}

fn parse_type_declaration(line: &str) -> Option<(&'static str, String)> {
	for (keyword, item_kind) in [
		("class", "class"),
		("enum", "enum"),
		("mixin", "mixin"),
		("typedef", "typedef"),
		("extension", "extension"),
	] {
		if let Some(name) = find_keyword_name(line, keyword)
			&& !name.starts_with('_')
		{
			return Some((item_kind, name));
		}
	}

	None
}

fn find_keyword_name(line: &str, keyword: &str) -> Option<String> {
	let mut previous = "";
	for token in line.split_whitespace() {
		if previous == keyword {
			let name = token
				.trim_matches(|character: char| {
					!character.is_ascii_alphanumeric() && character != '_'
				})
				.to_string();
			return (!name.is_empty()).then_some(name);
		}
		previous = token;
	}

	None
}

fn parse_top_level_function(line: &str) -> Option<String> {
	if !line.contains('(') || line.starts_with("export ") {
		return None;
	}
	if line.starts_with("if ")
		|| line.starts_with("for ")
		|| line.starts_with("while ")
		|| line.starts_with("switch ")
		|| line.starts_with("return ")
		|| line.starts_with("assert ")
		|| line.starts_with("catch ")
		|| line.starts_with("typedef ")
	{
		return None;
	}

	let open_paren = line.find('(')?;
	let before = line[..open_paren].trim_end();
	if before.is_empty() || before.ends_with('=') {
		return None;
	}

	let name = before
		.split_whitespace()
		.last()?
		.trim_matches(|character: char| !character.is_ascii_alphanumeric() && character != '_')
		.to_string();
	(!name.is_empty() && !name.starts_with('_')).then_some(name)
}

fn extract_quoted_text(text: &str) -> Option<String> {
	let quote = text
		.chars()
		.find(|character| matches!(character, '\'' | '"'))?;
	let start = text.find(quote)? + 1;
	let end = text[start..].find(quote)? + start;
	Some(text[start..end].to_string())
}

fn module_prefix_for_file(path: &Path) -> Vec<String> {
	let mut components = path
		.parent()
		.map(|parent| {
			parent
				.components()
				.filter_map(|component| component.as_os_str().to_str())
				.map(ToString::to_string)
				.collect::<Vec<_>>()
		})
		.unwrap_or_default();

	if components
		.first()
		.is_some_and(|component| component == "lib")
	{
		components.remove(0);
	}

	let stem = path
		.file_stem()
		.and_then(|stem| stem.to_str())
		.unwrap_or_default();
	if !stem.is_empty() && stem != "index" {
		components.push(stem.to_string());
	}

	components
}

#[allow(clippy::needless_pass_by_value)]
fn push_symbol(
	output: &mut Vec<PublicSymbol>,
	item_kind: &str,
	module_prefix: &[String],
	item_name: String,
	signature: &str,
	file_path: &Path,
) {
	let item_path = if module_prefix.is_empty() {
		item_name.clone()
	} else {
		format!("{}::{item_name}", module_prefix.join("::"))
	};

	output.push(PublicSymbol {
		item_kind: item_kind.to_string(),
		item_path,
		signature: signature.to_string(),
		file_path: file_path.to_path_buf(),
	});
}

fn diff_public_symbols(
	before: &BTreeMap<(String, String), PublicSymbol>,
	after: &BTreeMap<(String, String), PublicSymbol>,
) -> Vec<SemanticChange> {
	let mut changes = Vec::new();

	for (key, after_symbol) in after {
		match before.get(key) {
			None => {
				changes.push(build_symbol_change(
					SemanticChangeKind::Added,
					after_symbol,
					None,
					Some(after_symbol.signature.clone()),
				));
			}
			Some(before_symbol) if before_symbol.signature != after_symbol.signature => {
				changes.push(build_symbol_change(
					SemanticChangeKind::Modified,
					after_symbol,
					Some(before_symbol.signature.clone()),
					Some(after_symbol.signature.clone()),
				));
			}
			Some(_) => {}
		}
	}

	for (key, before_symbol) in before {
		if after.contains_key(key) {
			continue;
		}

		changes.push(build_symbol_change(
			SemanticChangeKind::Removed,
			before_symbol,
			Some(before_symbol.signature.clone()),
			None,
		));
	}

	changes
}

fn build_symbol_change(
	kind: SemanticChangeKind,
	symbol: &PublicSymbol,
	before_signature: Option<String>,
	after_signature: Option<String>,
) -> SemanticChange {
	let verb = if kind == SemanticChangeKind::Added {
		"added"
	} else if kind == SemanticChangeKind::Removed {
		"removed"
	} else {
		"modified"
	};

	SemanticChange {
		category: SemanticChangeCategory::PublicApi,
		kind,
		item_kind: symbol.item_kind.clone(),
		item_path: symbol.item_path.clone(),
		summary: format!("{} `{}` {verb}", symbol.item_kind, symbol.item_path),
		file_path: symbol.file_path.clone(),
		before_signature,
		after_signature,
	}
}

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
struct ManifestEntry {
	item_kind: String,
	value: String,
}

fn analyze_manifest_change(
	change: &AnalyzedFileChange,
	warnings: &mut Vec<String>,
) -> Vec<SemanticChange> {
	let mut changes = Vec::new();

	let before_manifest = parse_manifest(
		change.before_contents.as_deref(),
		&change.package_path,
		warnings,
	);
	let after_manifest = parse_manifest(
		change.after_contents.as_deref(),
		&change.package_path,
		warnings,
	);

	let before_exports = before_manifest
		.as_ref()
		.map(extract_export_entries)
		.unwrap_or_default();
	let after_exports = after_manifest
		.as_ref()
		.map(extract_export_entries)
		.unwrap_or_default();
	changes.extend(compare_manifest_entries(
		SemanticChangeCategory::Export,
		&change.package_path,
		&before_exports,
		&after_exports,
	));

	let before_dependencies = before_manifest
		.as_ref()
		.map(extract_dependency_entries)
		.unwrap_or_default();
	let after_dependencies = after_manifest
		.as_ref()
		.map(extract_dependency_entries)
		.unwrap_or_default();
	changes.extend(compare_manifest_entries(
		SemanticChangeCategory::Dependency,
		&change.package_path,
		&before_dependencies,
		&after_dependencies,
	));

	let before_metadata = before_manifest
		.as_ref()
		.map(extract_metadata_entries)
		.unwrap_or_default();
	let after_metadata = after_manifest
		.as_ref()
		.map(extract_metadata_entries)
		.unwrap_or_default();
	changes.extend(compare_manifest_entries(
		SemanticChangeCategory::Metadata,
		&change.package_path,
		&before_metadata,
		&after_metadata,
	));

	changes
}

fn parse_manifest(
	contents: Option<&str>,
	path: &Path,
	warnings: &mut Vec<String>,
) -> Option<Mapping> {
	let contents = contents?;
	match serde_yaml_ng::from_str::<Mapping>(contents) {
		Ok(value) => Some(value),
		Err(error) => {
			warnings.push(format!("failed to parse {}: {error}", path.display()));
			None
		}
	}
}

fn extract_export_entries(value: &Mapping) -> BTreeMap<String, ManifestEntry> {
	value
		.get(Value::String("executables".to_string()))
		.and_then(Value::as_mapping)
		.map(|executables| {
			executables
				.iter()
				.filter_map(|(name, value)| {
					name.as_str().map(|name| {
						(
							name.to_string(),
							ManifestEntry {
								item_kind: "command".to_string(),
								value: describe_yaml_value(value),
							},
						)
					})
				})
				.collect::<BTreeMap<_, _>>()
		})
		.unwrap_or_default()
}

fn extract_dependency_entries(value: &Mapping) -> BTreeMap<String, ManifestEntry> {
	let mut entries = BTreeMap::new();

	for (section, item_kind) in [
		("dependencies", "dependency"),
		("dev_dependencies", "dev_dependency"),
		("dependency_overrides", "dependency_override"),
	] {
		let Some(section_mapping) = value
			.get(Value::String(section.to_string()))
			.and_then(Value::as_mapping)
		else {
			continue;
		};

		for (name, entry) in section_mapping {
			let Some(name) = name.as_str() else {
				continue;
			};
			entries.insert(
				name.to_string(),
				ManifestEntry {
					item_kind: item_kind.to_string(),
					value: format!("[{section}] {}", describe_yaml_value(entry)),
				},
			);
		}
	}

	entries
}

fn extract_metadata_entries(value: &Mapping) -> BTreeMap<String, ManifestEntry> {
	let mut entries = BTreeMap::new();

	for field in ["publish_to"] {
		if let Some(field_value) = value.get(Value::String(field.to_string())) {
			entries.insert(
				field.to_string(),
				ManifestEntry {
					item_kind: "manifest_field".to_string(),
					value: describe_yaml_value(field_value),
				},
			);
		}
	}

	entries.extend(
		value
			.get(Value::String("environment".to_string()))
			.and_then(Value::as_mapping)
			.into_iter()
			.flat_map(|environment| {
				["sdk", "flutter"].into_iter().filter_map(move |field| {
					environment
						.get(Value::String(field.to_string()))
						.map(|field_value| {
							(
								format!("environment.{field}"),
								ManifestEntry {
									item_kind: "environment".to_string(),
									value: describe_yaml_value(field_value),
								},
							)
						})
				})
			}),
	);

	entries.extend(
		value
			.get(Value::String("flutter".to_string()))
			.and_then(Value::as_mapping)
			.and_then(|flutter| {
				flutter
					.get(Value::String("plugin".to_string()))
					.and_then(Value::as_mapping)
			})
			.and_then(|plugin| {
				plugin
					.get(Value::String("platforms".to_string()))
					.and_then(Value::as_mapping)
			})
			.into_iter()
			.flat_map(|platforms| {
				platforms
					.iter()
					.filter_map(|(platform_name, platform_value)| {
						platform_name.as_str().map(|platform_name| {
							(
								format!("flutter.plugin.platform.{platform_name}"),
								ManifestEntry {
									item_kind: "plugin_platform".to_string(),
									value: describe_yaml_value(platform_value),
								},
							)
						})
					})
			}),
	);

	entries
}

fn compare_manifest_entries(
	category: SemanticChangeCategory,
	file_path: &Path,
	before: &BTreeMap<String, ManifestEntry>,
	after: &BTreeMap<String, ManifestEntry>,
) -> Vec<SemanticChange> {
	let mut changes = Vec::new();

	for (name, after_entry) in after {
		match before.get(name) {
			None => {
				changes.push(build_manifest_change(
					category,
					SemanticChangeKind::Added,
					file_path,
					name,
					after_entry,
					None,
					Some(after_entry.value.clone()),
				));
			}
			Some(before_entry) if before_entry != after_entry => {
				changes.push(build_manifest_change(
					category,
					SemanticChangeKind::Modified,
					file_path,
					name,
					after_entry,
					Some(before_entry.value.clone()),
					Some(after_entry.value.clone()),
				));
			}
			Some(_) => {}
		}
	}

	for (name, before_entry) in before {
		if after.contains_key(name) {
			continue;
		}

		changes.push(build_manifest_change(
			category,
			SemanticChangeKind::Removed,
			file_path,
			name,
			before_entry,
			Some(before_entry.value.clone()),
			None,
		));
	}

	changes
}

fn build_manifest_change(
	category: SemanticChangeCategory,
	kind: SemanticChangeKind,
	file_path: &Path,
	item_path: &str,
	entry: &ManifestEntry,
	before_signature: Option<String>,
	after_signature: Option<String>,
) -> SemanticChange {
	let verb = if kind == SemanticChangeKind::Added {
		"added"
	} else if kind == SemanticChangeKind::Removed {
		"removed"
	} else {
		"modified"
	};

	SemanticChange {
		category,
		kind,
		item_kind: entry.item_kind.clone(),
		item_path: item_path.to_string(),
		summary: format!("{} `{}` {verb}", entry.item_kind, item_path),
		file_path: file_path.to_path_buf(),
		before_signature,
		after_signature,
	}
}

fn describe_yaml_value(value: &Value) -> String {
	match value {
		Value::Null => "null".to_string(),
		Value::Bool(boolean) => boolean.to_string(),
		Value::Number(number) => number.to_string(),
		Value::String(text) => text.clone(),
		Value::Sequence(items) => {
			items
				.iter()
				.map(describe_yaml_value)
				.collect::<Vec<_>>()
				.join(", ")
		}
		Value::Mapping(mapping) => {
			let mut fields = mapping
				.iter()
				.filter_map(|(key, value)| {
					key.as_str()
						.map(|key| format!("{key}={}", describe_yaml_value(value)))
				})
				.collect::<Vec<_>>();
			fields.sort();
			fields.join(", ")
		}
		Value::Tagged(tagged) => describe_yaml_value(&tagged.value),
	}
}

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