use crate::error::NdkError;
use crate::target::Target;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;

/// The default password used when creating the default `debug.keystore` via
/// [`Ndk::debug_key`]
pub const DEFAULT_DEV_KEYSTORE_PASSWORD: &str = "android";

fn is_tty() -> bool {
    use std::io::IsTerminal;
    std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
}

fn infer_sdk_from_adb() -> Option<std::path::PathBuf> {
    which::which(bin!("adb")).ok().and_then(|adb| {
        adb.parent()
            .and_then(|p| p.parent()) // .../platform-tools/..
            .map(|p| p.to_path_buf())
            .filter(|p| p.join("platform-tools").exists())
    })
}

fn default_sdk_locations() -> Vec<std::path::PathBuf> {
    let mut candidates = Vec::new();
    if let Some(home) = dirs::home_dir() {
        #[cfg(target_os = "macos")]
        candidates.push(home.join("Library/Android/sdk"));
        #[cfg(target_os = "linux")]
        candidates.push(home.join("Android/Sdk"));
        #[cfg(target_os = "windows")]
        {
            if let Some(local) = std::env::var_os("LOCALAPPDATA") {
                candidates.push(std::path::PathBuf::from(local).join("Android").join("Sdk"));
            }
            candidates.push(std::path::PathBuf::from(r"C:\Android\Sdk"));
        }
    }
    candidates
}

fn validate_sdk_root(p: &std::path::Path) -> bool {
    p.join("platforms").exists() && p.join("build-tools").exists()
}

fn parse_pkg_revision(s: &str) -> Option<(u32, u32, u32, bool)> {
    // returns (major, minor, patch, is_beta)
    // Accept 25.2.9519653 or 25.2.9519653-beta1
    let mut parts = s.trim().split('.');
    let major = parts.next()?.parse().ok()?;
    let minor = parts.next()?.parse().ok()?;
    let rest = parts.next()?;
    let (patch_str, beta) = rest
        .split_once('-')
        .map_or((rest, false), |(a, _)| (a, true));
    let patch = patch_str.parse().ok()?;
    Some((major, minor, patch, beta))
}

fn best_ndk_under(sdk: &std::path::Path) -> Option<std::path::PathBuf> {
    let ndk_dir = sdk.join("ndk");
    if !ndk_dir.exists() {
        return None;
    }
    let mut best: Option<(std::path::PathBuf, (u32, u32, u32, bool))> = None;
    for e in std::fs::read_dir(&ndk_dir).ok()?.flatten() {
        let cand = e.path();
        let props = cand.join("source.properties");
        if !props.exists() {
            continue;
        }
        if let Ok(text) = std::fs::read_to_string(&props) {
            let revision_line = text
                .lines()
                .find(|line| line.starts_with("Pkg.Revision"))
                .and_then(|line| line.split_once('='))
                .map(|(_, v)| v.trim());

            if let Some(v) = revision_line {
                if let Some(rev) = parse_pkg_revision(v) {
                    let pick = match &best {
                        None => true,
                        Some((_, old)) => rev > *old,
                    };
                    if pick {
                        best = Some((cand.clone(), rev));
                    }
                }
            }
        }
    }
    best.map(|(p, _)| p)
}

fn validate_ndk_root(p: &std::path::Path) -> bool {
    p.join("source.properties").exists() && p.join("toolchains").join("llvm").exists()
}

fn prompt_path(label: &str) -> Option<std::path::PathBuf> {
    use std::io::{self, Write};
    eprint!("Enter path to {label} (or leave empty to abort): ");
    io::stderr().flush().ok()?;
    let mut line = String::new();
    io::stdin().read_line(&mut line).ok()?;
    let trimmed = line.trim();
    if trimmed.is_empty() {
        return None;
    }
    Some(std::path::PathBuf::from(trimmed))
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Ndk {
    sdk_path: PathBuf,
    user_home: PathBuf,
    ndk_path: PathBuf,
    build_tools_version: String,
    build_tag: u32,
    platforms: Vec<u32>,
}

impl Ndk {
    pub fn from_env() -> Result<Self, NdkError> {
        // env first
        let sdk_path = {
            let sdk_env = std::env::var("ANDROID_SDK_ROOT")
                .ok()
                .or_else(|| std::env::var("ANDROID_HOME").ok());
            let mut sdk = sdk_env.map(std::path::PathBuf::from);

            if sdk.is_none() {
                sdk = infer_sdk_from_adb();
            }
            if sdk.is_none() {
                for cand in default_sdk_locations() {
                    if validate_sdk_root(&cand) {
                        sdk = Some(cand);
                        break;
                    }
                }
            }
            if sdk.is_none() && is_tty() {
                eprintln!(
                    "Android SDK not found via ANDROID_HOME/ANDROID_SDK_ROOT, adb, or default locations."
                );
                if let Some(p) = prompt_path("Android SDK") {
                    if validate_sdk_root(&p) {
                        eprintln!("Using SDK at {}", p.display());
                        unsafe { std::env::set_var("ANDROID_HOME", &p) };
                        sdk = Some(p);
                    } else {
                        eprintln!(
                            "Given path does not look like an Android SDK (missing platforms/build-tools)."
                        );
                    }
                }
            }
            sdk.ok_or(NdkError::SdkNotFound)?
        };

        // Existing deprecation note stays as-is
        if std::env::var_os("ANDROID_SDK_ROOT").is_some() {
            eprintln!(
                "Warning: Environment variable ANDROID_SDK_ROOT is deprecated \
            (https://developer.android.com/studio/command-line/variables#envar). \
            It will be used until it is unset and replaced by ANDROID_HOME."
            );
        }

        let user_home = {
            let user_home = std::env::var("ANDROID_SDK_HOME")
                .map(std::path::PathBuf::from)
                .map(|home| home.join(".android"))
                .ok();
            if user_home.is_some() {
                eprintln!(
                    "Warning: Environment variable ANDROID_SDK_HOME is deprecated \
                (https://developer.android.com/studio/command-line/variables#envar). \
                It will be used until it is unset and replaced by ANDROID_USER_HOME."
                );
            }
            user_home
                .or_else(|| {
                    std::env::var("ANDROID_USER_HOME")
                        .map(std::path::PathBuf::from)
                        .ok()
                })
                .or_else(|| dirs::home_dir().map(|home| home.join(".android")))
                .ok_or_else(|| NdkError::PathNotFound(std::path::PathBuf::from("$HOME")))?
        };

        let mut ndk_path = std::env::var("ANDROID_NDK_ROOT")
            .ok()
            .or_else(|| std::env::var("ANDROID_NDK_PATH").ok())
            .or_else(|| std::env::var("ANDROID_NDK_HOME").ok())
            .or_else(|| std::env::var("NDK_HOME").ok())
            .map(std::path::PathBuf::from);

        // then sdk/ndk/<version> (picks best), then ndk-bundle
        if ndk_path.is_none() {
            if let Some(p) = best_ndk_under(&sdk_path) {
                ndk_path = Some(p);
            } else if sdk_path.join("ndk-bundle").exists() {
                ndk_path = Some(sdk_path.join("ndk-bundle"));
            }
        }

        if ndk_path.is_none() && is_tty() {
            eprintln!(
                "Android NDK not found via env or under {}/ndk / ndk-bundle.",
                sdk_path.display()
            );
            if let Some(p) = prompt_path("Android NDK") {
                if validate_ndk_root(&p) {
                    eprintln!("Using NDK at {}", p.display());
                    unsafe { std::env::set_var("ANDROID_NDK_ROOT", &p) };
                    ndk_path = Some(p);
                } else {
                    eprintln!(
                        "Given path does not look like an Android NDK (missing toolchains/llvm, source.properties)."
                    );
                }
            }
        }

        let ndk_path = ndk_path.ok_or(NdkError::NdkNotFound)?;

        let build_tools_dir = sdk_path.join("build-tools");
        let build_tools_version = if let Ok(pinned) = std::env::var("ANDROID_BUILD_TOOLS_VERSION") {
            if build_tools_dir.join(&pinned).is_dir() {
                pinned
            } else {
                return Err(NdkError::BuildToolsNotFound);
            }
        } else {
            std::fs::read_dir(&build_tools_dir)
                .or(Err(NdkError::PathNotFound(build_tools_dir)))?
                .filter_map(|path| path.ok())
                .filter(|path| path.path().is_dir())
                .filter_map(|path| path.file_name().into_string().ok())
                .filter(|name| name.chars().next().unwrap().is_ascii_digit())
                .max()
                .ok_or(NdkError::BuildToolsNotFound)?
        };

        let build_tag = std::fs::read_to_string(ndk_path.join("source.properties"))
            .map_err(|e| NdkError::IoPathError(ndk_path.join("source.properties"), e))?;

        let build_tag = build_tag
            .lines()
            .find_map(|line| {
                let (key, value) = line.split_once('=')?;
                if key.trim() == "Pkg.Revision" {
                    let mut parts = value.trim().split('.');
                    let _major = parts.next()?;
                    let _minor = parts.next()?;
                    let patch = parts.next()?;
                    let patch = patch.split_once('-').map_or(patch, |(patch, _beta)| patch);
                    patch.parse::<u32>().ok()
                } else {
                    None
                }
            })
            .ok_or(NdkError::InvalidSemver)?;

        let ndk_platforms = std::fs::read_to_string(ndk_path.join("build/core/platforms.mk"))?;
        let ndk_platforms: HashMap<_, _> = ndk_platforms
            .lines()
            .filter_map(|s| s.split_once(" := "))
            .collect();

        let min_platform_level = ndk_platforms
            .get("NDK_MIN_PLATFORM_LEVEL")
            .and_then(|s| s.parse().ok())
            .ok_or(NdkError::InvalidSemver)?;
        let max_platform_level = ndk_platforms
            .get("NDK_MAX_PLATFORM_LEVEL")
            .and_then(|s| s.parse().ok())
            .ok_or(NdkError::InvalidSemver)?;
        let platforms_dir = sdk_path.join("platforms");
        let platforms: Vec<u32> = std::fs::read_dir(&platforms_dir)
            .or(Err(NdkError::PathNotFound(platforms_dir)))?
            .filter_map(|path| path.ok())
            .filter(|path| path.path().is_dir())
            .filter_map(|path| path.file_name().into_string().ok())
            .filter_map(|name| {
                name.strip_prefix("android-")
                    .and_then(|api| api.parse::<u32>().ok())
            })
            .filter(|level| *level >= min_platform_level)
            .collect();

        let excess: Vec<&u32> = platforms
            .iter()
            .filter(|l| **l > max_platform_level)
            .collect();
        if !excess.is_empty() {
            eprintln!(
                "warning: SDK has platforms ({}) newer than NDK max platform level ({})",
                excess
                    .iter()
                    .map(|l| l.to_string())
                    .collect::<Vec<_>>()
                    .join(", "),
                max_platform_level,
            );
        }

        if platforms.is_empty() {
            return Err(NdkError::NoPlatformFound);
        }

        Ok(Self {
            sdk_path,
            user_home,
            ndk_path,
            build_tools_version,
            build_tag,
            platforms,
        })
    }

    pub fn sdk(&self) -> &Path {
        &self.sdk_path
    }

    pub fn ndk(&self) -> &Path {
        &self.ndk_path
    }

    pub fn build_tools_version(&self) -> &str {
        &self.build_tools_version
    }

    pub fn build_tag(&self) -> u32 {
        self.build_tag
    }

    pub fn platforms(&self) -> &[u32] {
        &self.platforms
    }

    pub fn build_tool(&self, tool: &str) -> Result<Command, NdkError> {
        let path = self
            .sdk_path
            .join("build-tools")
            .join(&self.build_tools_version)
            .join(tool);
        if !path.exists() {
            return Err(NdkError::CmdNotFound(tool.to_string()));
        }
        Ok(Command::new(dunce::canonicalize(path)?))
    }

    pub fn platform_tool_path(&self, tool: &str) -> Result<PathBuf, NdkError> {
        let path = self.sdk_path.join("platform-tools").join(tool);
        if !path.exists() {
            return Err(NdkError::CmdNotFound(tool.to_string()));
        }
        Ok(dunce::canonicalize(path)?)
    }

    pub fn adb_path(&self) -> Result<PathBuf, NdkError> {
        self.platform_tool_path(bin!("adb"))
    }

    pub fn platform_tool(&self, tool: &str) -> Result<Command, NdkError> {
        Ok(Command::new(self.platform_tool_path(tool)?))
    }

    pub fn highest_supported_platform(&self) -> u32 {
        self.platforms().iter().max().cloned().unwrap()
    }

    /// Returns platform `36` as currently [required by Google Play], or lower
    /// when the detected SDK does not support it yet.
    ///
    /// [required by Google Play]: https://developer.android.com/distribute/best-practices/develop/target-sdk
    pub fn default_target_platform(&self) -> u32 {
        self.highest_supported_platform().min(36)
    }

    pub fn platform_dir(&self, platform: u32) -> Result<PathBuf, NdkError> {
        let dir = self
            .sdk_path
            .join("platforms")
            .join(format!("android-{platform}"));
        if !dir.exists() {
            return Err(NdkError::PlatformNotFound(platform));
        }
        Ok(dir)
    }

    pub fn android_jar(&self, platform: u32) -> Result<PathBuf, NdkError> {
        let android_jar = self.platform_dir(platform)?.join("android.jar");
        if !android_jar.exists() {
            return Err(NdkError::PathNotFound(android_jar));
        }
        Ok(android_jar)
    }

    fn host_arch() -> Result<&'static str, NdkError> {
        let host_os = std::env::var("HOST").ok();
        let host_contains = |s| host_os.as_ref().map(|h| h.contains(s)).unwrap_or(false);

        Ok(if host_contains("linux") {
            "linux"
        } else if host_contains("macos") {
            "darwin"
        } else if host_contains("windows") {
            "windows"
        } else if host_contains("android") {
            "android"
        } else if cfg!(target_os = "linux") {
            "linux"
        } else if cfg!(target_os = "macos") {
            "darwin"
        } else if cfg!(target_os = "windows") {
            "windows"
        } else if cfg!(target_os = "android") {
            "android"
        } else {
            return match host_os {
                Some(host_os) => Err(NdkError::UnsupportedHost(host_os)),
                _ => Err(NdkError::UnsupportedTarget),
            };
        })
    }

    pub fn toolchain_dir(&self) -> Result<PathBuf, NdkError> {
        let arch = Self::host_arch()?;
        let mut toolchain_dir = self
            .ndk_path
            .join("toolchains")
            .join("llvm")
            .join("prebuilt")
            .join(format!("{arch}-x86_64"));
        if !toolchain_dir.exists() {
            toolchain_dir.set_file_name(arch);
        }
        if !toolchain_dir.exists() {
            return Err(NdkError::PathNotFound(toolchain_dir));
        }
        Ok(toolchain_dir)
    }

    pub fn clang(&self) -> Result<(PathBuf, PathBuf), NdkError> {
        let ext = if cfg!(target_os = "windows") {
            "exe"
        } else {
            ""
        };

        let bin_path = self.toolchain_dir()?.join("bin");

        let clang = bin_path.join("clang").with_extension(ext);
        if !clang.exists() {
            return Err(NdkError::PathNotFound(clang));
        }

        let clang_pp = bin_path.join("clang++").with_extension(ext);
        if !clang_pp.exists() {
            return Err(NdkError::PathNotFound(clang_pp));
        }

        Ok((clang, clang_pp))
    }

    pub fn toolchain_bin(&self, name: &str, target: Target) -> Result<PathBuf, NdkError> {
        let ext = if cfg!(target_os = "windows") {
            ".exe"
        } else {
            ""
        };

        let toolchain_path = self.toolchain_dir()?.join("bin");

        // Since r21 (https://github.com/android/ndk/wiki/Changelog-r21) LLVM binutils are included _for testing_;
        // Since r22 (https://github.com/android/ndk/wiki/Changelog-r22) GNU binutils are deprecated in favour of LLVM's;
        // Since r23 (https://github.com/android/ndk/wiki/Changelog-r23) GNU binutils have been removed.
        // To maintain stability with the current rndk crate release, prefer GNU binutils for
        // as long as it is provided by the NDK instead of trying to use llvm-* from r21 onwards.
        let gnu_bin = format!("{}-{}{}", target.ndk_triple(), name, ext);
        let gnu_path = toolchain_path.join(&gnu_bin);
        if gnu_path.exists() {
            Ok(gnu_path)
        } else {
            let llvm_bin = format!("llvm-{name}{ext}");
            let llvm_path = toolchain_path.join(&llvm_bin);
            if llvm_path.exists() {
                Ok(llvm_path)
            } else {
                Err(NdkError::ToolchainBinaryNotFound {
                    toolchain_path,
                    gnu_bin,
                    llvm_bin,
                })
            }
        }
    }

    pub fn prebuilt_dir(&self) -> Result<PathBuf, NdkError> {
        let arch = Self::host_arch()?;
        let prebuilt_dir = self
            .ndk_path
            .join("prebuilt")
            .join(format!("{arch}-x86_64"));
        if !prebuilt_dir.exists() {
            Err(NdkError::PathNotFound(prebuilt_dir))
        } else {
            Ok(prebuilt_dir)
        }
    }

    pub fn ndk_gdb(
        &self,
        launch_dir: impl AsRef<Path>,
        launch_activity: &str,
        device_serial: Option<&str>,
    ) -> Result<(), NdkError> {
        let abi = self.detect_abi(device_serial)?;
        let jni_dir = launch_dir.as_ref().join("jni");
        std::fs::create_dir_all(&jni_dir)?;
        std::fs::write(
            jni_dir.join("Android.mk"),
            format!("APP_ABI={}\nTARGET_OUT=\n", abi.android_abi()),
        )?;
        let mut ndk_gdb = Command::new(self.prebuilt_dir()?.join("bin").join(cmd!("ndk-gdb")));

        if let Some(device_serial) = &device_serial {
            ndk_gdb.arg("-s").arg(device_serial);
        }

        ndk_gdb
            .arg("--adb")
            .arg(self.adb_path()?)
            .arg("--launch")
            .arg(launch_activity)
            .current_dir(launch_dir)
            .status()?;
        Ok(())
    }

    pub fn android_user_home(&self) -> Result<PathBuf, NdkError> {
        let android_user_home = self.user_home.clone();
        std::fs::create_dir_all(&android_user_home)?;
        Ok(android_user_home)
    }

    pub fn keytool(&self) -> Result<Command, NdkError> {
        self.java_tool("keytool")
    }

    pub fn javac(&self) -> Result<Command, NdkError> {
        self.java_tool("javac")
    }

    pub fn d8(&self) -> Result<Command, NdkError> {
        self.build_tool(bat!("d8"))
    }

    fn java_tool(&self, tool: &str) -> Result<Command, NdkError> {
        let tool_bin = if cfg!(target_os = "windows") {
            format!("{tool}.exe")
        } else {
            tool.to_string()
        };

        if let Ok(path) = which::which(&tool_bin) {
            return Ok(Command::new(path));
        }
        if let Ok(java_home) = std::env::var("JAVA_HOME") {
            let candidate = PathBuf::from(java_home).join("bin").join(&tool_bin);
            if candidate.exists() {
                return Ok(Command::new(candidate));
            }
        }
        Err(NdkError::CmdNotFound(tool.to_string()))
    }

    pub fn debug_key(&self) -> Result<Key, NdkError> {
        let path = self.android_user_home()?.join("debug.keystore");
        let password = DEFAULT_DEV_KEYSTORE_PASSWORD.to_owned();

        if !path.exists() {
            let mut keytool = self.keytool()?;
            keytool
                .arg("-genkey")
                .arg("-v")
                .arg("-keystore")
                .arg(&path)
                .arg("-storepass")
                .arg(&password)
                .arg("-alias")
                .arg("androiddebugkey")
                .arg("-keypass")
                .arg(&password)
                .arg("-dname")
                .arg("CN=Android Debug,O=Android,C=US")
                .arg("-keyalg")
                .arg("RSA")
                .arg("-keysize")
                .arg("2048")
                .arg("-validity")
                .arg("10000");
            if !keytool.status()?.success() {
                return Err(NdkError::CmdFailed(Box::new(keytool)));
            }
        }
        Ok(Key { path, password })
    }

    pub fn sysroot_lib_dir(&self, target: Target) -> Result<PathBuf, NdkError> {
        let sysroot_lib_dir = self
            .toolchain_dir()?
            .join("sysroot")
            .join("usr")
            .join("lib")
            .join(target.ndk_triple());
        if !sysroot_lib_dir.exists() {
            return Err(NdkError::PathNotFound(sysroot_lib_dir));
        }
        Ok(sysroot_lib_dir)
    }

    pub fn sysroot_platform_lib_dir(
        &self,
        target: Target,
        min_sdk_version: u32,
    ) -> Result<PathBuf, NdkError> {
        let sysroot_lib_dir = self.sysroot_lib_dir(target)?;

        // Look for a platform <= min_sdk_version
        let mut tmp_platform = min_sdk_version;
        while tmp_platform > 1 {
            let path = sysroot_lib_dir.join(tmp_platform.to_string());
            if path.exists() {
                return Ok(path);
            }
            tmp_platform += 1;
        }

        // Look for the minimum API level supported by the NDK
        let mut tmp_platform = min_sdk_version;
        while tmp_platform < 100 {
            let path = sysroot_lib_dir.join(tmp_platform.to_string());
            if path.exists() {
                return Ok(path);
            }
            tmp_platform += 1;
        }

        Err(NdkError::PlatformNotFound(min_sdk_version))
    }

    pub fn detect_abi(&self, device_serial: Option<&str>) -> Result<Target, NdkError> {
        let mut adb = self.adb(device_serial)?;

        let stdout = adb
            .arg("shell")
            .arg("getprop")
            .arg("ro.product.cpu.abi")
            .output()?
            .stdout;
        let abi = std::str::from_utf8(&stdout).or(Err(NdkError::UnsupportedTarget))?;
        Target::from_android_abi(abi.trim())
    }

    pub fn adb(&self, device_serial: Option<&str>) -> Result<Command, NdkError> {
        let mut adb = Command::new(self.adb_path()?);

        if let Some(device_serial) = device_serial {
            adb.arg("-s").arg(device_serial);
        }

        Ok(adb)
    }
}

pub struct Key {
    pub path: PathBuf,
    pub password: String,
}

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

    #[test]
    #[ignore]
    fn test_detect() {
        let ndk = Ndk::from_env().unwrap();
        assert_eq!(ndk.build_tools_version(), "29.0.2");
        assert_eq!(ndk.platforms(), &[29, 28]);
    }
}
