use std::fs;
use std::path::Path;

use crate::cast::cast_version;
use crate::finders::find_project_root;
use crate::git::{maybe_run_pre_commit, run_git_actions};
use crate::schema::{Config, FileKind, OnInvalidVersion};
use crate::tui::{select_changes, ProposedChange};
use crate::version::validate_version;

const DEFAULT_CONTEXT_LINES: usize = 3;

/// Format a path relative to project root, bold, with folder and filename in different colors
fn pretty_path(path: &Path) -> String {
    let rel_path = if let Some(root) = find_project_root() {
        path.strip_prefix(&root).unwrap_or(path)
    } else {
        path
    };

    let parent = rel_path.parent().map(|p| p.to_string_lossy()).unwrap_or_default();
    let filename = rel_path.file_name().map(|f| f.to_string_lossy()).unwrap_or_default();

    // Bold + blue for folder, bold + magenta for filename
    if parent.is_empty() {
        format!("\x1b[1;35m{}\x1b[0m", filename)
    } else {
        format!("\x1b[1;34m{}/\x1b[35m{}\x1b[0m", parent, filename)
    }
}

pub fn bump_version(config: &Config, target: &str, force: bool) -> Result<(), String> {
    let current_version = config
        .current_version
        .as_ref()
        .ok_or("No current_version found in config")?;

    let new_version = if is_version_string(target) {
        target.to_string()
    } else {
        compute_new_version(current_version, target)?
    };
    let context_lines = config.context_lines.unwrap_or(DEFAULT_CONTEXT_LINES);
    let project_root = find_project_root().ok_or("Could not find project root")?;

    let default_kind = config.default_kind;

    // Collect all proposed changes
    let mut proposed_changes: Vec<ProposedChange> = Vec::new();

    for file_config in &config.files {
        let file_path = project_root.join(&file_config.src);
        if !file_path.exists() {
            return Err(format!("File not found: {}", pretty_path(&file_path)));
        }

        let kind = file_config.kind.unwrap_or(default_kind);

        // Get the versions to use for this file (possibly casted)
        let old_file_version = get_file_version(current_version, kind, config.on_invalid_version, &file_config.src)?;
        let new_file_version = get_file_version(&new_version, kind, config.on_invalid_version, &file_config.src)?;

        let file_changes = collect_file_changes(&file_path, &old_file_version, &new_file_version, context_lines)?;
        proposed_changes.extend(file_changes);
    }

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

    // Show TUI to select changes
    let confirmed = select_changes(&mut proposed_changes)
        .map_err(|e| format!("TUI error: {e}"))?;

    if !confirmed {
        println!("Cancelled.");
        return Ok(());
    }

    // Apply selected changes
    let selected: Vec<_> = proposed_changes.iter().filter(|c| c.selected).collect();
    if selected.is_empty() {
        println!("No changes selected.");
        return Ok(());
    }

    println!("Applying {} change(s)...", selected.len());
    for change in &selected {
        apply_change(change)?;
    }

    // Validate git config before running any git operations
    config.git.validate()?;

    // Run pre-commit hooks if configured
    maybe_run_pre_commit(config.git.run_pre_commit)?;

    // Run git actions if configured
    run_git_actions(&config.git, current_version, &new_version, force)?;

    Ok(())
}

fn is_version_string(s: &str) -> bool {
    !matches!(s, "major" | "minor" | "patch" | "alpha" | "beta" | "rc" | "post" | "dev" | "release")
}

fn get_file_version(
    version: &str,
    kind: FileKind,
    on_invalid: OnInvalidVersion,
    src: &Path,
) -> Result<String, String> {
    // First, check if the version is already valid for this kind
    if validate_version(version, kind).is_ok() {
        return Ok(version.to_string());
    }

    // Version is invalid for this kind
    match on_invalid {
        OnInvalidVersion::Error => {
            let err = validate_version(version, kind).unwrap_err();
            Err(format!(
                "Invalid version '{}' for file '{}' (kind: {:?}): {}",
                version,
                src.display(),
                kind,
                err
            ))
        }
        OnInvalidVersion::Cast => {
            let casted = cast_version(version, kind).map_err(|e| {
                format!(
                    "Cannot cast version '{}' for file '{}' (kind: {:?}): {}",
                    version,
                    src.display(),
                    kind,
                    e
                )
            })?;

            // Validate the casted version
            validate_version(&casted, kind).map_err(|e| {
                format!(
                    "Casted version '{}' is still invalid for file '{}' (kind: {:?}): {}",
                    casted,
                    src.display(),
                    kind,
                    e
                )
            })?;

            Ok(casted)
        }
    }
}

fn compute_new_version(current: &str, component: &str) -> Result<String, String> {
    let parsed = parse_version(current)?;

    match component {
        "major" => Ok(format!("{}.0.0", parsed.major + 1)),
        "minor" => Ok(format!("{}.{}.0", parsed.major, parsed.minor + 1)),
        "patch" => {
            // If we have a prerelease, just drop it (1.0.0a1 -> 1.0.0)
            if parsed.prerelease.is_some() || parsed.post.is_some() || parsed.dev.is_some() {
                Ok(format!("{}.{}.{}", parsed.major, parsed.minor, parsed.patch))
            } else {
                Ok(format!("{}.{}.{}", parsed.major, parsed.minor, parsed.patch + 1))
            }
        }
        "release" => {
            // Drop all prerelease/post/dev suffixes
            Ok(format!("{}.{}.{}", parsed.major, parsed.minor, parsed.patch))
        }
        "alpha" => {
            let num = match &parsed.prerelease {
                Some((kind, n)) if kind == "alpha" => n + 1,
                _ => 1,
            };
            Ok(format!("{}.{}.{}a{}", parsed.major, parsed.minor, parsed.patch, num))
        }
        "beta" => {
            let num = match &parsed.prerelease {
                Some((kind, n)) if kind == "beta" => n + 1,
                _ => 1,
            };
            Ok(format!("{}.{}.{}b{}", parsed.major, parsed.minor, parsed.patch, num))
        }
        "rc" => {
            let num = match &parsed.prerelease {
                Some((kind, n)) if kind == "rc" => n + 1,
                _ => 1,
            };
            Ok(format!("{}.{}.{}rc{}", parsed.major, parsed.minor, parsed.patch, num))
        }
        "post" => {
            let num = parsed.post.map(|n| n + 1).unwrap_or(1);
            let base = format!("{}.{}.{}", parsed.major, parsed.minor, parsed.patch);
            let pre = match &parsed.prerelease {
                Some((kind, n)) => format!("{}{}", prerelease_prefix(kind), n),
                None => String::new(),
            };
            Ok(format!("{}{}.post{}", base, pre, num))
        }
        "dev" => {
            let num = parsed.dev.map(|n| n + 1).unwrap_or(1);
            let base = format!("{}.{}.{}", parsed.major, parsed.minor, parsed.patch);
            let pre = match &parsed.prerelease {
                Some((kind, n)) => format!("{}{}", prerelease_prefix(kind), n),
                None => String::new(),
            };
            let post = parsed.post.map(|n| format!(".post{}", n)).unwrap_or_default();
            Ok(format!("{}{}{}.dev{}", base, pre, post, num))
        }
        _ => Err(format!(
            "Invalid component: {component}. Use major, minor, patch, release, alpha, beta, rc, post, or dev"
        )),
    }
}

fn prerelease_prefix(kind: &str) -> &'static str {
    match kind {
        "alpha" => "a",
        "beta" => "b",
        "rc" => "rc",
        _ => "",
    }
}

#[derive(Debug, Default)]
struct ParsedVersion {
    major: u32,
    minor: u32,
    patch: u32,
    prerelease: Option<(String, u32)>, // (kind, number) e.g., ("alpha", 1)
    post: Option<u32>,
    dev: Option<u32>,
}

fn parse_version(version: &str) -> Result<ParsedVersion, String> {
    let version = version.to_lowercase();

    // Remove epoch if present
    let version = if let Some(pos) = version.find('!') {
        &version[pos + 1..]
    } else {
        version.as_str()
    };

    // Remove local version if present
    let version = if let Some(pos) = version.find('+') {
        &version[..pos]
    } else {
        version
    };

    let mut parsed = ParsedVersion::default();

    // Find dev suffix
    let (version, dev) = if let Some(pos) = version.find(".dev") {
        let dev_part = &version[pos + 4..];
        let dev_num: u32 = dev_part.parse().unwrap_or(0);
        (&version[..pos], Some(dev_num))
    } else if let Some(pos) = version.find("dev") {
        let dev_part = &version[pos + 3..];
        let dev_num: u32 = dev_part.parse().unwrap_or(0);
        (&version[..pos], Some(dev_num))
    } else {
        (version, None)
    };
    parsed.dev = dev;

    // Find post suffix
    let (version, post) = if let Some(pos) = version.find(".post") {
        let post_part = &version[pos + 5..];
        let post_num: u32 = post_part.parse().unwrap_or(0);
        (&version[..pos], Some(post_num))
    } else if let Some(pos) = version.find("post") {
        let post_part = &version[pos + 4..];
        let post_num: u32 = post_part.parse().unwrap_or(0);
        (&version[..pos], Some(post_num))
    } else {
        (version, None)
    };
    parsed.post = post;

    // Find prerelease suffix (alpha, beta, rc, a, b, c)
    let prerelease_markers = [
        ("alpha", "alpha"),
        ("beta", "beta"),
        ("preview", "rc"),
        ("rc", "rc"),
        ("a", "alpha"),
        ("b", "beta"),
        ("c", "rc"),
    ];

    let mut release = version;
    for (marker, kind) in prerelease_markers {
        if let Some(pos) = version.find(marker) {
            let before = &version[..pos];
            // Make sure it's at a valid position (after a digit or dot)
            if before.is_empty() || (!before.ends_with('.') && !before.chars().last().unwrap().is_ascii_digit()) {
                continue;
            }
            let after = &version[pos + marker.len()..];
            let num: u32 = after
                .chars()
                .take_while(|c| c.is_ascii_digit())
                .collect::<String>()
                .parse()
                .unwrap_or(0);
            parsed.prerelease = Some((kind.to_string(), num));
            release = before;
            break;
        }
    }

    // Also handle JS-style prerelease (1.0.0-alpha.1)
    let release = if let Some(pos) = release.find('-') {
        let pre_part = &release[pos + 1..];
        if pre_part.starts_with("alpha") {
            let num_part = pre_part.strip_prefix("alpha").unwrap_or("").trim_start_matches('.');
            let num: u32 = num_part.parse().unwrap_or(0);
            parsed.prerelease = Some(("alpha".to_string(), num));
        } else if pre_part.starts_with("beta") {
            let num_part = pre_part.strip_prefix("beta").unwrap_or("").trim_start_matches('.');
            let num: u32 = num_part.parse().unwrap_or(0);
            parsed.prerelease = Some(("beta".to_string(), num));
        } else if pre_part.starts_with("rc") {
            let num_part = pre_part.strip_prefix("rc").unwrap_or("").trim_start_matches('.');
            let num: u32 = num_part.parse().unwrap_or(0);
            parsed.prerelease = Some(("rc".to_string(), num));
        }
        &release[..pos]
    } else {
        release
    };

    // Parse major.minor.patch
    let parts: Vec<&str> = release.split('.').collect();
    if parts.is_empty() {
        return Err(format!("Invalid version format: {version}"));
    }

    parsed.major = parts[0]
        .parse()
        .map_err(|_| format!("Invalid major version: {}", parts[0]))?;
    parsed.minor = parts.get(1).unwrap_or(&"0")
        .parse()
        .map_err(|_| format!("Invalid minor version: {}", parts.get(1).unwrap_or(&"0")))?;
    parsed.patch = parts.get(2).unwrap_or(&"0")
        .parse()
        .map_err(|_| format!("Invalid patch version: {}", parts.get(2).unwrap_or(&"0")))?;

    Ok(parsed)
}

fn collect_file_changes(
    path: &Path,
    old_version: &str,
    new_version: &str,
    context_lines: usize,
) -> Result<Vec<ProposedChange>, String> {
    let content = fs::read_to_string(path).map_err(|e| format!("Failed to read {}: {e}", path.display()))?;
    let lines: Vec<&str> = content.lines().collect();

    let occurrences: Vec<usize> = lines
        .iter()
        .enumerate()
        .filter(|(_, line)| line.contains(old_version))
        .map(|(i, _)| i)
        .collect();

    if occurrences.is_empty() {
        return Err(format!(
            "Version '{}' not found in {}",
            old_version,
            pretty_path(path)
        ));
    }

    let mut changes = Vec::new();

    for &line_idx in &occurrences {
        let start = line_idx.saturating_sub(context_lines);
        let end = (line_idx + context_lines + 1).min(lines.len());

        let context_before: Vec<String> = lines[start..line_idx]
            .iter()
            .map(|s| s.to_string())
            .collect();
        let context_after: Vec<String> = lines[(line_idx + 1)..end]
            .iter()
            .map(|s| s.to_string())
            .collect();

        let old_line = lines[line_idx].to_string();
        let new_line = old_line.replace(old_version, new_version);

        changes.push(ProposedChange {
            path: path.to_path_buf(),
            line_idx,
            old_line,
            new_line,
            context_before,
            context_after,
            selected: true,
        });
    }

    Ok(changes)
}

fn apply_change(change: &ProposedChange) -> Result<(), String> {
    let original = fs::read_to_string(&change.path)
        .map_err(|e| format!("Failed to read {}: {e}", pretty_path(&change.path)))?;
    let lines: Vec<&str> = original.lines().collect();

    let new_content: Vec<String> = lines
        .iter()
        .enumerate()
        .map(|(i, line)| {
            if i == change.line_idx {
                change.new_line.clone()
            } else {
                (*line).to_string()
            }
        })
        .collect();

    let new_content = new_content.join("\n");

    // Preserve trailing newline if original had one
    let new_content = if original.ends_with('\n') {
        new_content + "\n"
    } else {
        new_content
    };

    fs::write(&change.path, &new_content)
        .map_err(|e| format!("Failed to write {}: {e}", pretty_path(&change.path)))?;

    println!("  Updated {}:{}", pretty_path(&change.path), change.line_idx + 1);
    Ok(())
}

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

    #[test]
    fn test_bump_major() {
        assert_eq!(compute_new_version("1.2.3", "major").unwrap(), "2.0.0");
        assert_eq!(compute_new_version("0.1.0", "major").unwrap(), "1.0.0");
        assert_eq!(compute_new_version("1.2.3a1", "major").unwrap(), "2.0.0");
    }

    #[test]
    fn test_bump_minor() {
        assert_eq!(compute_new_version("1.2.3", "minor").unwrap(), "1.3.0");
        assert_eq!(compute_new_version("0.1.0", "minor").unwrap(), "0.2.0");
        assert_eq!(compute_new_version("1.2.3a1", "minor").unwrap(), "1.3.0");
    }

    #[test]
    fn test_bump_patch() {
        assert_eq!(compute_new_version("1.2.3", "patch").unwrap(), "1.2.4");
        assert_eq!(compute_new_version("0.1.0", "patch").unwrap(), "0.1.1");
        // With prerelease, patch just drops the prerelease
        assert_eq!(compute_new_version("1.2.3a1", "patch").unwrap(), "1.2.3");
        assert_eq!(compute_new_version("1.2.3.post1", "patch").unwrap(), "1.2.3");
    }

    #[test]
    fn test_bump_release() {
        assert_eq!(compute_new_version("1.2.3a1", "release").unwrap(), "1.2.3");
        assert_eq!(compute_new_version("1.2.3b2", "release").unwrap(), "1.2.3");
        assert_eq!(compute_new_version("1.2.3rc1", "release").unwrap(), "1.2.3");
        assert_eq!(compute_new_version("1.2.3.post1", "release").unwrap(), "1.2.3");
        assert_eq!(compute_new_version("1.2.3.dev1", "release").unwrap(), "1.2.3");
        assert_eq!(compute_new_version("1.2.3", "release").unwrap(), "1.2.3");
    }

    #[test]
    fn test_bump_alpha() {
        assert_eq!(compute_new_version("1.2.3", "alpha").unwrap(), "1.2.3a1");
        assert_eq!(compute_new_version("1.2.3a1", "alpha").unwrap(), "1.2.3a2");
        assert_eq!(compute_new_version("1.2.3a5", "alpha").unwrap(), "1.2.3a6");
        // Switching from beta/rc to alpha resets to 1
        assert_eq!(compute_new_version("1.2.3b1", "alpha").unwrap(), "1.2.3a1");
    }

    #[test]
    fn test_bump_beta() {
        assert_eq!(compute_new_version("1.2.3", "beta").unwrap(), "1.2.3b1");
        assert_eq!(compute_new_version("1.2.3b1", "beta").unwrap(), "1.2.3b2");
        assert_eq!(compute_new_version("1.2.3a1", "beta").unwrap(), "1.2.3b1");
    }

    #[test]
    fn test_bump_rc() {
        assert_eq!(compute_new_version("1.2.3", "rc").unwrap(), "1.2.3rc1");
        assert_eq!(compute_new_version("1.2.3rc1", "rc").unwrap(), "1.2.3rc2");
        assert_eq!(compute_new_version("1.2.3b1", "rc").unwrap(), "1.2.3rc1");
    }

    #[test]
    fn test_bump_post() {
        assert_eq!(compute_new_version("1.2.3", "post").unwrap(), "1.2.3.post1");
        assert_eq!(compute_new_version("1.2.3.post1", "post").unwrap(), "1.2.3.post2");
        assert_eq!(compute_new_version("1.2.3a1", "post").unwrap(), "1.2.3a1.post1");
    }

    #[test]
    fn test_bump_dev() {
        assert_eq!(compute_new_version("1.2.3", "dev").unwrap(), "1.2.3.dev1");
        assert_eq!(compute_new_version("1.2.3.dev1", "dev").unwrap(), "1.2.3.dev2");
        assert_eq!(compute_new_version("1.2.3a1", "dev").unwrap(), "1.2.3a1.dev1");
        assert_eq!(compute_new_version("1.2.3.post1", "dev").unwrap(), "1.2.3.post1.dev1");
    }

    #[test]
    fn test_bump_js_style_prerelease() {
        // JS style: 1.0.0-alpha.1
        assert_eq!(compute_new_version("1.2.3-alpha.1", "alpha").unwrap(), "1.2.3a2");
        assert_eq!(compute_new_version("1.2.3-beta.1", "beta").unwrap(), "1.2.3b2");
        assert_eq!(compute_new_version("1.2.3-rc.1", "rc").unwrap(), "1.2.3rc2");
    }

    #[test]
    fn test_parse_version() {
        let p = parse_version("1.2.3").unwrap();
        assert_eq!((p.major, p.minor, p.patch), (1, 2, 3));
        assert!(p.prerelease.is_none());

        let p = parse_version("1.2.3a1").unwrap();
        assert_eq!((p.major, p.minor, p.patch), (1, 2, 3));
        assert_eq!(p.prerelease, Some(("alpha".to_string(), 1)));

        let p = parse_version("1.2.3.post1").unwrap();
        assert_eq!(p.post, Some(1));

        let p = parse_version("1.2.3.dev1").unwrap();
        assert_eq!(p.dev, Some(1));
    }
}
