use anyhow::{Context, Result, anyhow};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use crate::download::{download_with_progress, get_python_cache_dir};
use crate::platform::{Arch, Os, Platform};

/// Platform string for `uv pip install --python-platform`.
/// Uses Rust-style target triples as accepted by uv 0.10+.
fn platform_to_pip_platform(platform: &Platform) -> &'static str {
    match (&platform.os, &platform.arch) {
        (Os::Darwin, Arch::Aarch64) => "aarch64-apple-darwin",
        (Os::Darwin, Arch::X86_64) => "x86_64-apple-darwin",
        (Os::Linux, Arch::X86_64) => "x86_64-manylinux_2_28",
        (Os::Linux, Arch::Aarch64) => "aarch64-manylinux_2_28",
        (Os::Windows, Arch::X86_64) => "x86_64-pc-windows-msvc",
        (Os::Windows, Arch::Aarch64) => "aarch64-pc-windows-msvc",
    }
}

/// Return the persistent cache directory for a Python standalone distribution.
/// Layout: `~/.cache/ux/python/{platform}/{version}/`
pub fn python_cache_dir(platform: &Platform, python_version: &str) -> Result<PathBuf> {
    Ok(get_python_cache_dir()?
        .join(platform.to_target_string())
        .join(python_version))
}

/// Ensure the target-platform Python is present in `python_dir`.
/// Downloads from python-build-standalone (via uv's download-metadata.json) when absent.
pub async fn ensure_python_for_platform(
    python_dir: &Path,
    python_version: &str,
    target_platform: &Platform,
) -> Result<()> {
    if python_dir.exists() {
        println!(
            "Using cached Python {} for {}",
            python_version,
            target_platform.to_target_string()
        );
        return Ok(());
    }

    let url = find_python_download_url(python_version, target_platform).await?;

    println!(
        "Downloading Python {} for {} ...",
        python_version,
        target_platform.to_target_string()
    );

    let bytes = download_with_progress(&url).await?;

    println!(
        "Extracting Python installation ({:.1} MB) ...",
        bytes.len() as f64 / 1024.0 / 1024.0
    );
    fs::create_dir_all(python_dir)?;
    if url.ends_with(".tar.gz") {
        extract_python_tar_gz(&bytes, python_dir)
    } else {
        extract_python_tarball(&bytes, python_dir)
    }
    .with_context(|| "Failed to extract Python archive")?;

    Ok(())
}

/// Metadata entry from uv's download-metadata.json.
#[derive(Deserialize)]
struct PythonDownloadEntry {
    url: String,
    major: u32,
    minor: u32,
    patch: u32,
}

/// Look up the download URL for the requested Python version and target platform
/// from uv's canonical download-metadata.json.
async fn find_python_download_url(
    python_version: &str,
    target_platform: &Platform,
) -> Result<String> {
    let metadata_url = "https://raw.githubusercontent.com/astral-sh/uv/main/crates/uv-python/download-metadata.json";

    println!("Fetching Python download metadata ...");
    let client = reqwest::Client::new();
    let response = client
        .get(metadata_url)
        .header("User-Agent", "ux-bundler")
        .send()
        .await
        .with_context(|| "Failed to fetch Python download metadata")?;

    if !response.status().is_success() {
        return Err(anyhow!(
            "Failed to fetch Python metadata: HTTP {}",
            response.status()
        ));
    }

    let metadata: HashMap<String, PythonDownloadEntry> = response
        .json()
        .await
        .with_context(|| "Failed to parse Python download metadata")?;

    // Detect free-threaded request: "3.13t", "3.14t", etc.
    let free_threaded = python_version.ends_with('t');
    let base_version = python_version.trim_end_matches('t');

    // Parse the base version (e.g. "3.12", "3.12.9", "3.13").
    let parts: Vec<&str> = base_version.split('.').collect();
    let req_major: u32 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(3);
    let req_minor: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
    let req_patch: Option<u32> = parts.get(2).and_then(|s| s.parse().ok());

    // Key suffix in the metadata: "-{arch}-{os}-{libc}"
    let triple = target_platform.python_build_standalone_triple();
    let key_suffix = format!("-{}", triple);

    // Collect all matching entries, then pick the latest patch.
    //
    // Non-free-threaded: version segment must be digits and dots only (e.g. "3.13.12").
    //   Rejects "+freethreaded", "+debug", "rc2", "3.13t" shorthands.
    // Free-threaded (python_version ends with 't'): version segment must end with
    //   "+freethreaded" (e.g. "3.13.12+freethreaded").
    let prefix = "cpython-";
    let mut candidates: Vec<&PythonDownloadEntry> = metadata
        .iter()
        .filter(|(key, entry)| {
            if !key.starts_with(prefix) || !key.ends_with(&key_suffix) {
                return false;
            }
            let version_segment = &key[prefix.len()..key.len() - key_suffix.len()];
            let base_seg = if free_threaded {
                match version_segment.strip_suffix("+freethreaded") {
                    Some(s) => s,
                    None => return false,
                }
            } else {
                if !version_segment.chars().all(|c| c.is_ascii_digit() || c == '.') {
                    return false;
                }
                version_segment
            };
            if !base_seg.chars().all(|c| c.is_ascii_digit() || c == '.') {
                return false;
            }
            entry.major == req_major
                && entry.minor == req_minor
                && req_patch.is_none_or(|p| entry.patch == p)
        })
        .map(|(_, v)| v)
        .collect();

    if candidates.is_empty() {
        return Err(anyhow!(
            "No Python {} download found for platform {} (triple: {})",
            python_version,
            target_platform.to_target_string(),
            triple,
        ));
    }

    candidates.sort_by_key(|e| e.patch);
    Ok(candidates.last().unwrap().url.clone())
}

/// Extract a python-build-standalone `.tar.gz` archive (install_only_stripped variant).
///
/// Layout: `python/bin/python3`, `python/lib/...`  (no `install/` subdirectory)
fn extract_python_tar_gz(bytes: &[u8], python_dir: &Path) -> Result<()> {
    use flate2::read::GzDecoder;
    let cursor = std::io::Cursor::new(bytes);
    let decoder = GzDecoder::new(cursor);
    let mut archive = tar::Archive::new(decoder);
    extract_python_archive_entries(&mut archive, python_dir, Path::new("python"))
}

/// Extract a python-build-standalone `.tar.zst` archive.
///
/// python-build-standalone layout inside the archive:
/// ```
/// python/
///   install/       ← the usable Python installation
///     bin/python3
///     lib/
///     ...
///   PYTHON.json
/// ```
/// We strip the `python/install/` prefix and write everything under `python_dir`.
fn extract_python_tarball(bytes: &[u8], python_dir: &Path) -> Result<()> {
    let cursor = std::io::Cursor::new(bytes);
    let decoder = zstd::stream::read::Decoder::new(cursor)
        .with_context(|| "Failed to create zstd decoder")?;
    let mut archive = tar::Archive::new(decoder);
    extract_python_archive_entries(&mut archive, python_dir, Path::new("python/install"))
}

/// Shared entry-extraction logic for both `.tar.gz` and `.tar.zst` archives.
///
/// `prefix` is the path component to strip from archive entries:
/// - `"python"` for install_only_stripped `.tar.gz`
/// - `"python/install"` for full `.tar.zst`
fn extract_python_archive_entries<R: std::io::Read>(
    archive: &mut tar::Archive<R>,
    python_dir: &Path,
    prefix: &Path,
) -> Result<()> {

    for entry in archive.entries()? {
        let mut entry = entry?;
        let entry_path = entry.path()?.to_path_buf();

        let relative = match entry_path.strip_prefix(prefix) {
            Ok(r) => r.to_path_buf(),
            Err(_) => continue, // skip entries outside python/install/
        };

        if relative.as_os_str().is_empty() {
            continue; // skip the root itself
        }

        let dest = python_dir.join(&relative);

        match entry.header().entry_type() {
            tar::EntryType::Directory => {
                fs::create_dir_all(&dest)?;
            }
            tar::EntryType::Symlink =>
            {
                #[cfg(unix)]
                if let Some(link_target) = entry.link_name()? {
                    if let Some(parent) = dest.parent() {
                        fs::create_dir_all(parent)?;
                    }
                    if dest.exists() || dest.is_symlink() {
                        fs::remove_file(&dest)?;
                    }
                    std::os::unix::fs::symlink(&link_target, &dest)?;
                }
            }
            _ => {
                if let Some(parent) = dest.parent() {
                    fs::create_dir_all(parent)?;
                }
                let mut file = fs::File::create(&dest)?;
                std::io::copy(&mut entry, &mut file)?;

                #[cfg(unix)]
                {
                    use std::os::unix::fs::PermissionsExt;
                    let mode = entry.header().mode()?;
                    fs::set_permissions(&dest, fs::Permissions::from_mode(mode))?;
                }
            }
        }
    }

    Ok(())
}

/// Install pre-built wheels for the target platform directly into a site-packages directory.
///
/// For regular Python: uses `uv pip install --python-platform <tag> --python-version <ver>`
/// so wheels can be fetched cross-platform without running target Python.
///
/// For free-threaded Python (version ends with 't') on the same platform as the host:
/// uses `--python <bin>` so uv detects the cp<N>t ABI and fetches free-threaded wheels.
pub fn download_wheels(
    project_dir: &Path,
    site_packages_dir: &Path,
    target_platform: &Platform,
    python_version: &str,
    uv_path: &Path,
    python_dir: &Path,
) -> Result<()> {
    println!("Exporting locked dependencies...");
    fs::create_dir_all(site_packages_dir).with_context(|| {
        format!(
            "Failed to create site-packages dir: {}",
            site_packages_dir.display()
        )
    })?;

    // Export pinned production requirements from uv.lock.
    let export_out = Command::new(uv_path)
        .args([
            "export",
            "--format",
            "requirements-txt",
            "--no-hashes",
            "--frozen",
            "--no-dev",
            "--no-group",
            "dev",
            "--no-emit-project",
        ])
        .current_dir(project_dir)
        .output()
        .with_context(|| "Failed to run 'uv export'")?;

    if !export_out.status.success() {
        let stderr = String::from_utf8_lossy(&export_out.stderr);
        return Err(anyhow!("'uv export' failed: {}", stderr));
    }

    let requirements = String::from_utf8_lossy(&export_out.stdout);
    let requirements = requirements.trim();

    if requirements.is_empty() {
        println!("No dependencies — skipping package installation.");
        return Ok(());
    }

    let req_path = std::env::temp_dir().join(format!("ux-requirements-{}.txt", std::process::id()));
    fs::write(&req_path, requirements.as_bytes())?;

    let pip_platform = platform_to_pip_platform(target_platform);

    println!(
        "Installing packages into site-packages (platform={}, python={}) ...",
        pip_platform, python_version
    );

    // For free-threaded Python on the same platform as the host, pass the actual
    // interpreter binary so uv detects the cp<N>t ABI and fetches free-threaded wheels.
    // For cross-platform or regular Python, use --python-version + --python-platform.
    let free_threaded = python_version.ends_with('t');
    let current_platform = Platform::current()?;
    let same_platform =
        current_platform.to_target_string() == target_platform.to_target_string();

    let status = if free_threaded && same_platform {
        #[cfg(windows)]
        let python_bin = python_dir.join("python.exe");
        #[cfg(not(windows))]
        let python_bin = python_dir.join("bin").join("python3");

        Command::new(uv_path)
            .args([
                "pip",
                "install",
                "--target",
                site_packages_dir.to_str().unwrap(),
                "--python",
                python_bin.to_str().unwrap(),
                "--only-binary",
                ":all:",
                "--no-deps",
                "-r",
                req_path.to_str().unwrap(),
            ])
            .status()
            .with_context(|| "Failed to run 'uv pip install'")?
    } else {
        // uv pip install --python-version does not accept the 't' free-threaded suffix.
        let pip_python_version = python_version.trim_end_matches('t');

        Command::new(uv_path)
            .args([
                "pip",
                "install",
                "--target",
                site_packages_dir.to_str().unwrap(),
                "--python-platform",
                pip_platform,
                "--python-version",
                pip_python_version,
                "--only-binary",
                ":all:",
                "--no-deps",
                "-r",
                req_path.to_str().unwrap(),
            ])
            .status()
            .with_context(|| "Failed to run 'uv pip install'")?
    };

    let _ = fs::remove_file(&req_path);

    if !status.success() {
        return Err(anyhow!(
            "'uv pip install' failed. \
             Some packages may not have binary wheels for platform '{}'.",
            pip_platform
        ));
    }

    let count = fs::read_dir(site_packages_dir)?
        .filter_map(|e| e.ok())
        .count();
    println!("Installed packages into site-packages ({} entries)", count);
    Ok(())
}
