//! Core license analysis functionality and types

use crate::spdx;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use toml::Value as TomlValue;

use crate::cache;
use crate::cli;
use crate::config;
use crate::debug::{log, log_debug, log_error, FeludaResult, LogLevel};

static GITHUB_TOKEN: OnceLock<Option<String>> = OnceLock::new();

/// Set the GitHub API token for authenticated requests
pub fn set_github_token(token: Option<String>) {
    let _ = GITHUB_TOKEN.set(token);
}

/// Get the GitHub API token if set
fn get_github_token() -> Option<&'static str> {
    GITHUB_TOKEN.get().and_then(|t| t.as_deref())
}

/// License compatibility enum
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LicenseCompatibility {
    Compatible,
    Incompatible,
    Unknown,
}

impl std::fmt::Display for LicenseCompatibility {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Compatible => write!(f, "Compatible"),
            Self::Incompatible => write!(f, "Incompatible"),
            Self::Unknown => write!(f, "Unknown"),
        }
    }
}

/// Structure for deserializing license compatibility matrix from TOML
#[derive(Deserialize, Debug, Clone)]
struct LicenseCompatibilityMatrix {
    #[serde(rename = "MIT")]
    mit: Option<LicenseEntry>,
    #[serde(rename = "Apache-2_0")]
    apache_2_0: Option<LicenseEntry>,
    #[serde(rename = "GPL-3_0")]
    gpl_3_0: Option<LicenseEntry>,
    #[serde(rename = "GPL-2_0")]
    gpl_2_0: Option<LicenseEntry>,
    #[serde(rename = "AGPL-3_0")]
    agpl_3_0: Option<LicenseEntry>,
    #[serde(rename = "LGPL-3_0")]
    lgpl_3_0: Option<LicenseEntry>,
    #[serde(rename = "LGPL-2_1")]
    lgpl_2_1: Option<LicenseEntry>,
    #[serde(rename = "MPL-2_0")]
    mpl_2_0: Option<LicenseEntry>,
    #[serde(rename = "BSD-3-Clause")]
    bsd_3_clause: Option<LicenseEntry>,
    #[serde(rename = "BSD-2-Clause")]
    bsd_2_clause: Option<LicenseEntry>,
    #[serde(rename = "ISC")]
    isc: Option<LicenseEntry>,
    #[serde(rename = "_0BSD")]
    bsd_0: Option<LicenseEntry>,
    #[serde(rename = "Unlicense")]
    unlicense: Option<LicenseEntry>,
    #[serde(rename = "WTFPL")]
    wtfpl: Option<LicenseEntry>,
}

#[derive(Deserialize, Debug, Clone)]
struct LicenseEntry {
    compatible_with: Vec<String>,
}

/// Static cache for the compatibility matrix
#[cfg(not(test))]
static COMPATIBILITY_MATRIX: OnceLock<HashMap<String, Vec<String>>> = OnceLock::new();

/// OSI license status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OsiStatus {
    Approved,
    NotApproved,
    Unknown,
}

impl std::fmt::Display for OsiStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Approved => write!(f, "approved"),
            Self::NotApproved => write!(f, "not-approved"),
            Self::Unknown => write!(f, "unknown"),
        }
    }
}

/// OSI license information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsiLicenseInfo {
    pub id: String,
    pub name: String,
    pub status: OsiStatus,
}

/// License Info of dependencies
#[derive(Serialize, Debug, Clone)]
pub struct LicenseInfo {
    pub name: String,                        // The name of the software or library
    pub version: String,                     // The version of the software or library
    pub license: Option<String>, // An optional field that contains the license type (e.g., MIT, Apache 2.0)
    pub is_restrictive: bool,    // A boolean indicating whether the license is restrictive or not
    pub compatibility: LicenseCompatibility, // Compatibility with project license
    pub osi_status: OsiStatus,   // OSI approval status
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sub_project: Option<String>, // Workspace member that brought in this dependency (None for non-monorepos)
}

impl LicenseInfo {
    pub fn get_license(&self) -> String {
        match &self.license {
            Some(license_name) => String::from(license_name),
            None => String::from("No License"),
        }
    }

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

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

    pub fn is_restrictive(&self) -> &bool {
        &self.is_restrictive
    }

    pub fn compatibility(&self) -> &LicenseCompatibility {
        &self.compatibility
    }

    pub fn osi_status(&self) -> &OsiStatus {
        &self.osi_status
    }

    pub fn sub_project(&self) -> Option<&str> {
        self.sub_project.as_deref()
    }

    #[allow(dead_code)]
    pub fn osi_info(&self) -> Option<OsiLicenseInfo> {
        self.license.as_ref().map(|license| OsiLicenseInfo {
            id: license.clone(),
            name: license.clone(),
            status: self.osi_status,
        })
    }
}

/// License Info structure for GitHub API data
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct License {
    pub title: String,            // The full name of the license
    pub spdx_id: String,          // The SPDX identifier for the license
    pub permissions: Vec<String>, // A list of permissions granted by the license
    pub conditions: Vec<String>,  // A list of conditions that must be met under the license
    pub limitations: Vec<String>, // A list of limitations imposed by the license
}

/// Fetch license data from GitHub's official Licenses API
/// Attempts to load from cache first, falls back to GitHub API if cache miss or stale
pub fn fetch_licenses_from_github() -> FeludaResult<HashMap<String, License>> {
    log(LogLevel::Info, "Fetching licenses from GitHub Licenses API");

    match cache::load_github_licenses_from_cache() {
        Ok(Some(cached_licenses)) => {
            log(
                LogLevel::Info,
                &format!("Using cached licenses ({})", cached_licenses.len()),
            );
            return Ok(cached_licenses);
        }
        Ok(None) => {
            log(LogLevel::Info, "Cache miss or stale, fetching from GitHub");
        }
        Err(e) => {
            log(
                LogLevel::Warn,
                &format!("Cache read error: {e}, fetching from GitHub"),
            );
        }
    }

    let licenses_map = cli::with_spinner("Fetching licenses from GitHub API", |indicator| {
        let rt = match tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
        {
            Ok(rt) => rt,
            Err(err) => {
                log_error("Failed to create tokio runtime", &err);
                return HashMap::new();
            }
        };

        rt.block_on(fetch_licenses_concurrent(indicator))
    });

    if !licenses_map.is_empty() {
        if let Err(e) = cache::save_github_licenses_to_cache(&licenses_map) {
            log(LogLevel::Warn, &format!("Failed to save cache: {e}"));
        }
    } else {
        log(
            LogLevel::Warn,
            "No licenses fetched from GitHub API, cache not saved",
        );
    }

    Ok(licenses_map)
}

/// Async helper function for concurrent license fetching with rate limiting
async fn fetch_licenses_concurrent(
    indicator: &crate::cli::LoadingIndicator,
) -> HashMap<String, License> {
    let mut licenses_map = HashMap::new();

    // Create async HTTP client with optional authentication
    let mut client_builder = reqwest::Client::builder()
        .user_agent("feluda-license-checker/1.0")
        .timeout(Duration::from_secs(30));

    if let Some(token) = get_github_token() {
        log(
            LogLevel::Info,
            "Using authenticated GitHub API requests (higher rate limits)",
        );
        let mut headers = reqwest::header::HeaderMap::new();
        headers.insert(
            reqwest::header::AUTHORIZATION,
            format!("Bearer {token}")
                .parse()
                .expect("Invalid token format"),
        );
        client_builder = client_builder.default_headers(headers);
    }

    let client = match client_builder.build() {
        Ok(client) => client,
        Err(err) => {
            log_error("Failed to create HTTP client", &err);
            return licenses_map;
        }
    };

    indicator.update_progress("fetching license list");

    // First, get the list of available licenses
    let licenses_list_url = "https://api.github.com/licenses";
    let response = match client.get(licenses_list_url).send().await {
        Ok(response) => response,
        Err(err) => {
            log_error("Failed to fetch licenses list from GitHub API", &err);
            return licenses_map;
        }
    };

    if !response.status().is_success() {
        log(
            LogLevel::Error,
            &format!("GitHub API returned error status: {}", response.status()),
        );
        return licenses_map;
    }

    let licenses_list: Vec<serde_json::Value> = match response.json().await {
        Ok(list) => list,
        Err(err) => {
            log_error("Failed to parse licenses list JSON", &err);
            return licenses_map;
        }
    };

    let total_licenses = licenses_list.len();
    indicator.update_progress(&format!("found {total_licenses} licenses"));

    let client = Arc::new(client);

    let license_keys: Vec<String> = licenses_list
        .iter()
        .filter_map(|license_info| {
            license_info
                .get("key")
                .and_then(|k| k.as_str())
                .map(|s| s.to_string())
        })
        .collect();

    // H2 multiplexes all requests over a single connection; no throttling needed for ~13 licenses
    let mut join_set = tokio::task::JoinSet::new();

    for license_key in license_keys {
        let client = Arc::clone(&client);

        join_set.spawn(async move {
            log(
                LogLevel::Info,
                &format!("Fetching detailed license info: {license_key}"),
            );

            let license_url = format!("https://api.github.com/licenses/{license_key}");

            match client.get(&license_url).send().await {
                Ok(license_response) => {
                    if license_response.status().is_success() {
                        match license_response.json::<serde_json::Value>().await {
                            Ok(license_data) => {
                                // Extract the license information we need
                                let title = license_data
                                    .get("name")
                                    .and_then(|n| n.as_str())
                                    .unwrap_or(&license_key)
                                    .to_string();

                                let spdx_id = license_data
                                    .get("spdx_id")
                                    .and_then(|s| s.as_str())
                                    .unwrap_or(&license_key)
                                    .to_string();

                                let permissions = license_data
                                    .get("permissions")
                                    .and_then(|p| p.as_array())
                                    .map(|arr| {
                                        arr.iter()
                                            .filter_map(|v| v.as_str())
                                            .map(String::from)
                                            .collect()
                                    })
                                    .unwrap_or_default();

                                let conditions = license_data
                                    .get("conditions")
                                    .and_then(|c| c.as_array())
                                    .map(|arr| {
                                        arr.iter()
                                            .filter_map(|v| v.as_str())
                                            .map(String::from)
                                            .collect()
                                    })
                                    .unwrap_or_default();

                                let limitations = license_data
                                    .get("limitations")
                                    .and_then(|l| l.as_array())
                                    .map(|arr| {
                                        arr.iter()
                                            .filter_map(|v| v.as_str())
                                            .map(String::from)
                                            .collect()
                                    })
                                    .unwrap_or_default();

                                let license = License {
                                    title,
                                    spdx_id,
                                    permissions,
                                    conditions,
                                    limitations,
                                };

                                // Use the SPDX ID as the key for consistency
                                let key_to_use = license_data
                                    .get("spdx_id")
                                    .and_then(|s| s.as_str())
                                    .unwrap_or(&license_key);

                                log(
                                    LogLevel::Info,
                                    &format!("Successfully processed license: {key_to_use}"),
                                );

                                Some((key_to_use.to_string(), license))
                            }
                            Err(err) => {
                                log_error(
                                    &format!("Failed to parse license JSON for {license_key}"),
                                    &err,
                                );
                                None
                            }
                        }
                    } else {
                        log(
                            LogLevel::Error,
                            &format!(
                                "Failed to fetch license {}: HTTP {}",
                                license_key,
                                license_response.status()
                            ),
                        );
                        None
                    }
                }
                Err(err) => {
                    log_error(
                        &format!("Failed to fetch license details for {license_key}"),
                        &err,
                    );
                    None
                }
            }
        });
    }

    let mut license_count = 0;
    while let Some(result) = join_set.join_next().await {
        match result {
            Ok(Some((key, license))) => {
                licenses_map.insert(key, license);
                license_count += 1;
            }
            Ok(None) => {}
            Err(e) => log(
                LogLevel::Error,
                &format!("License fetch task panicked: {e}"),
            ),
        }
        indicator.update_progress(&format!("fetched {license_count}/{total_licenses}"));
    }

    log(
        LogLevel::Info,
        &format!("Fetched {license_count} licenses from GitHub API"),
    );

    licenses_map
}

/// Static cache for OSI approved licenses
#[cfg(not(test))]
static OSI_LICENSES: OnceLock<HashMap<String, OsiStatus>> = OnceLock::new();

/// Fetch OSI approved licenses from official API (single request, no async needed)
pub fn fetch_osi_licenses() -> FeludaResult<HashMap<String, OsiStatus>> {
    log(LogLevel::Info, "Fetching OSI approved licenses");

    let osi_map = cli::with_spinner("Fetching OSI approved licenses", |indicator| {
        let client = match reqwest::blocking::Client::builder()
            .user_agent("feluda-license-checker/1.0")
            .timeout(Duration::from_secs(30))
            .build()
        {
            Ok(client) => client,
            Err(err) => {
                log_error("Failed to create HTTP client", &err);
                return HashMap::new();
            }
        };

        indicator.update_progress("fetching OSI licenses");

        let response = match client.get("https://api.opensource.org/licenses/").send() {
            Ok(response) => response,
            Err(err) => {
                log_error("Failed to fetch OSI licenses from API", &err);
                return HashMap::new();
            }
        };

        if !response.status().is_success() {
            log(
                LogLevel::Error,
                &format!("OSI API returned error status: {}", response.status()),
            );
            return HashMap::new();
        }

        let osi_licenses: Vec<serde_json::Value> = match response.json() {
            Ok(licenses) => licenses,
            Err(err) => {
                log_error("Failed to parse OSI licenses JSON", &err);
                return HashMap::new();
            }
        };

        let total_licenses = osi_licenses.len();
        indicator.update_progress(&format!("found {total_licenses} OSI licenses"));

        let mut osi_map = HashMap::new();
        for license_data in osi_licenses {
            if let Some(id) = license_data.get("id").and_then(|id| id.as_str()) {
                osi_map.insert(id.to_string(), OsiStatus::Approved);
            }
        }

        indicator.update_progress(&format!("processed {total_licenses} OSI licenses"));
        log(
            LogLevel::Info,
            &format!("Fetched {total_licenses} OSI approved licenses"),
        );

        osi_map
    });

    Ok(osi_map)
}

/// Get the OSI licenses map, loading it if not already cached
fn get_osi_licenses() -> &'static HashMap<String, OsiStatus> {
    #[cfg(not(test))]
    {
        OSI_LICENSES.get_or_init(|| {
            fetch_osi_licenses().unwrap_or_else(|e| {
                log(LogLevel::Warn, &format!("Failed to load OSI licenses: {e}"));
                log(LogLevel::Warn, "Continuing without OSI license information");
                HashMap::new()
            })
        })
    }

    #[cfg(test)]
    {
        use std::cell::RefCell;
        thread_local! {
            static OSI_MAP: RefCell<Option<HashMap<String, OsiStatus>>> = const { RefCell::new(None) };
        }

        OSI_MAP.with(|m| {
            let mut map = m.borrow_mut();
            if map.is_none() {
                match fetch_osi_licenses() {
                    Ok(loaded_map) => {
                        *map = Some(loaded_map);
                    }
                    Err(_) => {
                        *map = Some(HashMap::new());
                    }
                }
            }

            // Leak the memory to get a static reference (only for tests)
            let leaked: &'static HashMap<String, OsiStatus> =
                Box::leak(Box::new(map.as_ref().unwrap().clone()));
            leaked
        })
    }
}

/// Check OSI approval status for a license ID (single, non-compound).
fn get_osi_status_single(license_id: &str) -> OsiStatus {
    let normalized_id = normalize_license_id(license_id);
    let osi_licenses = get_osi_licenses();

    if let Some(status) = osi_licenses.get(&normalized_id) {
        return *status;
    }

    if let Some(status) = osi_licenses.get(license_id) {
        return *status;
    }

    match normalized_id.as_str() {
        "MIT" | "Apache-2.0" | "BSD-3-Clause" | "BSD-2-Clause" | "GPL-3.0" | "GPL-2.0"
        | "LGPL-3.0" | "LGPL-2.1" | "MPL-2.0" | "ISC" | "0BSD" => OsiStatus::Approved,
        "No License" => OsiStatus::NotApproved,
        _ => OsiStatus::Unknown,
    }
}

/// Check OSI approval status for a license string, which may be a compound SPDX expression.
pub fn get_osi_status(license_id: &str) -> OsiStatus {
    if spdx::is_compound(license_id) {
        let expr = spdx::parse(license_id);
        return spdx::expression_osi_status(&expr, &get_osi_status_single);
    }
    get_osi_status_single(license_id)
}

/// Check if a single (non-compound) license ID is restrictive.
fn is_single_license_restrictive(
    license_str: &str,
    known_licenses: &HashMap<String, License>,
    config: &config::FeludaConfig,
    strict: bool,
) -> bool {
    if let Some(license_data) = known_licenses.get(license_str) {
        let conditions = if strict {
            vec![
                "source-disclosure",
                "network-use-disclosure",
                "disclose-source",
                "same-license",
            ]
        } else {
            vec!["source-disclosure", "network-use-disclosure"]
        };
        return conditions
            .iter()
            .any(|&c| license_data.conditions.contains(&c.to_string()));
    }

    let is_restrictive = config
        .licenses
        .restrictive
        .iter()
        .any(|r| license_str.contains(r.as_str()));

    if !is_restrictive && strict && license_str.contains("Unknown") {
        return true;
    }

    is_restrictive
}

/// Check if a license is considered restrictive based on configuration and known licenses.
///
/// Handles compound SPDX expressions:
///   - `OR`  → not restrictive if ANY alternative is permissive.
///   - `AND` → restrictive if ANY component is restrictive.
pub fn is_license_restrictive(
    license: &Option<String>,
    known_licenses: &HashMap<String, License>,
    strict: bool,
) -> bool {
    log(
        LogLevel::Info,
        &format!("Checking if license is restrictive: {license:?} (strict={strict})"),
    );

    let config = match config::load_config() {
        Ok(cfg) => cfg,
        Err(e) => {
            log_error("Error loading configuration", &e);
            config::FeludaConfig::default()
        }
    };

    if license.as_deref() == Some("No License") {
        log(
            LogLevel::Warn,
            "No license specified, considering as restrictive",
        );
        return true;
    }

    if let Some(license_str) = license {
        log_debug(
            "Checking against known licenses",
            &known_licenses.keys().collect::<Vec<_>>(),
        );

        if spdx::is_compound(license_str) {
            let expr = spdx::parse(license_str);
            let result = spdx::expression_is_restrictive(&expr, &|id| {
                is_single_license_restrictive(id, known_licenses, &config, strict)
            });
            log(
                LogLevel::Info,
                &format!("Compound expression '{license_str}' is_restrictive={result}"),
            );
            return result;
        }

        let result = is_single_license_restrictive(license_str, known_licenses, &config, strict);
        if result {
            log(
                LogLevel::Warn,
                &format!("License {license_str} is restrictive"),
            );
        } else {
            log(
                LogLevel::Info,
                &format!("License {license_str} is not restrictive"),
            );
        }
        return result;
    }

    if strict {
        log(
            LogLevel::Warn,
            "No license information available in strict mode, considering restrictive",
        );
        return true;
    }

    log(LogLevel::Warn, "No license information available");
    false
}

/// Check if a license should be ignored from analysis
///
/// Returns true if the license is in the ignore list configured in `.feluda.toml`
/// or via `FELUDA_LICENSES_IGNORE` environment variable.
pub fn is_license_ignored(license: Option<&str>) -> bool {
    log(
        LogLevel::Info,
        &format!("Checking if license should be ignored: {license:?}"),
    );

    let config = match config::load_config() {
        Ok(cfg) => {
            log(LogLevel::Info, "Successfully loaded configuration");
            cfg
        }
        Err(e) => {
            log_error("Error loading configuration", &e);
            log(LogLevel::Warn, "Using default configuration");
            config::FeludaConfig::default()
        }
    };

    if let Some(license_str) = license {
        let is_ignored = config
            .licenses
            .ignore
            .iter()
            .any(|ignore_license| license_str.contains(ignore_license));

        if is_ignored {
            log(
                LogLevel::Info,
                &format!("License {license_str} matches ignore pattern in config"),
            );
        } else {
            log(
                LogLevel::Info,
                &format!("License {license_str} does not match any ignore pattern"),
            );
        }

        return is_ignored;
    }

    log(LogLevel::Info, "No license specified, not ignoring");
    false
}

/// This is the default configuration
const EMBEDDED_LICENSE_COMPATIBILITY_TOML: &str =
    include_str!("../config/license_compatibility.toml");

/// Load license compatibility matrix from external TOML file if available
/// Looks for the file in the following order:
/// 1. .feluda/license_compatibility.toml (user-specific config directory)
/// 2. Embedded configuration
fn load_compatibility_matrix() -> FeludaResult<HashMap<String, Vec<String>>> {
    log(
        LogLevel::Info,
        "Loading license compatibility matrix from TOML file",
    );

    // Only check for user-specific config in .feluda directory
    let config_paths = vec![Path::new(".feluda/license_compatibility.toml").to_path_buf()];

    let mut config_content = None;
    let mut used_path = None;

    for path in &config_paths {
        if path.exists() {
            log(
                LogLevel::Info,
                &format!("Found license compatibility config at: {}", path.display()),
            );
            match fs::read_to_string(path) {
                Ok(content) => {
                    config_content = Some(content);
                    used_path = Some(path);
                    break;
                }
                Err(e) => {
                    log(
                        LogLevel::Warn,
                        &format!("Failed to read {}: {}", path.display(), e),
                    );
                    continue;
                }
            }
        }
    }

    // Use embedded configuration as fallback if no external file is found
    let config_content = match config_content {
        Some(content) => content,
        None => {
            log(
                LogLevel::Info,
                "No external license compatibility config found, using embedded configuration",
            );
            EMBEDDED_LICENSE_COMPATIBILITY_TOML.to_string()
        }
    };

    let matrix: LicenseCompatibilityMatrix = toml::from_str(&config_content).map_err(|e| {
        let source = match &used_path {
            Some(path) => format!("external config file ({})", path.display()),
            None => "embedded configuration".to_string(),
        };
        log(
            LogLevel::Error,
            &format!("Failed to parse license compatibility {source}: {e}"),
        );
        std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())
    })?;

    // Convert TOML structure to HashMap
    let entries = [
        ("MIT", &matrix.mit),
        ("Apache-2.0", &matrix.apache_2_0),
        ("GPL-3.0", &matrix.gpl_3_0),
        ("GPL-2.0", &matrix.gpl_2_0),
        ("AGPL-3.0", &matrix.agpl_3_0),
        ("LGPL-3.0", &matrix.lgpl_3_0),
        ("LGPL-2.1", &matrix.lgpl_2_1),
        ("MPL-2.0", &matrix.mpl_2_0),
        ("BSD-3-Clause", &matrix.bsd_3_clause),
        ("BSD-2-Clause", &matrix.bsd_2_clause),
        ("ISC", &matrix.isc),
        ("0BSD", &matrix.bsd_0),
        ("Unlicense", &matrix.unlicense),
        ("WTFPL", &matrix.wtfpl),
    ];

    let result: HashMap<String, Vec<String>> = entries
        .iter()
        .filter_map(|(key, option_entry)| {
            option_entry
                .as_ref()
                .map(|entry| (key.to_string(), entry.compatible_with.clone()))
        })
        .collect();

    log(
        LogLevel::Info,
        &format!("Loaded {} license compatibility entries", result.len()),
    );
    Ok(result)
}

/// Get the compatibility matrix, loading it if not already cached
fn get_compatibility_matrix() -> &'static HashMap<String, Vec<String>> {
    #[cfg(not(test))]
    {
        COMPATIBILITY_MATRIX.get_or_init(|| {
            load_compatibility_matrix().unwrap_or_else(|e| {
                log(LogLevel::Error, &format!("Failed to load license compatibility matrix: {e}"));
                log(LogLevel::Error, "This is a critical error. The application cannot function without license compatibility data.");
                std::process::exit(1);
            })
        })
    }

    #[cfg(test)]
    {
        // For tests, use a thread-local storage to avoid the OnceLock static initialization issue
        use std::cell::RefCell;
        thread_local! {
            static MATRIX: RefCell<Option<HashMap<String, Vec<String>>>> = const { RefCell::new(None) };
        }

        // This is a hack to return a static reference from thread-local storage
        // We leak the memory in tests, which is acceptable for testing
        MATRIX.with(|m| {
            let mut matrix = m.borrow_mut();
            if matrix.is_none() {
                match load_compatibility_matrix() {
                    Ok(loaded_matrix) => {
                        *matrix = Some(loaded_matrix);
                    }
                    Err(e) => {
                        panic!(
                            "License compatibility configuration file not found during testing: {e}"
                        );
                    }
                }
            }

            // Leak the memory to get a static reference (only for tests)
            let leaked: &'static HashMap<String, Vec<String>> =
                Box::leak(Box::new(matrix.as_ref().unwrap().clone()));
            leaked
        })
    }
}

/// Check if a single (non-compound) dependency license ID is compatible with the project license.
fn is_single_license_compatible(
    dependency_license: &str,
    project_license: &str,
    strict: bool,
) -> LicenseCompatibility {
    let compatibility_matrix = get_compatibility_matrix();
    let norm_dep = normalize_license_id(dependency_license);
    let norm_proj = normalize_license_id(project_license);

    match compatibility_matrix.get(&norm_proj) {
        Some(compatible_licenses) => {
            if compatible_licenses.contains(&norm_dep) {
                LicenseCompatibility::Compatible
            } else {
                LicenseCompatibility::Incompatible
            }
        }
        None => {
            if strict {
                LicenseCompatibility::Incompatible
            } else {
                LicenseCompatibility::Unknown
            }
        }
    }
}

/// Check if a license is compatible with the base project license.
///
/// Handles compound SPDX expressions in `dependency_license`:
///   - `OR`  → compatible if ANY alternative is compatible with the project license.
///   - `AND` → compatible only if ALL components are compatible.
pub fn is_license_compatible(
    dependency_license: &str,
    project_license: &str,
    strict: bool,
) -> LicenseCompatibility {
    log(
        LogLevel::Info,
        &format!(
            "Checking compatibility: dependency={dependency_license} project={project_license} strict={strict}"
        ),
    );

    if spdx::is_compound(dependency_license) {
        let expr = spdx::parse(dependency_license);
        let result =
            spdx::expression_compatibility(&expr, project_license, strict, &|dep, proj, s| {
                is_single_license_compatible(dep, proj, s)
            });
        log(
            LogLevel::Info,
            &format!("Compound expression '{dependency_license}' compatibility={result}"),
        );
        return result;
    }

    let result = is_single_license_compatible(dependency_license, project_license, strict);
    log(
        LogLevel::Info,
        &format!("License {dependency_license} compatibility with {project_license}: {result}"),
    );
    result
}

/// Normalize license identifier to a standard format
fn normalize_license_id(license_id: &str) -> String {
    let trimmed = license_id.trim().to_uppercase();

    // Handle common variations and aliases
    match trimmed.as_str() {
        "MIT" | "MIT LICENSE" => "MIT".to_string(),
        "ISC" | "ISC LICENSE" => "ISC".to_string(),
        "0BSD" | "BSD-ZERO-CLAUSE" | "BSD ZERO CLAUSE" => "0BSD".to_string(),
        "UNLICENSE" | "THE UNLICENSE" => "Unlicense".to_string(),
        "WTFPL" | "DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE" => "WTFPL".to_string(),
        "ZLIB" | "ZLIB LICENSE" => "Zlib".to_string(),
        "CC0" | "CC0-1.0" | "CC0 1.0" | "CREATIVE COMMONS ZERO" => "CC0-1.0".to_string(),

        id if id.contains("APACHE") && (id.contains("2.0") || id.contains("2")) => {
            "Apache-2.0".to_string()
        }

        id if id.contains("AGPL") && id.contains("3") => "AGPL-3.0".to_string(),
        id if id.contains("AFFERO") && id.contains("GPL") && id.contains("3") => {
            "AGPL-3.0".to_string()
        }

        id if id.contains("GPL") && id.contains("3") && !id.contains("LGPL") => {
            "GPL-3.0".to_string()
        }
        id if id.contains("GPL") && id.contains("2") && !id.contains("LGPL") => {
            "GPL-2.0".to_string()
        }

        id if id.contains("LGPL") && id.contains("3") => "LGPL-3.0".to_string(),
        id if id.contains("LGPL") && id.contains("2.1") => "LGPL-2.1".to_string(),
        id if id.contains("LGPL") && id.contains("2") && !id.contains("2.1") => {
            "LGPL-2.1".to_string()
        }

        id if id.contains("MPL") && id.contains("2.0") => "MPL-2.0".to_string(),

        id if id.contains("BSD") && (id.contains("3") || id.contains("THREE")) => {
            "BSD-3-Clause".to_string()
        }
        id if id.contains("BSD") && (id.contains("2") || id.contains("TWO")) => {
            "BSD-2-Clause".to_string()
        }

        _ => license_id.to_string(),
    }
}

/// A well-known license filename.
///
/// `implied_spdx` is `Some` when the filename alone is sufficient to identify the license
/// (e.g. `OFL.txt` → `OFL-1.1`).  Generic filenames like `LICENSE` leave it `None` and fall
/// through to content-based detection.
struct LicenseFilename {
    filename: &'static str,
    implied_spdx: Option<&'static str>,
}

/// Content-based detection rule.
///
/// The rule fires when **any** marker group matches, where a group matches when
/// **all** of its strings appear in the file content (OR-of-ANDs).  List more-specific
/// rules (more markers) before less-specific ones so the first match wins.
struct LicenseContentRule {
    spdx_id: &'static str,
    marker_groups: &'static [&'static [&'static str]],
}

/// Canonical set of license filenames to probe, in priority order.
///
/// To add support for a new license filename:
///   • Append a `LicenseFilename` entry here.
///   • If the filename unambiguously identifies the license, set `implied_spdx`.
///   • Otherwise leave `implied_spdx: None`; content rules below will handle detection.
static LICENSE_FILENAMES: &[LicenseFilename] = &[
    LicenseFilename {
        filename: "LICENSE",
        implied_spdx: None,
    },
    LicenseFilename {
        filename: "LICENSE.txt",
        implied_spdx: None,
    },
    LicenseFilename {
        filename: "LICENSE.md",
        implied_spdx: None,
    },
    LicenseFilename {
        filename: "license",
        implied_spdx: None,
    },
    LicenseFilename {
        filename: "COPYING",
        implied_spdx: None,
    },
    // OFL.txt is the conventional license file for font projects (e.g. Google Fonts).
    LicenseFilename {
        filename: "OFL.txt",
        implied_spdx: Some("OFL-1.1"),
    },
    LicenseFilename {
        filename: "OFL-1.1.txt",
        implied_spdx: Some("OFL-1.1"),
    },
];

/// Content-matching rules applied when a filename has no implied SPDX ID.
///
/// To add a new license:
///   • Add a `LicenseContentRule` with the SPDX identifier and one or more marker groups.
///   • Each group is a set of strings that must ALL appear in the file (logical AND).
///   • Multiple groups are tried in order; the first matching group wins (logical OR).
///   • Place more-specific rules (more markers) before less-specific ones.
static LICENSE_CONTENT_RULES: &[LicenseContentRule] = &[
    // GPL family must be ordered specific → general: AGPL and LGPL texts both
    // contain the "GNU GENERAL PUBLIC LICENSE" substring (via reference to GPL),
    // so they would otherwise match the GPL rules first.
    LicenseContentRule {
        spdx_id: "AGPL-3.0",
        marker_groups: &[&["GNU AFFERO GENERAL PUBLIC LICENSE", "Version 3"]],
    },
    LicenseContentRule {
        spdx_id: "LGPL-3.0",
        marker_groups: &[&["GNU LESSER GENERAL PUBLIC LICENSE", "Version 3"]],
    },
    LicenseContentRule {
        spdx_id: "LGPL-2.1",
        marker_groups: &[&["GNU LESSER GENERAL PUBLIC LICENSE", "Version 2.1"]],
    },
    LicenseContentRule {
        spdx_id: "GPL-3.0",
        marker_groups: &[&["GNU GENERAL PUBLIC LICENSE", "Version 3"]],
    },
    LicenseContentRule {
        spdx_id: "GPL-2.0",
        marker_groups: &[&["GNU GENERAL PUBLIC LICENSE", "Version 2"]],
    },
    LicenseContentRule {
        spdx_id: "Apache-2.0",
        marker_groups: &[&["Apache License", "Version 2.0"]],
    },
    LicenseContentRule {
        spdx_id: "MPL-2.0",
        marker_groups: &[&["Mozilla Public License", "Version 2.0"]],
    },
    // BSD-3 must come before BSD-2: "Neither the name" distinguishes them.
    LicenseContentRule {
        spdx_id: "BSD-3-Clause",
        marker_groups: &[&["BSD", "Redistribution and use", "Neither the name"]],
    },
    LicenseContentRule {
        spdx_id: "BSD-2-Clause",
        marker_groups: &[&["BSD", "Redistribution and use"]],
    },
    // OFL must come before MIT: the OFL grant text begins with "Permission is hereby
    // granted, free of charge" (the same opening as MIT), so OFL files would otherwise
    // match the MIT rule first.
    LicenseContentRule {
        spdx_id: "OFL-1.1",
        marker_groups: &[
            &["SIL OPEN FONT LICENSE"],
            &["This Font Software is licensed under the SIL Open Font License"],
        ],
    },
    LicenseContentRule {
        spdx_id: "MIT",
        marker_groups: &[
            &["MIT License"],
            // "associated documentation files" is MIT-specific phrasing — it
            // disambiguates from OFL/ISC/etc. which share the permission preamble.
            &[
                "Permission is hereby granted, free of charge",
                "associated documentation files",
            ],
        ],
    },
    LicenseContentRule {
        spdx_id: "ISC",
        marker_groups: &[&["ISC License"]],
    },
];

/// Return the SPDX ID for the first content rule that matches `content`, or `None`.
fn match_license_content(content: &str) -> Option<&'static str> {
    for rule in LICENSE_CONTENT_RULES {
        for group in rule.marker_groups {
            if group.iter().all(|marker| content.contains(marker)) {
                return Some(rule.spdx_id);
            }
        }
    }
    None
}

/// Detect the project's license
pub fn detect_project_license(project_path: &str) -> FeludaResult<Option<String>> {
    log(
        LogLevel::Info,
        &format!("Detecting license for project at path: {project_path}"),
    );

    for entry in LICENSE_FILENAMES {
        let license_path = Path::new(project_path).join(entry.filename);
        if !license_path.exists() {
            continue;
        }

        log(
            LogLevel::Info,
            &format!("Found license file: {}", license_path.display()),
        );

        // Filename alone is sufficient (e.g. OFL.txt → OFL-1.1).
        if let Some(spdx) = entry.implied_spdx {
            log(
                LogLevel::Info,
                &format!("Detected {spdx} license from filename"),
            );
            return Ok(Some(spdx.to_string()));
        }

        match fs::read_to_string(&license_path) {
            Ok(content) => {
                if let Some(spdx) = match_license_content(&content) {
                    log(LogLevel::Info, &format!("Detected {spdx} license"));
                    return Ok(Some(spdx.to_string()));
                }
                log(
                    LogLevel::Warn,
                    "License file found but could not determine license type",
                );
            }
            Err(err) => {
                log(
                    LogLevel::Error,
                    &format!("Failed to read license file: {}", license_path.display()),
                );
                log_debug("Error details", &err);
            }
        }
    }

    // Check package.json for Node.js projects
    let package_json_path = Path::new(project_path).join("package.json");
    if package_json_path.exists() {
        log(
            LogLevel::Info,
            &format!("Found package.json at {}", package_json_path.display()),
        );

        match fs::read_to_string(&package_json_path) {
            Ok(content) => match serde_json::from_str::<Value>(&content) {
                Ok(json) => {
                    if let Some(license) = json.get("license").and_then(|l| l.as_str()) {
                        log(
                            LogLevel::Info,
                            &format!("Detected license from package.json: {license}"),
                        );
                        return Ok(Some(license.to_string()));
                    }
                }
                Err(err) => {
                    log(
                        LogLevel::Error,
                        &format!("Failed to parse package.json: {err}"),
                    );
                }
            },
            Err(err) => {
                log(
                    LogLevel::Error,
                    &format!(
                        "Failed to read package.json: {}",
                        package_json_path.display()
                    ),
                );
                log_debug("Error details", &err);
            }
        }
    }

    // Check Cargo.toml for Rust projects
    let cargo_toml_path = Path::new(project_path).join("Cargo.toml");
    if cargo_toml_path.exists() {
        log(
            LogLevel::Info,
            &format!("Found Cargo.toml at {}", cargo_toml_path.display()),
        );

        match fs::read_to_string(&cargo_toml_path) {
            Ok(content) => match toml::from_str::<TomlValue>(&content) {
                Ok(toml) => {
                    if let Some(package) = toml.as_table().and_then(|t| t.get("package")) {
                        if let Some(license) = package.get("license").and_then(|l| l.as_str()) {
                            log(
                                LogLevel::Info,
                                &format!("Detected license from Cargo.toml: {license}"),
                            );
                            return Ok(Some(license.to_string()));
                        }
                    }
                }
                Err(err) => {
                    log(
                        LogLevel::Error,
                        &format!("Failed to parse Cargo.toml: {err}"),
                    );
                }
            },
            Err(err) => {
                log(
                    LogLevel::Error,
                    &format!("Failed to read Cargo.toml: {}", cargo_toml_path.display()),
                );
                log_debug("Error details", &err);
            }
        }
    }

    // Check pyproject.toml for Python projects
    let pyproject_toml_path = Path::new(project_path).join("pyproject.toml");
    if pyproject_toml_path.exists() {
        log(
            LogLevel::Info,
            &format!("Found pyproject.toml at {}", pyproject_toml_path.display()),
        );

        match fs::read_to_string(&pyproject_toml_path) {
            Ok(content) => match toml::from_str::<TomlValue>(&content) {
                Ok(toml) => {
                    if let Some(project) = toml.as_table().and_then(|t| t.get("project")) {
                        if let Some(license_info) = project.get("license") {
                            if let Some(license) = license_info.as_str() {
                                log(
                                    LogLevel::Info,
                                    &format!("Detected license from pyproject.toml: {license}"),
                                );
                                return Ok(Some(license.to_string()));
                            } else if let Some(license_table) = license_info.as_table() {
                                if let Some(license_text) =
                                    license_table.get("text").and_then(|t| t.as_str())
                                {
                                    log(
                                        LogLevel::Info,
                                        &format!(
                                            "Detected license from pyproject.toml: {license_text}"
                                        ),
                                    );
                                    return Ok(Some(license_text.to_string()));
                                }
                            }
                        }
                    }
                }
                Err(err) => {
                    log(
                        LogLevel::Error,
                        &format!("Failed to parse pyproject.toml: {err}"),
                    );
                }
            },
            Err(err) => {
                log(
                    LogLevel::Error,
                    &format!(
                        "Failed to read pyproject.toml: {}",
                        pyproject_toml_path.display()
                    ),
                );
                log_debug("Error details", &err);
            }
        }
    }

    log(LogLevel::Warn, "No license detected for project");
    Ok(None)
}

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

    #[test]
    fn test_license_compatibility_display() {
        assert_eq!(LicenseCompatibility::Compatible.to_string(), "Compatible");
        assert_eq!(
            LicenseCompatibility::Incompatible.to_string(),
            "Incompatible"
        );
        assert_eq!(LicenseCompatibility::Unknown.to_string(), "Unknown");
    }

    #[test]
    fn test_license_info_methods() {
        let info = LicenseInfo {
            name: "test_package".to_string(),
            version: "1.0.0".to_string(),
            license: Some("MIT".to_string()),
            is_restrictive: false,
            compatibility: LicenseCompatibility::Compatible,
            osi_status: OsiStatus::Approved,
            sub_project: None,
        };

        assert_eq!(info.name(), "test_package");
        assert_eq!(info.version(), "1.0.0");
        assert_eq!(info.get_license(), "MIT");
        assert!(!info.is_restrictive());
        assert_eq!(info.compatibility(), &LicenseCompatibility::Compatible);
    }

    #[test]
    fn test_license_info_no_license() {
        let info = LicenseInfo {
            name: "test_package".to_string(),
            version: "1.0.0".to_string(),
            license: None,
            is_restrictive: true,
            compatibility: LicenseCompatibility::Unknown,
            osi_status: OsiStatus::Unknown,
            sub_project: None,
        };

        assert_eq!(info.get_license(), "No License");
    }

    #[test]
    fn test_normalize_license_id() {
        assert_eq!(normalize_license_id("MIT"), "MIT");
        assert_eq!(normalize_license_id("mit"), "MIT");
        assert_eq!(normalize_license_id("Apache 2.0"), "Apache-2.0");
        assert_eq!(normalize_license_id("APACHE-2.0"), "Apache-2.0");
        assert_eq!(normalize_license_id("GPL 3.0"), "GPL-3.0");
        assert_eq!(normalize_license_id("gpl-3.0"), "GPL-3.0");
        assert_eq!(normalize_license_id("LGPL 3.0"), "LGPL-3.0");
        assert_eq!(normalize_license_id("MPL 2.0"), "MPL-2.0");
        assert_eq!(normalize_license_id("BSD 3-Clause"), "BSD-3-Clause");
        assert_eq!(normalize_license_id("BSD 2-Clause"), "BSD-2-Clause");
        assert_eq!(normalize_license_id("Unknown License"), "Unknown License");
        assert_eq!(normalize_license_id("  MIT  "), "MIT");
    }

    #[test]
    #[ignore] // Skip this test due to static initialization issues in test runner
    fn test_is_license_compatible_mit_project() {
        assert_eq!(
            is_license_compatible("MIT", "MIT", false),
            LicenseCompatibility::Compatible
        );
        assert_eq!(
            is_license_compatible("BSD-2-Clause", "MIT", false),
            LicenseCompatibility::Compatible
        );
        assert_eq!(
            is_license_compatible("BSD-3-Clause", "MIT", false),
            LicenseCompatibility::Compatible
        );
        assert_eq!(
            is_license_compatible("Apache-2.0", "MIT", false),
            LicenseCompatibility::Compatible
        );
        assert_eq!(
            is_license_compatible("LGPL-3.0", "MIT", false),
            LicenseCompatibility::Incompatible
        );
        assert_eq!(
            is_license_compatible("MPL-2.0", "MIT", false),
            LicenseCompatibility::Incompatible
        );
        assert_eq!(
            is_license_compatible("GPL-3.0", "MIT", false),
            LicenseCompatibility::Incompatible
        );
    }

    #[test]
    fn test_detect_project_license_mit_file() {
        let temp_dir = TempDir::new().unwrap();
        let license_path = temp_dir.path().join("LICENSE");

        std::fs::write(
            &license_path,
            "MIT License\n\nPermission is hereby granted, free of charge...",
        )
        .unwrap();

        let result = detect_project_license(temp_dir.path().to_str().unwrap()).unwrap();
        assert_eq!(result, Some("MIT".to_string()));
    }

    #[test]
    fn test_detect_project_license_ofl_filename() {
        // OFL.txt is the canonical font license file; filename alone should suffice.
        let temp_dir = TempDir::new().unwrap();
        std::fs::write(temp_dir.path().join("OFL.txt"), "some font license text").unwrap();
        let result = detect_project_license(temp_dir.path().to_str().unwrap()).unwrap();
        assert_eq!(result, Some("OFL-1.1".to_string()));
    }

    #[test]
    fn test_detect_project_license_ofl_content_in_license_file() {
        // When only a generic LICENSE file is present, content-based detection must fire.
        let temp_dir = TempDir::new().unwrap();
        std::fs::write(
            temp_dir.path().join("LICENSE"),
            "SIL OPEN FONT LICENSE\nVersion 1.1",
        )
        .unwrap();
        let result = detect_project_license(temp_dir.path().to_str().unwrap()).unwrap();
        assert_eq!(result, Some("OFL-1.1".to_string()));
    }

    #[test]
    fn test_detect_project_license_lgpl_not_misidentified_as_gpl() {
        // Regression: LGPL text contains "GNU GENERAL PUBLIC LICENSE" via reference,
        // so without correct rule ordering it would be misidentified as GPL-3.0.
        let temp_dir = TempDir::new().unwrap();
        std::fs::write(
            temp_dir.path().join("LICENSE"),
            "GNU LESSER GENERAL PUBLIC LICENSE\nVersion 3\n\
             This version of the GNU Lesser General Public License incorporates \
             the terms and conditions of version 3 of the GNU General Public License.",
        )
        .unwrap();
        let result = detect_project_license(temp_dir.path().to_str().unwrap()).unwrap();
        assert_eq!(result, Some("LGPL-3.0".to_string()));
    }

    #[test]
    fn test_detect_project_license_ofl_in_license_not_misidentified_as_mit() {
        // Regression: OFL grant text starts with the same "Permission is hereby granted,
        // free of charge" preamble as MIT, so without correct rule ordering it would be
        // misidentified as MIT.
        let temp_dir = TempDir::new().unwrap();
        std::fs::write(
            temp_dir.path().join("LICENSE"),
            "SIL OPEN FONT LICENSE Version 1.1\n\n\
             Permission is hereby granted, free of charge, to any person obtaining \
             a copy of the Font Software, to use, study, copy, merge, embed, modify...",
        )
        .unwrap();
        let result = detect_project_license(temp_dir.path().to_str().unwrap()).unwrap();
        assert_eq!(result, Some("OFL-1.1".to_string()));
    }

    #[test]
    fn test_detect_project_license_no_license() {
        let temp_dir = TempDir::new().unwrap();

        let result = detect_project_license(temp_dir.path().to_str().unwrap()).unwrap();
        assert_eq!(result, None);
    }

    #[test]
    fn test_is_license_ignored_with_no_license() {
        // Should return false when no license is provided
        assert!(!is_license_ignored(None));
    }

    #[test]
    fn test_is_license_ignored_not_in_ignore_list() {
        // License not in ignore list should return false
        // This test assumes no ignore list is configured
        let result = is_license_ignored(Some("GPL-3.0"));
        // Since we can't easily mock the config in this context,
        // we just verify it doesn't panic
        let _ = result;
    }

    #[test]
    fn test_is_license_ignored_empty_license() {
        // Empty string should return false
        assert!(!is_license_ignored(Some("")));
    }
}
