use std::env;
use std::fs::{self};
use std::io::{self, Write};
use std::path::{Path, PathBuf};

use anyhow::{Result, anyhow};
use managed_binary::ManagedBinary;
use regex::Regex;
use semver::Version;

pub mod cache;
pub mod cli;
pub mod config;
// pub mod metered;
pub mod managed_binary;
pub mod network;
pub mod test_utils;

pub trait ToK8sStr {
    fn to_k8s_str(&self) -> String;
}

impl ToK8sStr for Version {
    fn to_k8s_str(&self) -> String {
        format!("v{}", self)
    }
}

fn validate_version_format(version: &str) -> Result<()> {
    let re = Regex::new(r"^v\d+\.\d+\.\d+$")?;
    match re.is_match(version) {
        true => Ok(()),
        false => Err(anyhow::anyhow!(
            "Invalid version format from upstream: {}, expected vX.Y.Z pattern",
            version
        )),
    }
}

pub fn normalize_k8s_version_string(version: &str) -> Result<String> {
    let re = Regex::new(r"v(\d+)\.(\d+)\.(\d+)")?;

    if let Some(captures) = re.captures(version) {
        // Construct the normalized version string
        Ok(format!(
            "v{}.{}.{}",
            &captures[1], // X
            &captures[2], // Y
            &captures[3]  // Z
        ))
    } else {
        Err(anyhow!(
            "Version string does not match the expected pattern"
        ))
    }
}

pub fn list_binaries(dir: &Path, binary: &ManagedBinary) -> Result<Vec<PathBuf>> {
    let binaries = fs::read_dir(dir)?
        .filter_map(|entry| {
            let path = entry.ok()?.path();
            let filename = path.file_name()?.to_str()?;
            if filename.starts_with(&format!("{}-v", binary)) {
                Some(path)
            } else {
                None
            }
        })
        .collect();
    Ok(binaries)
}

pub fn extract_version_from_filename(path: &Path) -> Option<Version> {
    let filename = path.file_name()?.to_str()?;
    let version_str = if let Some(dash_v_pos) = filename.find("-v") {
        &filename[dash_v_pos + 2..]
    } else {
        return None;
    };
    Version::parse(version_str).ok()
}

pub fn is_compatible(target: &Version, candidate: &Version) -> bool {
    target.major == candidate.major
        && candidate.minor >= target.minor.saturating_sub(1)
        && candidate.minor <= target.minor.saturating_add(1)
}

pub fn find_most_recent_compatible_version(
    binaries: &[PathBuf],
    target: Option<&Version>,
) -> Result<Option<PathBuf>> {
    _find_most_recent_version(binaries, target, true)
}

pub fn find_most_recent_version(binaries: &[PathBuf]) -> Result<Option<PathBuf>> {
    _find_most_recent_version(binaries, None, false)
}

fn _find_most_recent_version(
    binaries: &[PathBuf],
    target: Option<&Version>,
    require_compatibility: bool,
) -> Result<Option<PathBuf>> {
    let mut best_match: Option<(PathBuf, Version)> = None;

    log::debug!(
        "Existing Versions:\n{}",
        binaries
            .iter()
            .map(|p| p.display().to_string())
            .collect::<Vec<_>>()
            .join("\n")
    );

    for path in binaries {
        if let Some(candidate_version) = extract_version_from_filename(path) {
            let is_valid = if require_compatibility {
                target.is_none_or(|t| is_compatible(t, &candidate_version))
            } else {
                true
            };

            if is_valid {
                let replace = match &best_match {
                    Some((_, existing_version)) => &candidate_version > existing_version,
                    None => true,
                };
                if replace {
                    best_match = Some((path.clone(), candidate_version));
                }
            }
        }
    }

    Ok(best_match.map(|(path, _)| path))
}

pub fn detect_os() -> String {
    match env::consts::OS {
        "macos" => "darwin".to_string(),
        "windows" => "windows".to_string(),
        _ => "linux".to_string(),
    }
}

pub fn detect_cpu_arch() -> String {
    match env::consts::ARCH {
        "x86" => "386".to_string(),
        "x86_64" => "amd64".to_string(),
        "arm" => "arm".to_string(),
        "aarch64" => "arm64".to_string(),
        _ => env::consts::ARCH.to_string(),
    }
}

pub fn get_user_confirmation(prompt: &str) -> bool {
    loop {
        print!("{} [y/N]: ", prompt);
        // Flush stdout to make sure prompt appears before input
        io::stdout().flush().expect("Failed to flush stdout");

        let mut input = String::new();
        if io::stdin().read_line(&mut input).is_err() {
            println!("Error reading input. Please try again.");
            continue;
        }

        let response = input.trim().to_lowercase();

        match response.as_str() {
            "y" | "yes" => return true,
            "n" | "no" | "" => return false,
            _ => println!("Please answer 'yes' or 'no' (or 'y'/'n')."),
        }
    }
}

pub fn get_shell_config_path() -> Option<PathBuf> {
    let shell_path = env::var("SHELL").ok()?;
    let binding = PathBuf::from(&shell_path);
    let shell_name = binding.file_name().and_then(|n| n.to_str())?;

    let home = dirs::home_dir()?;

    let config_file = match shell_name {
        "zsh" => home.join(".zshrc"),
        "bash" => home.join(".bashrc"),
        "fish" => home.join(".config/fish/config.fish"),
        "ash" => home.join(".profile"),
        "sh" => home.join(".profile"),
        _ => return None, // unknown shell
    };

    Some(config_file)
}

#[cfg(test)]
pub mod korrect_lib_tests {
    use super::*;
    use serde_json::Value;
    use tempfile::TempDir;

    use crate::{
        cache::{cache_lookup, parse_version},
        detect_cpu_arch, detect_os,
        test_utils::init_logger,
    };
    #[test]
    fn test_normalize_version() {
        init_logger();
        assert_eq!(normalize_k8s_version_string("v1.2.3").unwrap(), "v1.2.3");
        assert_eq!(
            normalize_k8s_version_string("v7.24.31").unwrap(),
            "v7.24.31"
        );
        assert_eq!(
            normalize_k8s_version_string("somethingv1.2.3-alpha").unwrap(),
            "v1.2.3"
        );
        assert_eq!(
            normalize_k8s_version_string("v1.2.3-alpha").unwrap(),
            "v1.2.3"
        );

        // Cases that should fail to match and return an error
        assert!(normalize_k8s_version_string("version1.2.3").is_err());
        assert!(normalize_k8s_version_string("invalid").is_err());
    }

    #[test]
    fn test_detect_os() {
        init_logger();
        let os = detect_os();
        match env::consts::OS {
            "macos" => assert_eq!(os, "darwin"),
            "windows" => assert_eq!(os, "windows"),
            _ => assert_eq!(os, "linux"),
        }
    }

    #[test]
    fn test_detect_cpu_arch() {
        init_logger();
        let arch = detect_cpu_arch();
        match env::consts::ARCH {
            "x86" => assert_eq!(arch, "386"),
            "x86_64" => assert_eq!(arch, "amd64"),
            "arm" => assert_eq!(arch, "arm"),
            "aarch64" => assert_eq!(arch, "arm64"),
            _ => assert_eq!(arch, env::consts::ARCH),
        }
    }

    #[test]
    fn test_validate_version_format() {
        init_logger();
        let good_input = "v1.2.3";
        let result = validate_version_format(good_input);
        assert!(result.is_ok());

        let good_input = "v3.18.2";
        let result = validate_version_format(good_input);
        assert!(result.is_ok());

        let bad_input = "q1.2.3";
        let result = validate_version_format(bad_input);
        assert!(result.is_err());
        let error = result.unwrap_err();
        assert_eq!(
            error.to_string(),
            format!(
                "Invalid version format from upstream: {}, expected vX.Y.Z pattern",
                bad_input
            )
        );
    }

    #[test]
    fn test_read_cached_version() {
        let path = TempDir::new()
            .expect("failed to create temp config dir")
            .path()
            .to_path_buf();
        let content = r#"{"version": "1.2.3", "ttl": 10}"#;
        fs::write(&path, content).unwrap();

        let cache_data: Value = serde_json::from_str(content).ok().unwrap();
        let version_str = cache_data["version"].as_str().unwrap();
        let version = parse_version(version_str).ok().unwrap();
        let ttl = cache_data["ttl"].as_i64().unwrap();

        let result = cache_lookup(&path);
        assert_eq!(result, Some((version, ttl)));
    }

    #[test]
    fn test_read_cached_version_new_invalid_cache() {
        let path = TempDir::new()
            .expect("failed to create temp config dir")
            .path()
            .to_path_buf();
        let content = r#"{"version": "not a valid version", "ttl": 10}"#;
        fs::write(&path, content).unwrap();

        let result = cache_lookup(&path);
        assert!(result.is_none());
    }
}
