use crate::commit_shared::{git_output, git_status_success, git_stdout_trimmed};
use crate::prompt;
use std::collections::{HashMap, HashSet};
use std::process::Output;

pub fn dispatch(cmd: &str, args: &[String]) -> Option<i32> {
    match cmd {
        "cleanup" | "delete-merged" => Some(run_cleanup(args)),
        _ => None,
    }
}

struct CleanupArgs {
    base_ref: String,
    squash_mode: bool,
    remove_worktrees: bool,
    help: bool,
}

fn run_cleanup(args: &[String]) -> i32 {
    let parsed = match parse_args(args) {
        Ok(value) => value,
        Err(code) => return code,
    };

    if parsed.help {
        print_help();
        return 0;
    }

    if !git_status_success(&["rev-parse", "--is-inside-work-tree"]) {
        eprintln!("❌ Not in a git repository");
        return 1;
    }

    let base_ref = parsed.base_ref;
    let squash_mode = parsed.squash_mode;
    let remove_worktrees = parsed.remove_worktrees;

    if !git_status_success(&["rev-parse", "--verify", "--quiet", &base_ref]) {
        eprintln!("❌ Invalid base ref: {base_ref}");
        return 1;
    }

    let base_commit = match git_stdout_trimmed(&["rev-parse", &format!("{base_ref}^{{commit}}")]) {
        Ok(value) => value,
        Err(_) => {
            eprintln!("❌ Unable to resolve base commit: {base_ref}");
            return 1;
        }
    };

    let head_commit = match git_stdout_trimmed(&["rev-parse", "HEAD"]) {
        Ok(value) => value,
        Err(_) => {
            eprintln!("❌ Unable to resolve HEAD commit");
            return 1;
        }
    };

    let delete_flag = if base_commit != head_commit {
        "-D"
    } else {
        "-d"
    };

    let current_branch = match git_stdout_trimmed(&["rev-parse", "--abbrev-ref", "HEAD"]) {
        Ok(value) => value,
        Err(_) => {
            eprintln!("❌ Unable to resolve current branch");
            return 1;
        }
    };

    let mut protected: HashSet<String> = ["main", "master", "develop", "trunk"]
        .iter()
        .map(|name| (*name).to_string())
        .collect();

    if current_branch != "HEAD" {
        protected.insert(current_branch.clone());
    }
    protected.insert(base_ref.clone());

    if let Some(base_local) = resolve_base_local(&base_ref) {
        protected.insert(base_local);
    }

    let merged_branches = match git_output(&[
        "for-each-ref",
        "--merged",
        &base_ref,
        "--format=%(refname:short)",
        "refs/heads",
    ]) {
        Ok(output) => parse_lines(&output),
        Err(err) => {
            eprintln!("{err:#}");
            return 1;
        }
    };

    let mut merged_set: HashSet<String> = HashSet::new();
    for branch in &merged_branches {
        merged_set.insert(branch.clone());
    }

    let linked_worktrees = match linked_worktrees_by_branch() {
        Ok(value) => value,
        Err(err) => {
            eprintln!("{err:#}");
            return 1;
        }
    };

    if !squash_mode && merged_branches.is_empty() {
        println!("✅ No merged local branches found.");
        return 0;
    }

    let mut candidates: Vec<String> = Vec::new();

    if squash_mode {
        let local_branches =
            match git_output(&["for-each-ref", "--format=%(refname:short)", "refs/heads"]) {
                Ok(output) => parse_lines(&output),
                Err(err) => {
                    eprintln!("{err:#}");
                    return 1;
                }
            };

        if local_branches.is_empty() {
            println!("✅ No local branches found.");
            return 0;
        }

        for branch in local_branches {
            if protected.contains(&branch) {
                continue;
            }

            if merged_set.contains(&branch) {
                candidates.push(branch);
                continue;
            }

            let cherry_output = match git_output(&["cherry", "-v", &base_ref, &branch]) {
                Ok(output) => output,
                Err(_) => {
                    eprintln!("❌ Failed to compare {branch} against {base_ref}");
                    return 1;
                }
            };

            let cherry_text = String::from_utf8_lossy(&cherry_output.stdout);
            let has_plus = cherry_text.lines().any(|line| line.starts_with('+'));
            if has_plus {
                continue;
            }

            candidates.push(branch);
        }
    } else {
        for branch in merged_branches {
            if protected.contains(&branch) {
                continue;
            }
            candidates.push(branch);
        }
    }

    if candidates.is_empty() {
        if squash_mode {
            println!("✅ No deletable branches found.");
        } else {
            println!("✅ No deletable merged branches.");
        }
        return 0;
    }

    if squash_mode {
        println!("🧹 Branches to delete (base: {base_ref}, mode: squash):");
    } else {
        println!("🧹 Merged branches to delete (base: {base_ref}):");
    }
    for branch in &candidates {
        println!("  - {branch}");
    }

    if remove_worktrees {
        let removable_worktrees: Vec<_> = candidates
            .iter()
            .filter_map(|branch| {
                linked_worktrees
                    .get(branch)
                    .map(|worktree_path| (branch, worktree_path))
            })
            .collect();

        if !removable_worktrees.is_empty() {
            println!("⚠️  Linked worktrees to remove (--remove-worktrees):");
            for (branch, worktree_path) in removable_worktrees {
                println!("  - {branch}: {worktree_path}");
            }
        }
    }

    if prompt::confirm_or_abort("❓ Proceed with deleting these branches? [y/N] ").is_err() {
        return 1;
    }

    let mut deleted_count = 0usize;
    let mut removed_worktrees_count = 0usize;
    let mut failed_deletions: Vec<(String, String)> = Vec::new();

    for branch in &candidates {
        let mut branch_delete_flag = delete_flag;
        if delete_flag == "-d" && squash_mode && !merged_set.contains(branch) {
            branch_delete_flag = "-D";
        }

        if remove_worktrees && let Some(worktree_path) = linked_worktrees.get(branch) {
            match git_output(&["worktree", "remove", "--force", worktree_path]) {
                Ok(_) => {
                    removed_worktrees_count += 1;
                }
                Err(err) => {
                    failed_deletions.push((
                        branch.clone(),
                        format!(
                            "failed to remove linked worktree {worktree_path}: {}",
                            summarize_git_error(&err.to_string())
                        ),
                    ));
                    continue;
                }
            }
        }

        match git_output(&["branch", branch_delete_flag, "--", branch]) {
            Ok(_) => {
                deleted_count += 1;
            }
            Err(err) => {
                failed_deletions.push((branch.clone(), summarize_git_error(&err.to_string())));
            }
        }
    }

    if removed_worktrees_count > 0 {
        println!("✅ Removed {removed_worktrees_count} linked worktree(s).");
    }

    if !failed_deletions.is_empty() {
        if deleted_count > 0 {
            println!("✅ Deleted {deleted_count} branch(es).");
        }

        eprintln!(
            "⚠️  Failed to delete {} branch(es):",
            failed_deletions.len()
        );
        for (branch, reason) in &failed_deletions {
            eprintln!("  - {branch}: {reason}");
        }
        return 1;
    }

    println!("✅ Deleted {deleted_count} branch(es).");
    0
}

fn parse_args(args: &[String]) -> Result<CleanupArgs, i32> {
    let mut base_ref = "HEAD".to_string();
    let mut squash_mode = false;
    let mut remove_worktrees = false;
    let mut help = false;

    let mut i = 0usize;
    while i < args.len() {
        match args[i].as_str() {
            "-h" | "--help" => {
                help = true;
            }
            "-s" | "--squash" => {
                squash_mode = true;
            }
            "-w" | "--remove-worktrees" => {
                remove_worktrees = true;
            }
            "-b" | "--base" => {
                let Some(value) = args.get(i + 1) else {
                    return Err(2);
                };
                base_ref = value.to_string();
                i += 1;
            }
            _ => {}
        }
        i += 1;
    }

    Ok(CleanupArgs {
        base_ref,
        squash_mode,
        remove_worktrees,
        help,
    })
}

fn print_help() {
    println!(
        "Usage: git-delete-merged-branches [-b|--base <ref>] [-s|--squash] [-w|--remove-worktrees]"
    );
    println!("  -b, --base <ref>  Base ref used to determine merged branches (default: HEAD)");
    println!("  -s, --squash      Include branches already applied to base (git cherry)");
    println!("  -w, --remove-worktrees  Force-remove linked worktrees for candidate branches");
}

fn parse_lines(output: &Output) -> Vec<String> {
    String::from_utf8_lossy(&output.stdout)
        .lines()
        .filter(|line| !line.trim().is_empty())
        .map(|line| line.to_string())
        .collect()
}

fn summarize_git_error(message: &str) -> String {
    let trimmed = message.trim();
    let summary = trimmed
        .rsplit_once(" failed: ")
        .map(|(_, suffix)| suffix.trim())
        .unwrap_or(trimmed);
    summary.replace('\n', " ")
}

fn linked_worktrees_by_branch() -> anyhow::Result<HashMap<String, String>> {
    let output = git_output(&["worktree", "list", "--porcelain"])?;
    let mut branch_worktrees: HashMap<String, String> = HashMap::new();
    let mut current_worktree_path: Option<String> = None;

    for line in String::from_utf8_lossy(&output.stdout).lines() {
        if line.trim().is_empty() {
            current_worktree_path = None;
            continue;
        }

        if let Some(path) = line.strip_prefix("worktree ") {
            current_worktree_path = Some(path.to_string());
            continue;
        }

        if let Some(branch_ref) = line.strip_prefix("branch refs/heads/")
            && let Some(worktree_path) = &current_worktree_path
        {
            branch_worktrees.insert(branch_ref.to_string(), worktree_path.clone());
        }
    }

    Ok(branch_worktrees)
}

fn resolve_base_local(base_ref: &str) -> Option<String> {
    let remote_ref = format!("refs/remotes/{base_ref}");
    if git_status_success(&["show-ref", "--verify", "--quiet", &remote_ref]) {
        return Some(
            base_ref
                .split_once('/')
                .map(|(_, tail)| tail.to_string())
                .unwrap_or_else(|| base_ref.to_string()),
        );
    }

    let local_ref = format!("refs/heads/{base_ref}");
    if git_status_success(&["show-ref", "--verify", "--quiet", &local_ref]) {
        return Some(base_ref.to_string());
    }

    None
}

#[cfg(test)]
mod tests {
    use super::{
        dispatch, linked_worktrees_by_branch, parse_args, parse_lines, resolve_base_local,
        summarize_git_error,
    };
    use nils_test_support::{EnvGuard, GlobalStateLock, StubBinDir};
    use pretty_assertions::assert_eq;
    use std::process::Command;

    #[test]
    fn dispatch_unknown_returns_none() {
        assert_eq!(dispatch("unknown", &[]), None);
    }

    #[test]
    fn cleanup_help_exits_success_without_git_runtime() {
        let args = vec!["--help".to_string()];
        assert_eq!(dispatch("cleanup", &args), Some(0));
        assert_eq!(dispatch("delete-merged", &args), Some(0));
    }

    #[test]
    fn parse_args_supports_base_and_squash_flags() {
        let args = vec![
            "--base".to_string(),
            "origin/main".to_string(),
            "--squash".to_string(),
            "--remove-worktrees".to_string(),
            "--unknown".to_string(),
        ];
        let parsed = parse_args(&args).expect("parsed");
        assert_eq!(parsed.base_ref, "origin/main");
        assert!(parsed.squash_mode);
        assert!(parsed.remove_worktrees);
        assert!(!parsed.help);
    }

    #[test]
    fn parse_args_requires_value_for_base_flag() {
        let args = vec!["--base".to_string()];
        let err_code = match parse_args(&args) {
            Ok(_) => panic!("expected usage error"),
            Err(code) => code,
        };
        assert_eq!(err_code, 2);
    }

    #[test]
    fn parse_lines_skips_blank_entries() {
        let output = Command::new("/bin/sh")
            .arg("-c")
            .arg("printf 'main\\n\\nfeature/a\\n'")
            .output()
            .expect("output");
        let lines = parse_lines(&output);
        assert_eq!(lines, vec!["main".to_string(), "feature/a".to_string()]);
    }

    #[test]
    fn summarize_git_error_strips_prefix_and_normalizes_lines() {
        let message =
            "git [\"branch\", \"-d\"] failed: error: cannot delete branch\nhint: checked out";
        let summary = summarize_git_error(message);
        assert_eq!(summary, "error: cannot delete branch hint: checked out");
    }

    #[test]
    fn linked_worktrees_by_branch_parses_porcelain_output() {
        let lock = GlobalStateLock::new();
        let stubs = StubBinDir::new();
        stubs.write_exe(
            "git",
            r#"#!/bin/bash
set -euo pipefail
if [[ "${1:-}" == "worktree" && "${2:-}" == "list" && "${3:-}" == "--porcelain" ]]; then
  printf 'worktree /repo\n'
  printf 'HEAD 1111111111111111111111111111111111111111\n'
  printf 'branch refs/heads/main\n'
  printf '\n'
  printf 'worktree /repo/wt/topic\n'
  printf 'HEAD 2222222222222222222222222222222222222222\n'
  printf 'branch refs/heads/feature/topic\n'
  exit 0
fi
exit 1
"#,
        );

        let _guard = EnvGuard::set(&lock, "PATH", &stubs.path_str());
        let mapping = linked_worktrees_by_branch().expect("parse linked worktrees");
        assert_eq!(
            mapping.get("feature/topic"),
            Some(&"/repo/wt/topic".to_string())
        );
        assert_eq!(mapping.get("main"), Some(&"/repo".to_string()));
    }

    #[test]
    fn resolve_base_local_prefers_remote_then_local_then_none() {
        let lock = GlobalStateLock::new();

        let remote_stubs = StubBinDir::new();
        remote_stubs.write_exe(
            "git",
            r#"#!/bin/bash
set -euo pipefail
if [[ "${1:-}" == "show-ref" && "${2:-}" == "--verify" && "${3:-}" == "--quiet" ]]; then
  if [[ "${4:-}" == "refs/remotes/origin/main" ]]; then
    exit 0
  fi
  exit 1
fi
exit 1
"#,
        );
        let remote_guard = EnvGuard::set(&lock, "PATH", &remote_stubs.path_str());
        assert_eq!(resolve_base_local("origin/main"), Some("main".to_string()));
        drop(remote_guard);

        let local_stubs = StubBinDir::new();
        local_stubs.write_exe(
            "git",
            r#"#!/bin/bash
set -euo pipefail
if [[ "${1:-}" == "show-ref" && "${2:-}" == "--verify" && "${3:-}" == "--quiet" ]]; then
  if [[ "${4:-}" == "refs/heads/main" ]]; then
    exit 0
  fi
  exit 1
fi
exit 1
"#,
        );
        let local_guard = EnvGuard::set(&lock, "PATH", &local_stubs.path_str());
        assert_eq!(resolve_base_local("main"), Some("main".to_string()));
        drop(local_guard);

        let none_stubs = StubBinDir::new();
        none_stubs.write_exe(
            "git",
            r#"#!/bin/bash
set -euo pipefail
exit 1
"#,
        );
        let _none_guard = EnvGuard::set(&lock, "PATH", &none_stubs.path_str());
        assert_eq!(resolve_base_local("feature/topic"), None);
    }
}
