use std::fmt::Write;

use crate::cli::AliasCommands;
use crate::repository::Repository;
use crate::suggest;

pub fn handle_alias(cmd: AliasCommands) -> Result<(), Box<dyn std::error::Error>> {
    match cmd {
        AliasCommands::Add { name, command } => handle_add(&name, &command),
        AliasCommands::Remove { name } => handle_remove(&name),
        AliasCommands::List { json } => handle_list(json),
        AliasCommands::Apply { stdout } => handle_apply(stdout),
        AliasCommands::AddSuggested {
            min_count,
            min_length,
            days,
            top,
        } => handle_add_suggested(min_count, min_length, days, top),
        AliasCommands::Suggest {
            min_count,
            min_length,
            days,
            top,
            text,
        } => {
            if text {
                suggest::handle_suggest_aliases_text(min_count, min_length, days, top)
            } else {
                suggest::handle_suggest_aliases_tui(min_count, min_length, days, top)
            }
        }
    }
}

fn handle_add(name: &str, command: &str) -> Result<(), Box<dyn std::error::Error>> {
    let repo = Repository::init()?;
    add_alias_to(&repo, name, command)?;
    // Regenerate the managed alias file so the new alias is live in the next
    // shell without a separate `suv alias apply` step.
    apply_aliases_from(&repo, false)
}

fn add_alias_to(
    repo: &Repository,
    name: &str,
    command: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    // Validate alias name: alphanumeric, hyphens, underscores only
    if name.is_empty()
        || !name
            .chars()
            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
    {
        return Err(format!("Invalid alias name: '{name}'. Use only alphanumeric characters, hyphens, and underscores.").into());
    }

    repo.add_alias(name, command)?;
    println!("✓ Alias '{name}' → '{command}'");
    Ok(())
}

fn handle_remove(name: &str) -> Result<(), Box<dyn std::error::Error>> {
    let repo = Repository::init()?;
    remove_alias_from(&repo, name)
}

fn remove_alias_from(repo: &Repository, name: &str) -> Result<(), Box<dyn std::error::Error>> {
    if repo.remove_alias(name)? {
        println!("✓ Removed alias '{name}'");
    } else {
        println!("No alias named '{name}' found.");
    }
    Ok(())
}

fn handle_list(json: bool) -> Result<(), Box<dyn std::error::Error>> {
    let repo = Repository::init()?;
    list_aliases_from(&repo, json)
}

fn list_aliases_from(repo: &Repository, json: bool) -> Result<(), Box<dyn std::error::Error>> {
    let aliases = repo.list_aliases()?;

    if json {
        println!("{}", serde_json::to_string_pretty(&aliases)?);
        return Ok(());
    }

    if aliases.is_empty() {
        println!("No managed aliases. Use 'suv alias add <name> <command>' to add one.");
        return Ok(());
    }

    let max_name = aliases.iter().map(|a| a.name.len()).max().unwrap_or(4);
    println!();
    let color = crate::util::color_enabled();
    for a in &aliases {
        if color {
            println!(
                "  \x1b[36m{:<width$}\x1b[0m  →  {}",
                a.name,
                a.command,
                width = max_name
            );
        } else {
            println!("  {:<width$}  →  {}", a.name, a.command, width = max_name);
        }
    }
    println!();
    println!(
        "  {} alias{}",
        aliases.len(),
        if aliases.len() == 1 { "" } else { "es" }
    );
    println!();
    Ok(())
}

fn handle_apply(to_stdout: bool) -> Result<(), Box<dyn std::error::Error>> {
    let repo = Repository::init()?;
    apply_aliases_from(&repo, to_stdout)
}

#[cfg(test)]
fn build_apply_output(repo: &Repository) -> Result<Option<String>, Box<dyn std::error::Error>> {
    let aliases = repo.list_aliases()?;

    if aliases.is_empty() {
        return Ok(None);
    }

    let mut lines = String::new();
    lines.push_str("# Suvadu managed aliases\n");
    lines.push_str("# Auto-generated by 'suv alias apply' — do not edit manually\n\n");
    for a in &aliases {
        let _ = writeln!(
            lines,
            "alias {}='{}'",
            a.name,
            suggest::shell_quote(&a.command)
        );
    }

    Ok(Some(lines))
}

fn apply_aliases_from(
    repo: &Repository,
    to_stdout: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let aliases = repo.list_aliases()?;

    if aliases.is_empty() {
        println!("No managed aliases to apply.");
        return Ok(());
    }

    let mut lines = String::new();
    lines.push_str("# Suvadu managed aliases\n");
    lines.push_str("# Auto-generated by 'suv alias apply' — do not edit manually\n\n");
    for a in &aliases {
        let _ = writeln!(
            lines,
            "alias {}='{}'",
            a.name,
            suggest::shell_quote(&a.command)
        );
    }

    if to_stdout {
        print!("{lines}");
    } else {
        let dirs = crate::util::project_dirs().ok_or("Could not determine data directory")?;
        let data_dir = dirs.data_dir();
        if !data_dir.exists() {
            std::fs::create_dir_all(data_dir)?;
        }
        let path = data_dir.join("aliases.sh");
        crate::util::atomic_write_with_mode(&path, &lines, 0o600)?;

        println!("✓ Wrote {} aliases to {}", aliases.len(), path.display());
        println!(
            "  Source it from your shell config:\n  source \"{}\"",
            path.display()
        );
    }

    Ok(())
}

fn handle_add_suggested(
    min_count: usize,
    min_length: usize,
    days: Option<usize>,
    top: usize,
) -> Result<(), Box<dyn std::error::Error>> {
    let selected = suggest::run_suggest_and_select(min_count, min_length, days, top, true)?;

    match selected {
        Some(suggestions) if !suggestions.is_empty() => {
            let repo = Repository::init()?;
            let mut added = 0;
            for s in &suggestions {
                repo.add_alias(&s.name, &s.command)?;
                added += 1;
            }
            println!(
                "✓ Added {added} alias{}",
                if added == 1 { "" } else { "es" }
            );
            // Regenerate the managed alias file automatically.
            apply_aliases_from(&repo, false)?;
        }
        _ => {
            println!("No aliases added.");
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_utils::test_repo;

    #[test]
    fn test_alias_add_and_list() {
        let (_dir, repo) = test_repo();
        repo.add_alias("gst", "git status").unwrap();
        repo.add_alias("gco", "git checkout").unwrap();
        let aliases = repo.list_aliases().unwrap();
        assert_eq!(aliases.len(), 2);
        assert_eq!(aliases[0].name, "gco"); // ordered by name
        assert_eq!(aliases[1].name, "gst");
    }

    #[test]
    fn test_alias_upsert() {
        let (_dir, repo) = test_repo();
        repo.add_alias("gst", "git status").unwrap();
        repo.add_alias("gst", "git status --short").unwrap();
        let aliases = repo.list_aliases().unwrap();
        assert_eq!(aliases.len(), 1);
        assert_eq!(aliases[0].command, "git status --short");
    }

    #[test]
    fn test_alias_remove() {
        let (_dir, repo) = test_repo();
        repo.add_alias("gst", "git status").unwrap();
        assert!(repo.remove_alias("gst").unwrap());
        assert!(!repo.remove_alias("gst").unwrap()); // already gone
        assert!(repo.list_aliases().unwrap().is_empty());
    }

    #[test]
    fn test_alias_name_validation() {
        // Valid names
        for name in &["gst", "my-alias", "my_alias", "abc123"] {
            assert!(
                !name.is_empty()
                    && name
                        .chars()
                        .all(|c| c.is_alphanumeric() || c == '-' || c == '_'),
                "{name} should be valid"
            );
        }
        // Invalid names
        for name in &["", "has space", "has!bang", "a/b"] {
            assert!(
                name.is_empty()
                    || !name
                        .chars()
                        .all(|c| c.is_alphanumeric() || c == '-' || c == '_'),
                "{name} should be invalid"
            );
        }
    }

    #[test]
    fn test_alias_add_and_list_roundtrip() {
        let (_dir, repo) = test_repo();
        add_alias_to(&repo, "gst", "git status").unwrap();
        add_alias_to(&repo, "gco", "git checkout").unwrap();
        add_alias_to(&repo, "dps", "docker ps").unwrap();
        let aliases = repo.list_aliases().unwrap();
        assert_eq!(aliases.len(), 3);
        assert_eq!(aliases[0].name, "dps");
        assert_eq!(aliases[0].command, "docker ps");
        assert_eq!(aliases[1].name, "gco");
        assert_eq!(aliases[2].name, "gst");
    }

    #[test]
    fn test_alias_add_invalid_name() {
        let (_dir, repo) = test_repo();
        let err = add_alias_to(&repo, "", "git status").unwrap_err();
        assert!(err.to_string().contains("Invalid alias name"));

        let err = add_alias_to(&repo, "has space", "git status").unwrap_err();
        assert!(err.to_string().contains("Invalid alias name"));

        let err = add_alias_to(&repo, "a/b", "git status").unwrap_err();
        assert!(err.to_string().contains("Invalid alias name"));

        // Ensure nothing was stored
        assert!(repo.list_aliases().unwrap().is_empty());
    }

    #[test]
    fn test_alias_remove_nonexistent() {
        let (_dir, repo) = test_repo();
        // remove_alias_from succeeds even when nothing exists (prints message)
        remove_alias_from(&repo, "nope").unwrap();
        // The underlying repo call returns false
        assert!(!repo.remove_alias("nope").unwrap());
    }

    #[test]
    fn test_alias_upsert_updates_command() {
        let (_dir, repo) = test_repo();
        add_alias_to(&repo, "gst", "git status").unwrap();
        add_alias_to(&repo, "gst", "git status --short --branch").unwrap();
        let aliases = repo.list_aliases().unwrap();
        assert_eq!(aliases.len(), 1);
        assert_eq!(aliases[0].command, "git status --short --branch");
    }

    #[test]
    fn test_alias_apply_stdout_format() {
        let (_dir, repo) = test_repo();
        repo.add_alias("gst", "git status").unwrap();
        repo.add_alias("dcu", "docker compose up").unwrap();

        let output = build_apply_output(&repo).unwrap().unwrap();
        assert!(output.starts_with("# Suvadu managed aliases\n"));
        assert!(output.contains("alias dcu='docker compose up'\n"));
        assert!(output.contains("alias gst='git status'\n"));
    }

    #[test]
    fn test_alias_apply_empty() {
        let (_dir, repo) = test_repo();
        let output = build_apply_output(&repo).unwrap();
        assert!(output.is_none());
    }

    #[test]
    fn test_alias_list_empty() {
        let (_dir, repo) = test_repo();
        let aliases = repo.list_aliases().unwrap();
        assert!(aliases.is_empty());
    }

    #[test]
    fn test_alias_list_with_entries() {
        let (_dir, repo) = test_repo();
        repo.add_alias("a", "alpha").unwrap();
        repo.add_alias("b", "bravo").unwrap();
        repo.add_alias("c", "charlie").unwrap();
        let aliases = repo.list_aliases().unwrap();
        assert_eq!(aliases.len(), 3);
        // Verify alphabetical ordering by name
        let names: Vec<&str> = aliases.iter().map(|a| a.name.as_str()).collect();
        assert_eq!(names, vec!["a", "b", "c"]);
    }
}
