use std::path::Path;
use std::path::PathBuf;

use monochange_analysis::AnalysisConfig;
use monochange_analysis::ChangeAnalysis;
use monochange_analysis::ChangeFrame;
use monochange_analysis::DetectionLevel;
use monochange_analysis::ReleaseTrajectoryRefs;
use monochange_config::load_workspace_configuration;
use monochange_config::resolve_package_reference;
use monochange_core::EffectiveReleaseIdentity;
use monochange_core::MonochangeError;
use monochange_core::MonochangeResult;
use monochange_core::PackageRecord;
use monochange_core::ReleaseOwnerKind;
use monochange_core::VersionFormat;
use serde::Serialize;

use crate::OutputFormat;
use crate::discover_workspace;
use crate::git_support::resolve_git_commit_ref;
use crate::git_support::run_git_capture;
use crate::release_artifacts::parse_tag_prefix_and_version;

#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AnalyzePackageSelection {
	requested_reference: String,
	package_id: String,
	package_record_id: String,
	package_name: String,
	ecosystem: monochange_core::Ecosystem,
	manifest_path: PathBuf,
	version_group_id: Option<String>,
}

#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AnalyzeRefs {
	release: Option<String>,
	main: String,
	head: String,
}

#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AnalyzeFrames {
	release_to_main: Option<ChangeAnalysis>,
	main_to_head: ChangeAnalysis,
	release_to_head: Option<ChangeAnalysis>,
}

#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AnalyzeReport {
	package: AnalyzePackageSelection,
	release_identity: Option<EffectiveReleaseIdentity>,
	first_release: bool,
	refs: AnalyzeRefs,
	frames: AnalyzeFrames,
	warnings: Vec<String>,
}

pub(crate) async fn render_analyze_report(
	root: &Path,
	package_reference: &str,
	release_ref: Option<&str>,
	main_ref: Option<&str>,
	head_ref: Option<&str>,
	detection_level: &str,
	format: OutputFormat,
) -> MonochangeResult<String> {
	let detection_level = parse_detection_level(detection_level)?;
	let refs = (release_ref, main_ref, head_ref);
	let report = build_report(root, package_reference, refs, detection_level).await?;

	match format {
		OutputFormat::Json => {
			let rendered = serde_json::to_string_pretty(&report);
			debug_assert!(
				rendered.is_ok(),
				"analyze report serialization should succeed"
			);
			Ok(rendered.unwrap_or_default())
		}
		OutputFormat::Markdown | OutputFormat::Text => Ok(render_text_report(&report)),
	}
}

async fn build_report(
	root: &Path,
	package_reference: &str,
	refs: (Option<&str>, Option<&str>, Option<&str>),
	detection_level: DetectionLevel,
) -> MonochangeResult<AnalyzeReport> {
	let (release_ref, main_ref, head_ref) = refs;
	let configuration = load_workspace_configuration(root)?;
	let discovery = discover_workspace(root)?;
	let selected_record_id =
		resolve_package_reference(package_reference, root, &discovery.packages)?;
	let selected_package = discovery
		.packages
		.iter()
		.find(|package| package.id == selected_record_id)
		.cloned()
		.expect("resolved package should exist in discovery packages");
	let selected_package_id = preferred_package_id(&selected_package);
	let selected_manifest_path = selected_package
		.relative_manifest_path(root)
		.unwrap_or_else(|| selected_package.manifest_path.clone());
	let release_identity = configuration.effective_release_identity(&selected_package_id);
	let main_ref = match main_ref {
		Some(main_ref) => main_ref.to_string(),
		None => default_branch_name(root).await?,
	};
	let head_ref = head_ref.map_or_else(|| "HEAD".to_string(), ToString::to_string);
	let resolved_release_ref = match release_ref {
		Some(release_ref) => Some(release_ref.to_string()),
		None => latest_release_tag_for_identity(root, release_identity.as_ref()).await?,
	};
	let config = AnalysisConfig {
		detection_level,
		..AnalysisConfig::default()
	};
	let mut warnings = Vec::new();
	let (first_release, frames) = if let Some(release_ref) = &resolved_release_ref {
		let refs = ReleaseTrajectoryRefs {
			release_ref: release_ref.clone(),
			main_ref: main_ref.clone(),
			head_ref: head_ref.clone(),
		};
		let trajectory =
			monochange_analysis::analyze_release_trajectory_for_refs(root, &refs, &config)?;
		warnings.extend(trajectory.warnings);
		(
			false,
			AnalyzeFrames {
				release_to_main: Some(filter_change_analysis(
					trajectory.frames.release_to_main,
					&selected_package_id,
				)),
				main_to_head: filter_change_analysis(
					trajectory.frames.main_to_head,
					&selected_package_id,
				),
				release_to_head: Some(filter_change_analysis(
					trajectory.frames.release_to_head,
					&selected_package_id,
				)),
			},
		)
	} else {
		warnings.push(first_release_warning(
			&selected_package_id,
			&main_ref,
			&head_ref,
			release_identity.as_ref(),
		));
		let main_to_head =
			analyze_package_range(root, &main_ref, &head_ref, &config, &selected_package_id)?;
		(
			true,
			AnalyzeFrames {
				release_to_main: None,
				main_to_head,
				release_to_head: None,
			},
		)
	};

	Ok(AnalyzeReport {
		package: AnalyzePackageSelection {
			requested_reference: package_reference.to_string(),
			package_id: selected_package_id,
			package_record_id: selected_package.id,
			package_name: selected_package.name,
			ecosystem: selected_package.ecosystem,
			manifest_path: selected_manifest_path,
			version_group_id: selected_package.version_group_id,
		},
		release_identity,
		first_release,
		refs: AnalyzeRefs {
			release: resolved_release_ref,
			main: main_ref,
			head: head_ref,
		},
		frames,
		warnings,
	})
}

fn analyze_package_range(
	root: &Path,
	base: &str,
	head: &str,
	config: &AnalysisConfig,
	selected_package_id: &str,
) -> MonochangeResult<ChangeAnalysis> {
	let frame = ChangeFrame::CustomRange {
		base: base.to_string(),
		head: head.to_string(),
	};
	let analysis = monochange_analysis::analyze_changes(root, &frame, config)?;

	Ok(filter_change_analysis(analysis, selected_package_id))
}

fn parse_detection_level(value: &str) -> MonochangeResult<DetectionLevel> {
	match value {
		"basic" => Ok(DetectionLevel::Basic),
		"signature" => Ok(DetectionLevel::Signature),
		"semantic" => Ok(DetectionLevel::Semantic),
		_ => {
			Err(MonochangeError::Config(format!(
				"unknown detection level `{value}`; expected one of: basic, signature, semantic"
			)))
		}
	}
}

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

fn parse_origin_head_branch(symbolic_ref: &str) -> Option<String> {
	let branch = symbolic_ref.trim();
	let branch = branch.strip_prefix("origin/").unwrap_or(branch);

	match branch {
		"" | "HEAD" => None,
		_ => Some(branch.to_string()),
	}
}

async fn default_branch_name(root: &Path) -> MonochangeResult<String> {
	let result = run_git_capture(
		root,
		&["rev-parse", "--abbrev-ref", "origin/HEAD"],
		"failed to determine default branch from origin/HEAD",
	)
	.await
	.ok()
	.and_then(|symbolic_ref| parse_origin_head_branch(&symbolic_ref));
	match result {
		Some(branch) => Ok(branch),
		None => fallback_default_branch(root).await,
	}
}

async fn fallback_default_branch(root: &Path) -> MonochangeResult<String> {
	for branch in ["main", "master"] {
		if resolve_git_commit_ref(root, branch).await.is_ok() {
			return Ok(branch.to_string());
		}
	}

	Err(MonochangeError::Discovery(
		"could not determine default branch".to_string(),
	))
}

async fn latest_release_tag_for_identity(
	root: &Path,
	release_identity: Option<&EffectiveReleaseIdentity>,
) -> MonochangeResult<Option<String>> {
	let Some(release_identity) = release_identity else {
		return Ok(None);
	};
	if !release_identity.tag {
		return Ok(None);
	}

	let tag_prefix = tag_prefix_for_identity(release_identity);
	let tag_output = run_git_capture(
		root,
		&["tag", "--list", "--sort=-v:refname"],
		"failed to list git tags for release baseline resolution",
	)
	.await?;
	let latest = tag_output
		.lines()
		.map(str::trim)
		.filter(|tag| !tag.is_empty())
		.find_map(|tag| {
			let (prefix, _) = parse_tag_prefix_and_version(tag)?;
			(prefix == tag_prefix).then(|| tag.to_string())
		});

	Ok(latest)
}

fn tag_prefix_for_identity(release_identity: &EffectiveReleaseIdentity) -> String {
	match release_identity.version_format {
		VersionFormat::Namespaced => format!("{}/v", release_identity.owner_id),
		_ => "v".to_string(),
	}
}

fn first_release_warning(
	selected_package_id: &str,
	main_ref: &str,
	head_ref: &str,
	release_identity: Option<&EffectiveReleaseIdentity>,
) -> String {
	let Some(release_identity) = release_identity else {
		return format!(
			"package `{selected_package_id}` has no configured release identity; analyzing only `{main_ref} -> {head_ref}`"
		);
	};

	format!(
		"no prior release tag found for {} `{}`; treating `{selected_package_id}` as a first release and analyzing only `{main_ref} -> {head_ref}`",
		release_owner_label(release_identity.owner_kind),
		release_identity.owner_id,
	)
}

fn release_owner_label(owner_kind: ReleaseOwnerKind) -> &'static str {
	if owner_kind == ReleaseOwnerKind::Group {
		"group"
	} else {
		"package"
	}
}

fn filter_change_analysis(
	mut analysis: ChangeAnalysis,
	selected_package_id: &str,
) -> ChangeAnalysis {
	analysis
		.package_analyses
		.retain(|package_id, _| package_id == selected_package_id);
	analysis
}

fn render_text_report(report: &AnalyzeReport) -> String {
	let mut lines = vec!["analyze:".to_string()];
	lines.push(format!(
		"  package: {} ({})",
		report.package.package_id, report.package.package_name
	));
	lines.push(format!(
		"  package record: {}",
		report.package.package_record_id
	));
	lines.push(format!(
		"  ecosystem: {}",
		report.package.ecosystem.as_str()
	));
	lines.push(format!(
		"  manifest: {}",
		report.package.manifest_path.display()
	));
	if let Some(version_group_id) = &report.package.version_group_id {
		lines.push(format!("  version group: {version_group_id}"));
	}
	if let Some(release_identity) = &report.release_identity {
		lines.push(format!(
			"  release identity: {} {}",
			release_owner_label(release_identity.owner_kind),
			release_identity.owner_id
		));
	}
	lines.push("  refs:".to_string());
	if let Some(release_ref) = &report.refs.release {
		lines.push(format!("    release: {release_ref}"));
	} else {
		lines.push("    release: none".to_string());
	}
	lines.push(format!("    main: {}", report.refs.main));
	lines.push(format!("    head: {}", report.refs.head));
	lines.push(format!("  first release: {}", yes_no(report.first_release)));

	if !report.warnings.is_empty() {
		lines.push(String::new());
		lines.push("warnings:".to_string());
		for warning in &report.warnings {
			lines.push(format!("- {warning}"));
		}
	}

	render_frame_section(
		&mut lines,
		"main -> head",
		&report.frames.main_to_head,
		&report.package.package_id,
	);

	if let Some(release_to_main) = &report.frames.release_to_main {
		render_frame_section(
			&mut lines,
			"release -> main",
			release_to_main,
			&report.package.package_id,
		);
	}

	if let Some(release_to_head) = &report.frames.release_to_head {
		render_frame_section(
			&mut lines,
			"release -> head",
			release_to_head,
			&report.package.package_id,
		);
	}

	lines.join("\n")
}

fn render_frame_section(
	lines: &mut Vec<String>,
	label: &str,
	analysis: &ChangeAnalysis,
	selected_package_id: &str,
) {
	lines.push(String::new());
	lines.push(format!("{label}:"));
	lines.push(format!("  frame: {}", analysis.frame));
	let Some(package_analysis) = analysis.package_analyses.get(selected_package_id) else {
		let no_change_message = [
			"  no semantic changes detected for `",
			selected_package_id,
			"` in this frame",
		]
		.concat();
		lines.push(no_change_message);
		push_warning_lines_if_any(lines, &analysis.warnings);
		return;
	};

	let semantic_change_count = package_analysis.semantic_changes.len();
	lines.push(format!("  semantic changes: {semantic_change_count}"));
	push_changed_file_lines_if_any(lines, &package_analysis.changed_files);
	push_semantic_change_lines_if_any(lines, &package_analysis.semantic_changes);
	push_section_warnings_if_any(lines, &package_analysis.warnings, &analysis.warnings);
}

fn push_changed_file_lines_if_any(lines: &mut Vec<String>, changed_files: &[PathBuf]) {
	if changed_files.is_empty() {
		return;
	}

	push_changed_file_lines(lines, changed_files);
}

fn push_changed_file_lines(lines: &mut Vec<String>, changed_files: &[PathBuf]) {
	lines.push("  changed files:".to_string());
	lines.extend(
		changed_files
			.iter()
			.map(|changed_file| format!("  - {}", changed_file.display())),
	);
}

fn push_semantic_change_lines_if_any(
	lines: &mut Vec<String>,
	semantic_changes: &[monochange_analysis::SemanticChange],
) {
	if semantic_changes.is_empty() {
		return;
	}

	push_semantic_change_lines(lines, semantic_changes);
}

fn push_semantic_change_lines(
	lines: &mut Vec<String>,
	semantic_changes: &[monochange_analysis::SemanticChange],
) {
	lines.push("  changes:".to_string());
	lines.extend(
		semantic_changes
			.iter()
			.map(|change| format!("  - {} ({})", change.summary, change.file_path.display())),
	);
}

fn push_warning_lines_if_any(lines: &mut Vec<String>, warnings: &[String]) {
	if warnings.is_empty() {
		return;
	}

	push_warning_lines(lines, warnings);
}

fn push_section_warnings_if_any(
	lines: &mut Vec<String>,
	package_warnings: &[String],
	analysis_warnings: &[String],
) {
	if package_warnings.is_empty() && analysis_warnings.is_empty() {
		return;
	}

	lines.push("  warnings:".to_string());
	push_warning_items(lines, package_warnings);
	push_warning_items(lines, analysis_warnings);
}

fn push_warning_lines(lines: &mut Vec<String>, warnings: &[String]) {
	lines.push("  warnings:".to_string());
	push_warning_items(lines, warnings);
}

fn push_warning_items(lines: &mut Vec<String>, warnings: &[String]) {
	lines.extend(warnings.iter().map(|warning| format!("  - {warning}")));
}

fn yes_no(value: bool) -> &'static str {
	if value { "yes" } else { "no" }
}

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