use crate::colors::get_colors;
use crate::config::get_config;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::process::exit;

#[derive(Serialize, Deserialize, Debug)]
pub struct PackageData {
    #[serde(rename = "PRGNAM")]
    pub prgnam: String,
    #[serde(rename = "VERSION")]
    pub version: String,
    #[serde(rename = "HOMEPAGE")]
    pub homepage: String,
    #[serde(rename = "DOWNLOAD")]
    pub download: String,
    #[serde(rename = "MD5SUM")]
    pub md5sum: String,
    #[serde(rename = "DOWNLOAD_x86_64")]
    pub download_x86_64: String,
    #[serde(rename = "MD5SUM_x86_64")]
    pub md5sum_x86_64: String,
    #[serde(rename = "REQUIRES")]
    pub requires: String,
    #[serde(rename = "MAINTAINER")]
    pub maintainer: String,
    #[serde(rename = "EMAIL")]
    pub email: String,
    #[serde(rename = "CATEGORY")]
    pub category: String,
    #[serde(rename = "BUILD")]
    pub build: String,
    #[serde(rename = "TAG")]
    pub tag: String,
    #[serde(rename = "PKGTYPE")]
    pub pkgtype: String,
    #[serde(rename = "ARCH")]
    pub arch: String,
    #[serde(rename = "PACKAGE")]
    pub package: String,
}

/// Parses the contents of a `.info` file into a key-value map.
///
/// Each line is expected to have the form `KEY="value"`. Lines that do not
/// contain `=` are silently skipped.
///
/// # Example
///
/// ```
/// // KEY="hello" -> {"KEY": "hello"}
/// ```
fn parse_info(content: &str) -> HashMap<String, String> {
    let mut map = HashMap::new();
    let mut current_key = String::new();
    let mut current_val = String::new();

    for line in content.lines() {
        let is_new_key = !line.starts_with(' ') && !line.starts_with('\t') && line.contains('=');

        if is_new_key {
            if !current_key.is_empty() {
                map.insert(current_key.clone(), current_val.trim().to_string());
            }
            if let Some((key, val)) = line.split_once('=') {
                current_key = key.trim().to_string();
                current_val = val
                    .trim()
                    .trim_matches('"')
                    .trim_end_matches('\\')
                    .trim()
                    .to_string();
            }
        } else if !line.trim().is_empty() {
            let addition = line.trim().trim_matches('"').trim_end_matches('\\').trim();
            if !addition.is_empty() {
                current_val.push(' ');
                current_val.push_str(addition);
            }
        }
    }

    if !current_key.is_empty() {
        map.insert(current_key, current_val.trim().to_string());
    }

    map
}

/// Extracts the value of a shell variable from a `.SlackBuild` script.
///
/// Looks for a line starting with `VAR=` and returns the assigned value.
/// If the value uses `${VAR:-default}` syntax, the default is returned.
/// Returns an empty string if the variable is not found.
///
/// # Example
///
/// ```
/// // BUILD="1" -> "1"
/// // ARCH=${ARCH:-x86_64} -> "x86_64"
/// ```
fn extract_slackbuild_var(content: &str, var: &str) -> String {
    for line in content.lines() {
        if line.starts_with(&format!("{var}=")) {
            let val = line[var.len() + 1..].trim().trim_matches('"');
            if val.starts_with("${") {
                if let Some(default) = val.split(":-").nth(1) {
                    return default.trim_end_matches('}').to_string();
                }
            }
            return val.to_string();
        }
    }
    String::new()
}

/// Walks the local `SlackBuilds` repository and writes a `data.json` index file.
///
/// For every package directory found under each category, the corresponding
/// `.info` and `.SlackBuild` files are read and combined into a [`PackageData`]
/// struct. The result is serialised as pretty-printed JSON and saved next to
/// the repository root.
///
/// # Exits
///
/// Exits with code 1 if the JSON cannot be serialised or the output file cannot be written.
pub fn parse_repo() {
    let cfg = get_config();
    let color = get_colors(cfg.colors);

    let mut data: HashMap<String, HashMap<String, PackageData>> = HashMap::new();

    let categories = match fs::read_dir(&cfg.repo_path) {
        Ok(directory) => directory,
        Err(e) => {
            eprintln!("{}Error:{} reading repo: {}", color.red, color.reset, e);
            return;
        }
    };

    for category_entry in categories.flatten() {
        let cat_path = category_entry.path();
        if !cat_path.is_dir() {
            continue;
        }

        let category_name = category_entry.file_name().to_string_lossy().to_string();
        let mut packages: HashMap<String, PackageData> = HashMap::new();

        let Ok(pkg_entries) = fs::read_dir(&cat_path) else {
            continue;
        };

        for pkg_entry in pkg_entries.flatten() {
            let pkg_path = pkg_entry.path();
            if !pkg_path.is_dir() {
                continue;
            }

            let pkg_name = pkg_entry.file_name().to_string_lossy().to_string();
            let info_path = pkg_path.join(format!("{pkg_name}.info"));
            let slackbuild_path = pkg_path.join(format!("{pkg_name}.SlackBuild"));

            if !info_path.exists() || !slackbuild_path.exists() {
                continue;
            }

            let info_content = fs::read_to_string(&info_path).unwrap_or_default();
            let sb_content = fs::read_to_string(&slackbuild_path).unwrap_or_default();
            let info = parse_info(&info_content);

            packages.insert(
                pkg_name,
                PackageData {
                    prgnam: info.get("PRGNAM").cloned().unwrap_or_default(),
                    version: info.get("VERSION").cloned().unwrap_or_default(),
                    homepage: info.get("HOMEPAGE").cloned().unwrap_or_default(),
                    download: info.get("DOWNLOAD").cloned().unwrap_or_default(),
                    md5sum: info.get("MD5SUM").cloned().unwrap_or_default(),
                    download_x86_64: info.get("DOWNLOAD_x86_64").cloned().unwrap_or_default(),
                    md5sum_x86_64: info.get("MD5SUM_x86_64").cloned().unwrap_or_default(),
                    requires: info.get("REQUIRES").cloned().unwrap_or_default(),
                    maintainer: info.get("MAINTAINER").cloned().unwrap_or_default(),
                    email: info.get("EMAIL").cloned().unwrap_or_default(),
                    category: category_name.clone(),
                    build: extract_slackbuild_var(&sb_content, "BUILD"),
                    tag: extract_slackbuild_var(&sb_content, "TAG"),
                    pkgtype: extract_slackbuild_var(&sb_content, "PKGTYPE"),
                    arch: {
                        let a = extract_slackbuild_var(&sb_content, "ARCH");
                        if a == "noarch" {
                            a
                        } else {
                            cfg.arch.clone()
                        }
                    },
                    package: {
                        let a = extract_slackbuild_var(&sb_content, "ARCH");
                        let arch = if a == "noarch" { &a } else { &cfg.arch };
                        format!(
                            "{}-{}-{}-{}{}",
                            info.get("PRGNAM").cloned().unwrap_or_default(),
                            info.get("VERSION").cloned().unwrap_or_default(),
                            arch,
                            extract_slackbuild_var(&sb_content, "BUILD"),
                            extract_slackbuild_var(&sb_content, "TAG"),
                        )
                    },
                },
            );
        }

        if !packages.is_empty() {
            data.insert(category_name, packages);
        }
    }

    let json = serde_json::to_string_pretty(&data).unwrap_or_else(|e| {
        eprintln!(
            "{}Error:{} Failed to serialize: {}",
            color.red, color.reset, e
        );
        exit(1);
    });

    let out_path = Path::new(&cfg.repo_path).join(&cfg.data_json);
    fs::write(&out_path, json).unwrap_or_else(|e| {
        eprintln!(
            "{}Error:{} Failed to write {}: {}",
            color.red, color.reset, cfg.data_json, e
        );
        exit(1);
    });

    println!("Parsed {} categories → {}", data.len(), out_path.display());
}

/// Unit tests for .info file parsing and SlackBuild variable extraction.
#[cfg(test)]
mod tests {
    use super::*;

    /// Simple KEY="value" line is parsed correctly.
    #[test]
    fn test_parse_info_simple() {
        let content = r#"PRGNAM="ffmpeg"
VERSION="6.0""#;
        let map = parse_info(content);
        assert_eq!(map.get("PRGNAM").unwrap(), "ffmpeg");
        assert_eq!(map.get("VERSION").unwrap(), "6.0");
    }

    /// Multi-line value (continuation with backslash) is joined correctly.
    #[test]
    fn test_parse_info_multiline() {
        let content =
            "DOWNLOAD=\"https://example.com/a.tar.gz \\\n  https://example.com/b.tar.gz\"";
        let map = parse_info(content);
        let dl = map.get("DOWNLOAD").unwrap();
        assert!(dl.contains("a.tar.gz"));
        assert!(dl.contains("b.tar.gz"));
    }

    /// Lines without `=` are silently skipped.
    #[test]
    fn test_parse_info_skips_invalid_lines() {
        let content = "not a key value line\nPRGNAM=\"foo\"";
        let map = parse_info(content);
        assert_eq!(map.get("PRGNAM").unwrap(), "foo");
        assert!(!map.contains_key("not a key value line"));
    }

    /// Simple quoted variable is extracted correctly.
    #[test]
    fn test_extract_slackbuild_var_quoted() {
        let content = "BUILD=\"2\"\nTAG=\"_SBo\"";
        assert_eq!(extract_slackbuild_var(content, "BUILD"), "2");
        assert_eq!(extract_slackbuild_var(content, "TAG"), "_SBo");
    }

    /// Default syntax `${VAR:-default}` returns the default value.
    #[test]
    fn test_extract_slackbuild_var_default_syntax() {
        let content = "ARCH=${ARCH:-x86_64}";
        assert_eq!(extract_slackbuild_var(content, "ARCH"), "x86_64");
    }

    /// Missing variable returns empty string.
    #[test]
    fn test_extract_slackbuild_var_missing() {
        let content = "BUILD=\"1\"";
        assert_eq!(extract_slackbuild_var(content, "TAG"), "");
    }
}
