use serde_json::Value;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::Command;
use toml::Value as TomlValue;

use crate::config::FeludaConfig;
use crate::debug::{log, log_debug, log_error, LogLevel};
use crate::licenses::{
    fetch_licenses_from_github, is_license_restrictive, LicenseCompatibility, LicenseInfo,
};

/// Represents an environment marker in a Python requirement
/// Environment markers follow PEP 508 and are used to specify conditional dependencies
/// Examples: "python_version < '3.8'", "sys_platform == 'win32'", "os_name == 'nt' and python_version >= '3.6'"
#[derive(Debug, Clone, PartialEq)]
pub struct EnvironmentMarker {
    /// The raw marker string
    pub raw: String,
    /// Parsed marker components (simplified representation)
    pub components: Vec<MarkerComponent>,
}

/// A single marker component (variable, operator, value)
#[derive(Debug, Clone, PartialEq)]
pub struct MarkerComponent {
    pub variable: String,
    pub operator: String,
    pub value: String,
}

impl EnvironmentMarker {
    /// Parse an environment marker string from a PEP 508 requirement
    /// Returns Some(marker) if marker exists, None if no marker or parsing fails gracefully
    fn parse(marker_str: &str) -> Option<Self> {
        let marker_str = marker_str.trim();
        if marker_str.is_empty() {
            return None;
        }

        let raw = marker_str.to_string();
        let components = parse_marker_components(marker_str);

        Some(EnvironmentMarker { raw, components })
    }

    /// Get a human-readable description of what environments this marker applies to
    pub fn describe(&self) -> String {
        if self.components.is_empty() {
            return self.raw.clone();
        }

        let mut descriptions = Vec::new();
        for component in &self.components {
            descriptions.push(format!(
                "{} {} '{}'",
                component.variable, component.operator, component.value
            ));
        }

        descriptions.join(" and ")
    }

    /// Check if this marker applies to a specific environment
    /// For now, we assume all markers apply (conservative approach)
    /// In production, you'd evaluate against actual environment
    #[allow(dead_code)]
    pub fn applies_to_environment(&self) -> bool {
        // Conservative approach: include all requirements regardless of markers
        // This ensures we don't miss license dependencies for specific environments
        true
    }
}

/// Parse environment marker components from a marker string
/// Supports markers like:
/// - "python_version < '3.8'"
/// - "sys_platform == 'win32'"
/// - "python_version >= '3.6' and os_name == 'nt'"
fn parse_marker_components(marker_str: &str) -> Vec<MarkerComponent> {
    let mut components = Vec::new();

    // Split by "and" operator for multiple conditions
    let conditions: Vec<&str> = marker_str.split(" and ").collect();

    for condition in conditions {
        let condition = condition.trim();

        // Try to parse each condition as a marker component
        // Format: variable operator 'value' or variable operator "value"
        let operators = vec!["!=", "==", "<=", ">=", "<", ">", "in", "not"];

        for op in operators {
            if let Some(parts) = condition.split_once(op) {
                let variable = parts.0.trim().to_string();
                let value_part = parts.1.trim();

                // Remove quotes from value
                let value = value_part.trim_matches('\'').trim_matches('"').to_string();

                if !variable.is_empty() && !value.is_empty() {
                    components.push(MarkerComponent {
                        variable,
                        operator: op.to_string(),
                        value,
                    });
                    break;
                }
            }
        }
    }

    components
}

/// Analyze the licenses of Python dependencies with transitive resolution
pub fn analyze_python_licenses(package_file_path: &str, config: &FeludaConfig) -> Vec<LicenseInfo> {
    let mut licenses = Vec::new();
    log(
        LogLevel::Info,
        &format!("Analyzing Python dependencies from: {package_file_path}"),
    );

    let known_licenses = match fetch_licenses_from_github() {
        Ok(licenses) => {
            log(
                LogLevel::Info,
                &format!("Fetched {} known licenses from GitHub", licenses.len()),
            );
            licenses
        }
        Err(err) => {
            log_error("Failed to fetch licenses from GitHub", &err);
            HashMap::new()
        }
    };

    // Check if it's a pyproject.toml file
    if package_file_path.ends_with("pyproject.toml") {
        let project_root = Path::new(package_file_path)
            .parent()
            .map(Path::to_path_buf)
            .unwrap_or_else(|| PathBuf::from("."));
        let attribution = build_uv_workspace_attribution(&project_root, package_file_path);
        if !attribution.is_empty() {
            log(
                LogLevel::Info,
                &format!(
                    "Built uv workspace attribution for {} unique deps across members",
                    attribution.len()
                ),
            );
        }

        match fs::read_to_string(package_file_path) {
            Ok(content) => match toml::from_str::<TomlValue>(&content) {
                Ok(toml_config) => {
                    let mut direct_deps = extract_pep508_deps_from_toml(&toml_config);
                    let is_workspace = is_uv_workspace_root(&toml_config);

                    if is_workspace {
                        let member_dirs =
                            collect_uv_workspace_member_dirs(&project_root, &toml_config);
                        log(
                            LogLevel::Info,
                            &format!(
                                "uv workspace detected; merging deps from {} members",
                                member_dirs.len()
                            ),
                        );
                        for member_dir in &member_dirs {
                            let member_pyproject = member_dir.join("pyproject.toml");
                            if !member_pyproject.exists() {
                                continue;
                            }
                            if let Ok(c) = fs::read_to_string(&member_pyproject) {
                                if let Ok(member_toml) = toml::from_str::<TomlValue>(&c) {
                                    let extra = extract_pep508_deps_from_toml(&member_toml);
                                    log(
                                        LogLevel::Info,
                                        &format!(
                                            "Member {} contributed {} direct deps",
                                            member_dir.display(),
                                            extra.len()
                                        ),
                                    );
                                    direct_deps.extend(extra);
                                }
                            }
                        }

                        let mut seen = HashSet::new();
                        direct_deps.retain(|(n, _)| seen.insert(n.clone()));
                    }

                    if direct_deps.is_empty() {
                        if is_workspace {
                            log(LogLevel::Warn, "uv workspace has no member dependencies");
                        } else if toml_config
                            .as_table()
                            .and_then(|t| t.get("project"))
                            .is_none()
                        {
                            log(
                                LogLevel::Warn,
                                "No 'project' section found in pyproject.toml",
                            );
                        } else {
                            log(
                                LogLevel::Warn,
                                "Failed to find dependencies in pyproject.toml",
                            );
                        }
                    } else {
                        log(
                            LogLevel::Info,
                            &format!("Found {} direct Python dependencies", direct_deps.len()),
                        );
                        log_debug("Direct dependencies", &direct_deps);

                        // Try to resolve all dependencies (direct + transitive) using uv or fallback to PyPI
                        let max_depth = config.dependencies.max_depth;
                        log(
                            LogLevel::Info,
                            &format!("Using max dependency depth: {max_depth}"),
                        );
                        let all_deps =
                            resolve_python_dependencies(&direct_deps, package_file_path, max_depth);

                        // Process all resolved dependencies
                        for (name, version) in all_deps {
                            log(
                                LogLevel::Info,
                                &format!("Processing dependency: {name} ({version})"),
                            );

                            let license_result =
                                fetch_license_for_python_dependency(&name, &version);
                            let license = Some(license_result);
                            let is_restrictive =
                                is_license_restrictive(&license, &known_licenses, config.strict);

                            if is_restrictive {
                                log(
                                    LogLevel::Warn,
                                    &format!("Restrictive license found: {license:?} for {name}"),
                                );
                            }

                            let sub_project = attribution.get(&name).map(|members| {
                                members.iter().cloned().collect::<Vec<_>>().join(", ")
                            });

                            licenses.push(LicenseInfo {
                                name,
                                version,
                                license: license.clone(),
                                is_restrictive,
                                compatibility: LicenseCompatibility::Unknown,
                                osi_status: match &license {
                                    Some(l) => crate::licenses::get_osi_status(l),
                                    None => crate::licenses::OsiStatus::Unknown,
                                },
                                sub_project,
                            });
                        }
                    }
                }
                Err(err) => {
                    log_error("Failed to parse pyproject.toml", &err);
                }
            },
            Err(err) => {
                log_error("Failed to read pyproject.toml file", &err);
            }
        }
    } else {
        log(LogLevel::Info, "Processing requirements.txt format");

        match File::open(package_file_path) {
            Ok(file) => {
                let reader = BufReader::new(file);
                let mut direct_deps = Vec::new();

                // Direct dependencies
                for line_result in reader.lines() {
                    match line_result {
                        Ok(line) => {
                            let line = line.trim();
                            if line.is_empty() || line.starts_with('#') {
                                continue;
                            }

                            // Parse requirement line (supporting various formats)
                            if let Some((name, version)) = parse_requirement_line(line) {
                                direct_deps.push((name, version));
                            } else {
                                log(LogLevel::Warn, &format!("Invalid requirement line: {line}"));
                            }
                        }
                        Err(err) => {
                            log_error("Failed to read line from requirements.txt", &err);
                        }
                    }
                }

                log(
                    LogLevel::Info,
                    &format!(
                        "Found {} direct requirements in requirements.txt",
                        direct_deps.len()
                    ),
                );

                // Try to resolve all dependencies (direct + transitive)
                let max_depth = config.dependencies.max_depth;
                log(
                    LogLevel::Info,
                    &format!("Using max dependency depth: {max_depth}"),
                );
                let all_deps =
                    resolve_python_dependencies(&direct_deps, package_file_path, max_depth);

                // Process all resolved dependencies
                for (name, version) in all_deps {
                    log(
                        LogLevel::Info,
                        &format!("Processing dependency: {name} ({version})"),
                    );

                    let license_result = fetch_license_for_python_dependency(&name, &version);
                    let license = Some(license_result);
                    let is_restrictive =
                        is_license_restrictive(&license, &known_licenses, config.strict);

                    if is_restrictive {
                        log(
                            LogLevel::Warn,
                            &format!("Restrictive license found: {license:?} for {name}"),
                        );
                    }

                    licenses.push(LicenseInfo {
                        name,
                        version,
                        license: license.clone(),
                        is_restrictive,
                        compatibility: LicenseCompatibility::Unknown,
                        osi_status: match &license {
                            Some(l) => crate::licenses::get_osi_status(l),
                            None => crate::licenses::OsiStatus::Unknown,
                        },
                        sub_project: None,
                    });
                }

                log(
                    LogLevel::Info,
                    &format!(
                        "Processed {} total dependencies (including transitive)",
                        licenses.len()
                    ),
                );
            }
            Err(err) => {
                log_error("Failed to open requirements.txt file", &err);
            }
        }
    }

    log(
        LogLevel::Info,
        &format!("Found {} Python dependencies with licenses", licenses.len()),
    );
    licenses
}

/// Fetch the license for a Python dependency, trying local sources first, then PyPI
pub fn fetch_license_for_python_dependency(name: &str, version: &str) -> String {
    if let Some(license) = get_license_from_local_site_packages(name) {
        log(
            LogLevel::Info,
            &format!("Found license in local site-packages for {name}: {license}"),
        );
        return license;
    }

    fetch_license_from_pypi(name, version)
}

fn get_license_from_local_site_packages(package_name: &str) -> Option<String> {
    let python_paths = get_python_site_packages_paths();

    for site_packages in python_paths {
        if let Some(license) = check_site_package_metadata(&site_packages, package_name) {
            return Some(license);
        }

        if let Some(license) = check_site_package_license_file(&site_packages, package_name) {
            return Some(license);
        }
    }
    None
}

fn get_python_site_packages_paths() -> Vec<std::path::PathBuf> {
    let mut paths = Vec::new();

    if let Ok(output) = Command::new("python3")
        .args([
            "-c",
            "import site; print('\\n'.join(site.getsitepackages()))",
        ])
        .output()
    {
        if output.status.success() {
            let stdout = String::from_utf8_lossy(&output.stdout);
            for line in stdout.lines() {
                paths.push(std::path::PathBuf::from(line.trim()));
            }
        }
    }

    if let Ok(output) = Command::new("python")
        .args([
            "-c",
            "import site; print('\\n'.join(site.getsitepackages()))",
        ])
        .output()
    {
        if output.status.success() {
            let stdout = String::from_utf8_lossy(&output.stdout);
            for line in stdout.lines() {
                let path = std::path::PathBuf::from(line.trim());
                if !paths.contains(&path) {
                    paths.push(path);
                }
            }
        }
    }

    paths
}

fn check_site_package_metadata(site_packages: &Path, package_name: &str) -> Option<String> {
    let metadata_file = site_packages
        .join(format!("{package_name}.dist-info"))
        .join("METADATA");

    if metadata_file.exists() {
        if let Ok(content) = fs::read_to_string(&metadata_file) {
            for line in content.lines() {
                if line.starts_with("License:") {
                    if let Some(license) = line.strip_prefix("License:") {
                        let license = license.trim();
                        if !license.is_empty() && license != "UNKNOWN" {
                            return Some(license.to_string());
                        }
                    }
                }
            }
        }
    }

    let normalized_name = package_name.replace('-', "_");
    let metadata_file_normalized = site_packages
        .join(format!("{normalized_name}.dist-info"))
        .join("METADATA");

    if metadata_file_normalized.exists() {
        if let Ok(content) = fs::read_to_string(&metadata_file_normalized) {
            for line in content.lines() {
                if line.starts_with("License:") {
                    if let Some(license) = line.strip_prefix("License:") {
                        let license = license.trim();
                        if !license.is_empty() && license != "UNKNOWN" {
                            return Some(license.to_string());
                        }
                    }
                }
            }
        }
    }

    None
}

fn check_site_package_license_file(site_packages: &Path, package_name: &str) -> Option<String> {
    let package_dirs = vec![
        site_packages.join(package_name),
        site_packages.join(package_name.replace('-', "_")),
    ];

    let license_files = [
        "LICENSE",
        "LICENSE.txt",
        "LICENSE.md",
        "COPYING",
        "COPYING.md",
    ];

    for package_dir in package_dirs {
        if !package_dir.exists() {
            continue;
        }

        for license_file in &license_files {
            let license_path = package_dir.join(license_file);
            if license_path.exists() {
                if let Ok(content) = fs::read_to_string(&license_path) {
                    if let Some(license) = detect_license_from_content(&content) {
                        return Some(license);
                    }
                }
            }
        }
    }
    None
}

fn detect_license_from_content(content: &str) -> Option<String> {
    let content_upper = content.to_uppercase();

    let patterns = vec![
        ("MIT", "MIT License"),
        ("APACHE", "Apache License"),
        ("GPL", "GPL"),
        ("BSD", "BSD"),
        ("ISC", "ISC License"),
        ("LGPL", "LGPL"),
        ("UNLICENSE", "Unlicense"),
        ("MPL", "Mozilla Public License"),
    ];

    for (pattern, label) in patterns {
        if content_upper.contains(pattern) {
            return Some(label.to_string());
        }
    }

    None
}

fn fetch_license_from_pypi(name: &str, version: &str) -> String {
    let api_url = format!("https://pypi.org/pypi/{name}/{version}/json");
    log(
        LogLevel::Info,
        &format!("Fetching license from PyPI: {api_url}"),
    );

    match reqwest::blocking::get(&api_url) {
        Ok(response) => {
            let status = response.status();
            log(
                LogLevel::Info,
                &format!("PyPI API response status: {status}"),
            );

            if status.is_success() {
                match response.json::<Value>() {
                    Ok(json) => match json["info"]["license"].as_str() {
                        Some(license_str) if !license_str.is_empty() => {
                            log(
                                LogLevel::Info,
                                &format!("License found for {name}: {license_str}"),
                            );
                            license_str.to_string()
                        }
                        _ => {
                            log(
                                LogLevel::Warn,
                                &format!("No license found for {name} ({version})"),
                            );
                            format!("Unknown license for {name}: {version}")
                        }
                    },
                    Err(err) => {
                        log_error(&format!("Failed to parse JSON for {name}: {version}"), &err);
                        String::from("Unknown")
                    }
                }
            } else {
                log(
                    LogLevel::Error,
                    &format!("Failed to fetch metadata for {name}: HTTP {status}"),
                );
                String::from("Unknown")
            }
        }
        Err(err) => {
            log_error(&format!("Failed to fetch metadata for {name}"), &err);
            String::from("Unknown")
        }
    }
}

/// Parse a requirement line from requirements.txt supporting various formats
/// Handles requirements.txt format with optional environment markers
/// Examples:
/// - "requests==2.31.0"
/// - "flask>=2.0.0"
/// - "django; python_version >= '3.8'"
/// - "numpy>=1.20.0; sys_platform == 'linux'"
fn parse_requirement_line(line: &str) -> Option<(String, String)> {
    let line = line.trim();

    // Extract marker if present
    let (base_req, marker) = if let Some((base, marker_str)) = line.split_once(';') {
        (base.trim(), EnvironmentMarker::parse(marker_str))
    } else {
        (line, None)
    };

    // Log marker information if present
    if let Some(marker) = &marker {
        log_debug(
            "Environment Marker (requirements.txt)",
            &format!("Detected marker: {} -> {}", marker.raw, marker.describe()),
        );
    }

    // Handle various requirement formats on the base requirement
    if let Some((name, version)) = base_req
        .split_once("==")
        .or_else(|| base_req.split_once(">="))
        .or_else(|| base_req.split_once(">"))
        .or_else(|| base_req.split_once("~="))
        .or_else(|| base_req.split_once("<="))
        .or_else(|| base_req.split_once("<"))
    {
        let name = name.trim();
        let version = version
            .trim()
            .trim_matches('"')
            .trim_matches('\'')
            .replace("^", "")
            .replace("~", "");
        Some((name.to_string(), version))
    } else {
        // Package name without version (with or without marker)
        Some((base_req.to_string(), "latest".to_string()))
    }
}

/// Resolve all Python dependencies (direct + transitive) with configurable depth
fn resolve_python_dependencies(
    direct_deps: &[(String, String)],
    package_file_path: &str,
    max_depth: u32,
) -> Vec<(String, String)> {
    log(
        LogLevel::Info,
        &format!("Resolving Python dependencies (including transitive up to depth {max_depth})"),
    );

    // First, try using uv for complete dependency resolution
    if let Ok(uv_deps) = resolve_with_uv(package_file_path, max_depth) {
        if !uv_deps.is_empty() {
            log(
                LogLevel::Info,
                &format!(
                    "Resolved {} dependencies using uv (depth {})",
                    uv_deps.len(),
                    max_depth
                ),
            );
            return uv_deps;
        }
    }

    // Fallback to PyPI-based transitive resolution
    log(
        LogLevel::Info,
        "Falling back to PyPI-based transitive dependency resolution",
    );
    resolve_with_pypi(direct_deps, max_depth)
}

/// Try to resolve dependencies using uv tool with depth limit
fn resolve_with_uv(
    package_file_path: &str,
    max_depth: u32,
) -> Result<Vec<(String, String)>, String> {
    let project_dir = Path::new(package_file_path)
        .parent()
        .ok_or("Cannot determine project directory")?;

    log(
        LogLevel::Info,
        &format!("Attempting to resolve dependencies with uv (max depth: {max_depth})"),
    );

    // Try uv lock command first (for uv-managed projects)
    if let Ok(output) = Command::new("uv")
        .args(["lock", "--dry-run"])
        .current_dir(project_dir)
        .output()
    {
        if output.status.success() {
            // Parse uv.lock file if it exists
            let lock_file = project_dir.join("uv.lock");
            if lock_file.exists() {
                if let Ok(deps) = parse_uv_lock(&lock_file, max_depth) {
                    log(
                        LogLevel::Info,
                        &format!("Resolved {} dependencies from uv.lock", deps.len()),
                    );
                    return Ok(deps);
                }
            }
        }
    }

    // Try pip-compile style resolution using uv
    if let Ok(output) = Command::new("uv")
        .args(["pip", "compile", "--dry-run", package_file_path])
        .current_dir(project_dir)
        .output()
    {
        if output.status.success() {
            let stdout_str = String::from_utf8_lossy(&output.stdout);
            let deps = parse_pip_compile_output(&stdout_str);
            log(
                LogLevel::Info,
                &format!(
                    "Resolved {} dependencies from pip-compile output",
                    deps.len()
                ),
            );
            return Ok(deps);
        }
    }

    Err("uv resolution failed".to_string())
}

/// Parse uv.lock file to extract dependencies with depth awareness
fn parse_uv_lock(lock_file: &Path, max_depth: u32) -> Result<Vec<(String, String)>, String> {
    let content =
        fs::read_to_string(lock_file).map_err(|e| format!("Failed to read uv.lock: {e}"))?;

    // uv.lock is TOML format
    let lock_data: TomlValue =
        toml::from_str(&content).map_err(|e| format!("Failed to parse uv.lock: {e}"))?;

    let mut deps = Vec::new();

    log(
        LogLevel::Info,
        &format!("Parsing uv.lock with max depth {max_depth}"),
    );

    // Extract packages from uv.lock format
    if let Some(packages) = lock_data.get("package").and_then(|p| p.as_array()) {
        for package in packages {
            if let Some(package_table) = package.as_table() {
                if let (Some(name), Some(version)) = (
                    package_table.get("name").and_then(|n| n.as_str()),
                    package_table.get("version").and_then(|v| v.as_str()),
                ) {
                    deps.push((name.to_string(), version.to_string()));
                }
            }
        }

        log(
            LogLevel::Info,
            &format!(
                "Extracted {} dependencies from uv.lock (all depths included)",
                deps.len()
            ),
        );
    }

    Ok(deps)
}

/// Parse pip-compile style output to extract dependencies
fn parse_pip_compile_output(output: &str) -> Vec<(String, String)> {
    let mut deps = Vec::new();

    for line in output.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        if let Some((name, version)) = parse_requirement_line(line) {
            deps.push((name, version));
        }
    }

    deps
}

/// Resolve transitive dependencies using PyPI API with configurable depth limit
fn resolve_with_pypi(direct_deps: &[(String, String)], max_depth: u32) -> Vec<(String, String)> {
    let mut all_deps = HashMap::new();
    let mut processed = HashSet::new();
    let mut to_process: Vec<(String, String, u32)> = direct_deps
        .iter()
        .map(|(name, version)| (name.clone(), version.clone(), 0))
        .collect();

    log(
        LogLevel::Info,
        &format!(
            "Starting PyPI-based resolution with {} direct dependencies (max depth: {})",
            direct_deps.len(),
            max_depth
        ),
    );

    let mut depth_stats = HashMap::new();

    // Iteratively resolve dependencies with depth tracking
    while let Some((name, version, depth)) = to_process.pop() {
        let key = format!("{name}@{version}");
        if processed.contains(&key) {
            continue;
        }

        // Skip if we've exceeded max depth
        if depth >= max_depth {
            log(
                LogLevel::Info,
                &format!("Skipping {name}@{version} - exceeded max depth {max_depth}"),
            );
            continue;
        }

        processed.insert(key);
        all_deps.insert(name.clone(), version.clone());

        // Track depth statistics
        *depth_stats.entry(depth).or_insert(0) += 1;

        log(
            LogLevel::Info,
            &format!("Resolving dependencies for: {name}@{version} (depth {depth})"),
        );

        // Fetch dependencies for this package
        if let Ok(transitive_deps) = fetch_pypi_dependencies(&name, &version) {
            log(
                LogLevel::Info,
                &format!(
                    "Found {} transitive dependencies for {} at depth {}",
                    transitive_deps.len(),
                    name,
                    depth
                ),
            );

            for (dep_name, dep_version) in transitive_deps {
                let dep_key = format!("{dep_name}@{dep_version}");
                if !processed.contains(&dep_key) {
                    to_process.push((dep_name, dep_version, depth + 1));
                }
            }
        }
    }

    // Log depth statistics
    for depth in 0..=max_depth {
        if let Some(count) = depth_stats.get(&depth) {
            log(
                LogLevel::Info,
                &format!("Depth {depth}: {count} dependencies"),
            );
        }
    }

    log(
        LogLevel::Info,
        &format!(
            "PyPI resolution completed. Total dependencies: {} (explored up to depth {})",
            all_deps.len(),
            max_depth
        ),
    );

    all_deps.into_iter().collect()
}

/// Fetch dependencies from PyPI for a specific package
fn fetch_pypi_dependencies(name: &str, version: &str) -> Result<Vec<(String, String)>, String> {
    let api_url = format!("https://pypi.org/pypi/{name}/{version}/json");

    match reqwest::blocking::get(&api_url) {
        Ok(response) => {
            if response.status().is_success() {
                if let Ok(json) = response.json::<Value>() {
                    let mut deps = Vec::new();

                    // Extract requires_dist information
                    if let Some(requires_dist) = json["info"]["requires_dist"].as_array() {
                        for req in requires_dist {
                            if let Some(req_str) = req.as_str() {
                                if let Some((dep_name, dep_version)) =
                                    parse_pypi_requirement(req_str)
                                {
                                    deps.push((dep_name, dep_version));
                                }
                            }
                        }
                    }

                    return Ok(deps);
                }
            }
        }
        Err(err) => {
            log_error(&format!("Failed to fetch dependencies for {name}"), &err);
        }
    }

    Ok(Vec::new())
}

/// Parse a PyPI requires_dist requirement string with full PEP 508 support
/// Handles requirements like:
/// - "requests>=2.20.0"
/// - "flask"
/// - "typing-extensions>=3.7.4; python_version < '3.8'"
/// - "urllib3 (<3,>=1.21.1); python_version >= '3.10'"
/// - "package[extra1,extra2]; sys_platform == 'win32'"
fn parse_pypi_requirement(req_str: &str) -> Option<(String, String)> {
    let req_str = req_str.trim();

    // Parse the requirement and extract marker separately
    let (base_req, marker) = if let Some((base, marker_str)) = req_str.split_once(';') {
        (base.trim(), EnvironmentMarker::parse(marker_str))
    } else {
        (req_str, None)
    };

    // Log marker information if present
    if let Some(marker) = &marker {
        log_debug(
            "Environment Marker",
            &format!("Detected marker: {} -> {}", marker.raw, marker.describe()),
        );
    }

    // Parse base requirement (name and version)
    let mut chars = base_req.chars().peekable();
    let mut name = String::new();

    // Extract package name (stop at version constraints, spaces, or extras)
    while let Some(ch) = chars.peek() {
        if ">=<!~=()[".contains(*ch) || ch.is_whitespace() {
            break;
        }
        if let Some(ch) = chars.next() {
            name.push(ch);
        }
    }

    let name = name.trim().to_string();
    if name.is_empty() {
        return None;
    }

    // Skip extras in square brackets if present (e.g., package[extra1,extra2])
    while let Some(ch) = chars.peek() {
        if *ch == '[' {
            chars.next(); // consume '['
            for ch in chars.by_ref() {
                if ch == ']' {
                    break;
                }
            }
        } else {
            break;
        }
    }

    // Skip whitespace and parentheses
    while let Some(ch) = chars.peek() {
        if ch.is_whitespace() || *ch == '(' {
            chars.next();
        } else {
            break;
        }
    }

    // Extract version constraint
    let remaining: String = chars.collect();
    let remaining = remaining.trim_end_matches(')').trim();

    if remaining.is_empty() {
        return Some((name, "latest".to_string()));
    }

    // Parse version constraints
    let constraints: Vec<&str> = remaining.split(',').collect();
    let mut best_version = "latest";

    for constraint in &constraints {
        if let Some((_operator, version_part)) = parse_version_constraint(constraint.trim()) {
            if constraint.trim().starts_with(">=") || constraint.trim().starts_with("==") {
                best_version = version_part.trim();
                break;
            } else if best_version == "latest" {
                best_version = version_part.trim();
            }
        }
    }

    Some((name, best_version.to_string()))
}

/// Parse version constraint operators
fn parse_version_constraint(constraint: &str) -> Option<(&str, &str)> {
    let constraint = constraint.trim();

    if let Some(version) = constraint.strip_prefix(">=") {
        Some((">=", version.trim()))
    } else if let Some(version) = constraint.strip_prefix("<=") {
        Some(("<=", version.trim()))
    } else if let Some(version) = constraint.strip_prefix("==") {
        Some(("==", version.trim()))
    } else if let Some(version) = constraint.strip_prefix("~=") {
        Some(("~=", version.trim()))
    } else if let Some(version) = constraint.strip_prefix("!=") {
        Some(("!=", version.trim()))
    } else if let Some(version) = constraint.strip_prefix(">") {
        Some((">", version.trim()))
    } else {
        constraint
            .strip_prefix("<")
            .map(|version| ("<", version.trim()))
    }
}

/// Extract direct deps from a parsed pyproject.toml's `[project] dependencies` array.
/// Returns (name, version) pairs; version is "latest" when no constraint is present.
fn extract_pep508_deps_from_toml(toml_config: &TomlValue) -> Vec<(String, String)> {
    let mut deps = Vec::new();
    if let Some(arr) = toml_config
        .as_table()
        .and_then(|t| t.get("project"))
        .and_then(|p| p.as_table())
        .and_then(|t| t.get("dependencies"))
        .and_then(|d| d.as_array())
    {
        for dep in arr {
            if let Some(dep_str) = dep.as_str() {
                deps.push(split_pep508_dep(dep_str));
            }
        }
    }
    deps
}

fn split_pep508_dep(dep_str: &str) -> (String, String) {
    if let Some((n, v)) = dep_str
        .split_once("==")
        .or_else(|| dep_str.split_once(">="))
        .or_else(|| dep_str.split_once(">"))
        .or_else(|| dep_str.split_once("~="))
        .or_else(|| dep_str.split_once("<="))
        .or_else(|| dep_str.split_once("<"))
    {
        (n.trim().to_string(), v.trim().to_string())
    } else {
        (dep_str.trim().to_string(), "latest".to_string())
    }
}

fn is_uv_workspace_root(toml_config: &TomlValue) -> bool {
    toml_config
        .as_table()
        .and_then(|t| t.get("tool"))
        .and_then(|t| t.as_table())
        .and_then(|t| t.get("uv"))
        .and_then(|u| u.as_table())
        .and_then(|u| u.get("workspace"))
        .is_some()
}

/// Expand a `[tool.uv.workspace]` block into a list of member directories.
///
/// Honors the `members` glob list and the optional `exclude` patterns. The exclude
/// match is intentionally simple — it strips a trailing `/*` and compares the prefix.
fn collect_uv_workspace_member_dirs(project_root: &Path, toml_config: &TomlValue) -> Vec<PathBuf> {
    let workspace = match toml_config
        .as_table()
        .and_then(|t| t.get("tool"))
        .and_then(|t| t.get("uv"))
        .and_then(|u| u.get("workspace"))
        .and_then(|w| w.as_table())
    {
        Some(w) => w,
        None => return Vec::new(),
    };

    let members: Vec<&str> = workspace
        .get("members")
        .and_then(|m| m.as_array())
        .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
        .unwrap_or_default();

    let exclude: Vec<&str> = workspace
        .get("exclude")
        .and_then(|m| m.as_array())
        .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
        .unwrap_or_default();

    let mut result = Vec::new();
    for pattern in members {
        for dir in expand_uv_member_pattern(project_root, pattern) {
            if !is_uv_member_excluded(&dir, project_root, &exclude) {
                result.push(dir);
            }
        }
    }
    result
}

fn expand_uv_member_pattern(project_root: &Path, pattern: &str) -> Vec<PathBuf> {
    let mut result = Vec::new();
    if let Some(stripped) = pattern.strip_suffix("/*") {
        let parent = project_root.join(stripped);
        if parent.is_dir() {
            if let Ok(entries) = fs::read_dir(&parent) {
                for entry in entries.flatten() {
                    let p = entry.path();
                    if p.is_dir() {
                        result.push(p);
                    }
                }
            }
        }
    } else {
        let p = project_root.join(pattern);
        if p.is_dir() {
            result.push(p);
        }
    }
    result
}

fn is_uv_member_excluded(dir: &Path, project_root: &Path, exclude: &[&str]) -> bool {
    let rel = match dir.strip_prefix(project_root) {
        Ok(r) => r.to_string_lossy().replace('\\', "/"),
        Err(_) => return false,
    };
    for pat in exclude {
        let prefix = pat.strip_suffix("/*").unwrap_or(pat);
        if rel == prefix || rel.starts_with(&format!("{prefix}/")) {
            return true;
        }
    }
    false
}

/// Build a map from dep name -> set of workspace member names that declare it.
///
/// Returns an empty map for non-workspace pyproject.toml files. The root project's
/// own deps are attributed to its `[project] name` (or "root" if unnamed).
fn build_uv_workspace_attribution(
    project_root: &Path,
    pyproject_path: &str,
) -> HashMap<String, BTreeSet<String>> {
    let mut attribution: HashMap<String, BTreeSet<String>> = HashMap::new();

    let root_content = match fs::read_to_string(pyproject_path) {
        Ok(c) => c,
        Err(_) => return attribution,
    };
    let root_toml: TomlValue = match toml::from_str(&root_content) {
        Ok(v) => v,
        Err(_) => return attribution,
    };

    if !is_uv_workspace_root(&root_toml) {
        return attribution;
    }

    let root_name = root_toml
        .as_table()
        .and_then(|t| t.get("project"))
        .and_then(|p| p.as_table())
        .and_then(|p| p.get("name"))
        .and_then(|n| n.as_str())
        .map(String::from)
        .unwrap_or_else(|| "root".to_string());
    record_uv_direct_deps(&root_toml, &root_name, &mut attribution);

    for member_dir in collect_uv_workspace_member_dirs(project_root, &root_toml) {
        let member_pyproject = member_dir.join("pyproject.toml");
        if !member_pyproject.exists() {
            continue;
        }
        let content = match fs::read_to_string(&member_pyproject) {
            Ok(c) => c,
            Err(_) => continue,
        };
        let member_toml: TomlValue = match toml::from_str(&content) {
            Ok(v) => v,
            Err(_) => continue,
        };
        let member_name = member_toml
            .as_table()
            .and_then(|t| t.get("project"))
            .and_then(|p| p.as_table())
            .and_then(|p| p.get("name"))
            .and_then(|n| n.as_str())
            .map(String::from)
            .unwrap_or_else(|| {
                member_dir
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("unknown")
                    .to_string()
            });
        record_uv_direct_deps(&member_toml, &member_name, &mut attribution);
    }

    attribution
}

fn record_uv_direct_deps(
    toml_config: &TomlValue,
    member_name: &str,
    attribution: &mut HashMap<String, BTreeSet<String>>,
) {
    for (dep_name, _) in extract_pep508_deps_from_toml(toml_config) {
        attribution
            .entry(dep_name)
            .or_default()
            .insert(member_name.to_string());
    }
}

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

    #[test]
    fn test_analyze_python_licenses_pyproject_toml() {
        let temp_dir = TempDir::new().unwrap();
        let pyproject_toml_path = temp_dir.path().join("pyproject.toml");

        std::fs::write(
            &pyproject_toml_path,
            r#"[project]
    name = "test-project"
    version = "0.1.0"
    dependencies = [
        "requests>=2.31.0",
        "flask~=2.0.0"
    ]
    "#,
        )
        .unwrap();

        let config = FeludaConfig::default();
        let result = analyze_python_licenses(pyproject_toml_path.to_str().unwrap(), &config);
        assert!(!result.is_empty());
        assert!(result.iter().any(|info| info.name == "requests"));
        assert!(result.iter().any(|info| info.name == "flask"));
    }

    #[test]
    fn test_analyze_python_licenses_empty_file() {
        let temp_dir = TempDir::new().unwrap();
        let requirements_path = temp_dir.path().join("requirements.txt");

        std::fs::write(&requirements_path, "").unwrap();

        let config = FeludaConfig::default();
        let result = analyze_python_licenses(requirements_path.to_str().unwrap(), &config);
        assert!(result.is_empty());
    }

    #[test]
    fn test_analyze_python_licenses_invalid_format() {
        let temp_dir = TempDir::new().unwrap();
        let requirements_path = temp_dir.path().join("requirements.txt");

        std::fs::write(&requirements_path, "# This is a comment\n\n").unwrap();

        let config = FeludaConfig::default();
        let result = analyze_python_licenses(requirements_path.to_str().unwrap(), &config);
        assert!(result.is_empty());
    }

    #[test]
    fn test_analyze_python_licenses_packages_without_versions() {
        let temp_dir = TempDir::new().unwrap();
        let requirements_path = temp_dir.path().join("requirements.txt");

        std::fs::write(
            &requirements_path,
            "requests\nflask\n# This is a comment\nnumpy",
        )
        .unwrap();

        let config = FeludaConfig::default();
        let result = analyze_python_licenses(requirements_path.to_str().unwrap(), &config);
        // Process packages without explicit versions using transitive resolution
        assert!(!result.is_empty());
        assert!(result.iter().any(|info| info.name == "requests"));
        assert!(result.iter().any(|info| info.name == "flask"));
        assert!(result.iter().any(|info| info.name == "numpy"));
    }

    #[test]
    fn test_fetch_license_for_python_dependency_error_handling() {
        // Test with a definitely non-existent package
        let result =
            fetch_license_for_python_dependency("definitely_nonexistent_package_12345", "1.0.0");
        assert!(result.contains("Unknown") || result.contains("nonexistent"));
    }

    #[test]
    fn test_parse_requirement_line() {
        // Test various requirement formats
        assert_eq!(
            parse_requirement_line("requests==2.31.0"),
            Some(("requests".to_string(), "2.31.0".to_string()))
        );
        assert_eq!(
            parse_requirement_line("flask>=2.0.0"),
            Some(("flask".to_string(), "2.0.0".to_string()))
        );
        assert_eq!(
            parse_requirement_line("django"),
            Some(("django".to_string(), "latest".to_string()))
        );
    }

    #[test]
    fn test_parse_pypi_requirement() {
        // Test PyPI requires_dist format parsing
        assert_eq!(
            parse_pypi_requirement("requests>=2.20.0"),
            Some(("requests".to_string(), "2.20.0".to_string()))
        );
        assert_eq!(
            parse_pypi_requirement("typing-extensions>=3.7.4; python_version < '3.8'"),
            Some(("typing-extensions".to_string(), "3.7.4".to_string()))
        );
        assert_eq!(
            parse_pypi_requirement("flask"),
            Some(("flask".to_string(), "latest".to_string()))
        );

        // Test complex version constraints
        assert_eq!(
            parse_pypi_requirement("urllib3 (<3,>=1.21.1)"),
            Some(("urllib3".to_string(), "1.21.1".to_string()))
        );
        assert_eq!(
            parse_pypi_requirement("chardet (<6,>=3.0.2)"),
            Some(("chardet".to_string(), "3.0.2".to_string()))
        );
        assert_eq!(
            parse_pypi_requirement("PySocks (!=1.5.7,>=1.5.6)"),
            Some(("PySocks".to_string(), "1.5.6".to_string()))
        );
    }

    #[test]
    fn test_parse_environment_markers() {
        // Test simple markers
        let marker1 = EnvironmentMarker::parse("python_version < '3.8'");
        assert!(marker1.is_some());
        if let Some(m) = marker1 {
            assert!(!m.components.is_empty());
            assert!(m.components[0].variable.contains("python_version"));
            assert_eq!(m.components[0].operator, "<");
            assert_eq!(m.components[0].value, "3.8");
        }

        // Test marker with ==
        let marker2 = EnvironmentMarker::parse("sys_platform == 'win32'");
        assert!(marker2.is_some());
        if let Some(m) = marker2 {
            assert!(!m.components.is_empty());
            assert_eq!(m.components[0].operator, "==");
            assert_eq!(m.components[0].value, "win32");
        }

        // Test multiple conditions
        let marker3 = EnvironmentMarker::parse("python_version >= '3.6' and os_name == 'nt'");
        assert!(marker3.is_some());
        if let Some(m) = marker3 {
            assert_eq!(m.components.len(), 2);
        }

        // Test empty marker
        let marker4 = EnvironmentMarker::parse("");
        assert!(marker4.is_none());
    }

    #[test]
    fn test_parse_requirement_with_markers() {
        // Test requirements.txt format with markers
        assert_eq!(
            parse_requirement_line("requests==2.31.0"),
            Some(("requests".to_string(), "2.31.0".to_string()))
        );

        assert_eq!(
            parse_requirement_line("django>=3.2; python_version >= '3.8'"),
            Some(("django".to_string(), "3.2".to_string()))
        );

        assert_eq!(
            parse_requirement_line("numpy; sys_platform == 'linux'"),
            Some(("numpy".to_string(), "latest".to_string()))
        );

        // Test with multiple version constraints and markers
        // Note: We keep the full version string when there are multiple constraints
        assert_eq!(
            parse_requirement_line("scipy>=1.7,<2.0; python_version >= '3.9'"),
            Some(("scipy".to_string(), "1.7,<2.0".to_string()))
        );
    }

    #[test]
    fn test_parse_pypi_requirement_with_extras() {
        // Test requirements with extras (square brackets)
        assert_eq!(
            parse_pypi_requirement("requests[security]>=2.20.0"),
            Some(("requests".to_string(), "2.20.0".to_string()))
        );

        assert_eq!(
            parse_pypi_requirement("package[extra1,extra2]>=1.0; python_version >= '3.7'"),
            Some(("package".to_string(), "1.0".to_string()))
        );
    }

    #[test]
    fn test_marker_description() {
        let marker = EnvironmentMarker::parse("python_version < '3.8' and sys_platform == 'win32'");
        assert!(marker.is_some());
        if let Some(m) = marker {
            let description = m.describe();
            assert!(description.contains("python_version"));
            assert!(description.contains("sys_platform"));
        }
    }

    #[test]
    fn test_marker_applies_to_environment() {
        // All markers should return true (conservative approach)
        let marker1 = EnvironmentMarker::parse("python_version < '3.8'");
        assert!(marker1.unwrap().applies_to_environment());

        let marker2 = EnvironmentMarker::parse("sys_platform == 'darwin'");
        assert!(marker2.unwrap().applies_to_environment());
    }

    #[test]
    fn test_split_pep508_dep_variants() {
        assert_eq!(
            split_pep508_dep("fastapi==0.115.0"),
            ("fastapi".to_string(), "0.115.0".to_string())
        );
        assert_eq!(
            split_pep508_dep("click>=8.0"),
            ("click".to_string(), "8.0".to_string())
        );
        assert_eq!(
            split_pep508_dep("requests~=2.31.0"),
            ("requests".to_string(), "2.31.0".to_string())
        );
        assert_eq!(
            split_pep508_dep("celery"),
            ("celery".to_string(), "latest".to_string())
        );
    }

    #[test]
    fn test_extract_pep508_deps_from_toml() {
        let toml_content = r#"
[project]
name = "demo"
dependencies = ["fastapi==0.115.0", "click>=8.0", "no-version"]
"#;
        let parsed: TomlValue = toml::from_str(toml_content).unwrap();
        let deps = extract_pep508_deps_from_toml(&parsed);
        assert_eq!(deps.len(), 3);
        assert!(deps.iter().any(|(n, v)| n == "fastapi" && v == "0.115.0"));
        assert!(deps.iter().any(|(n, v)| n == "click" && v == "8.0"));
        assert!(deps.iter().any(|(n, v)| n == "no-version" && v == "latest"));
    }

    #[test]
    fn test_is_uv_workspace_root_detects_workspace() {
        let with_workspace = toml::from_str::<TomlValue>(
            r#"
[project]
name = "monorepo"

[tool.uv.workspace]
members = ["packages/*"]
"#,
        )
        .unwrap();
        assert!(is_uv_workspace_root(&with_workspace));

        let without_workspace = toml::from_str::<TomlValue>(
            r#"
[project]
name = "single"
dependencies = []
"#,
        )
        .unwrap();
        assert!(!is_uv_workspace_root(&without_workspace));
    }

    #[test]
    fn test_collect_uv_workspace_member_dirs_globs_and_excludes() {
        let temp = TempDir::new().unwrap();
        let root = temp.path();
        for sub in &[
            "services/api",
            "services/worker",
            "services/legacy",
            "packages/shared",
        ] {
            std::fs::create_dir_all(root.join(sub)).unwrap();
        }

        let toml_config: TomlValue = toml::from_str(
            r#"
[project]
name = "monorepo"

[tool.uv.workspace]
members = ["services/*", "packages/*"]
exclude = ["services/legacy"]
"#,
        )
        .unwrap();

        let dirs = collect_uv_workspace_member_dirs(root, &toml_config);
        let names: BTreeSet<String> = dirs
            .iter()
            .filter_map(|p| p.file_name().and_then(|n| n.to_str()).map(String::from))
            .collect();

        assert!(names.contains("api"));
        assert!(names.contains("worker"));
        assert!(names.contains("shared"));
        assert!(
            !names.contains("legacy"),
            "legacy was excluded but still surfaced"
        );
    }

    #[test]
    fn test_build_uv_workspace_attribution_root_and_members() {
        let temp = TempDir::new().unwrap();
        let root = temp.path();

        std::fs::write(
            root.join("pyproject.toml"),
            r#"
[project]
name = "monorepo-root"
dependencies = ["click>=8.0"]

[tool.uv.workspace]
members = ["services/*"]
"#,
        )
        .unwrap();

        let api = root.join("services/api");
        std::fs::create_dir_all(&api).unwrap();
        std::fs::write(
            api.join("pyproject.toml"),
            r#"
[project]
name = "api-service"
dependencies = ["fastapi==0.115.0", "click>=8.0"]
"#,
        )
        .unwrap();

        let worker = root.join("services/worker");
        std::fs::create_dir_all(&worker).unwrap();
        std::fs::write(
            worker.join("pyproject.toml"),
            r#"
[project]
name = "worker-service"
dependencies = ["celery>=5.0"]
"#,
        )
        .unwrap();

        let attribution =
            build_uv_workspace_attribution(root, root.join("pyproject.toml").to_str().unwrap());

        let click = attribution.get("click").expect("click attributed");
        assert!(click.contains("monorepo-root"));
        assert!(click.contains("api-service"));
        assert_eq!(click.len(), 2);

        let fastapi = attribution.get("fastapi").expect("fastapi attributed");
        assert_eq!(fastapi.iter().next().unwrap(), "api-service");

        let celery = attribution.get("celery").expect("celery attributed");
        assert_eq!(celery.iter().next().unwrap(), "worker-service");
    }

    #[test]
    fn test_build_uv_workspace_attribution_returns_empty_for_non_workspace() {
        let temp = TempDir::new().unwrap();
        std::fs::write(
            temp.path().join("pyproject.toml"),
            r#"
[project]
name = "single"
dependencies = ["click>=8.0"]
"#,
        )
        .unwrap();

        let attribution = build_uv_workspace_attribution(
            temp.path(),
            temp.path().join("pyproject.toml").to_str().unwrap(),
        );
        assert!(attribution.is_empty());
    }
}
