use crate::colors::get_colors;
use crate::config::get_config;
use crate::options::Options;
use crate::parse::parse_repo;
use crate::utils::{confirm, run_command};
use std::fs;
use std::io;
use std::io::Write;
use std::process::{exit, Command};

/// Syncs the `SlackBuilds` repository from the configured mirror and rebuilds
/// the package index via [`crate::parse::parse_repo`].
///
/// The sync method is detected automatically from the mirror URL: URLs ending
/// in `.git` use `git clone`, all others use `rsync`.
///
/// Creates the local repository directory if it does not already exist.
///
/// # Exits
///
/// Exits the process with a non-zero code if `rsync` or `git` fails or is not found.
pub fn update(options: &Options) {
    let check = check_for_updates();

    let cfg = get_config();
    let color = get_colors(cfg.colors);

    println!(
        "{}Syncing SlackBuilds {} repository:\n{}",
        color.bold, cfg.branch, color.reset
    );

    println!("Mirror:      {}{}{}", color.yellow, cfg.mirror, color.reset);
    println!("Branch:      {}{}{}", color.yellow, cfg.branch, color.reset);
    println!(
        "Local repo:  {}{}{}",
        color.yellow, cfg.local_repo, color.reset
    );

    if !check {
        confirm("\nSync anyway? [y/N]: ", options.no_confirm);
    }

    println!();

    if !std::path::Path::new(&cfg.repo_path).exists() {
        fs::create_dir_all(&cfg.repo_path).unwrap_or_else(|e| {
            eprintln!(
                "{}Error:{} Failed to create {}: {}",
                color.red, color.reset, &cfg.repo_path, e
            );
            exit(1);
        });
    }
    if options.generate {
        parse_repo();
    } else {
        println!("Syncing from: {}", cfg.mirror);

        #[allow(clippy::case_sensitive_file_extension_comparisons)]
        if cfg.mirror.ends_with(".git") {
            fs::remove_dir_all(&cfg.repo_path).unwrap_or_else(|e| {
                eprintln!(
                    "{}Error:{} Failed to remove {}: {}",
                    color.red, color.reset, cfg.repo_path, e
                );
                exit(1);
            });

            let mut args = cfg.git_args.clone();
            args.push(cfg.mirror.clone());
            args.push(cfg.repo_path.clone());

            run_command("git", &args);
        } else {
            let mirror = format!("{}/", cfg.mirror_url);

            let mut args = cfg.rsync_args.clone();
            args.push(mirror);
            args.push(cfg.repo_path.clone());

            run_command("rsync", &args);

            if cfg.branch != "current" {
                println!(
                    "Importing SBo repository for Slackware {} GPG Key...",
                    cfg.branch
                );

                Command::new("gpg")
                    .args([
                        "--quiet",
                        "--fetch-key",
                        "https://www.slackbuilds.org/GPG-KEY",
                    ])
                    .status()
                    .ok();

                println!("Import done.");
            }
        }
        parse_repo();
    }
}

/// Downloads the remote `ChangeLog.txt` silently (via rsync or wget depending
/// on the mirror type) and compares its first line with the local copy.
/// Returns `true` if updates are available, `false` if the repository is up to date.
fn check_for_updates() -> bool {
    let cfg = get_config();
    let color = get_colors(cfg.colors);
    let tmp_changelog = format!("/tmp/{}", cfg.changelog);
    print!("Checking for repository updates... ");
    let _ = io::stdout().flush();

    let mut args = parse_git_mirror(&cfg.mirror, &cfg.branch);

    #[allow(clippy::case_sensitive_file_extension_comparisons)]
    if cfg.mirror.ends_with(".git") {
        args.push("-q".to_string());
        args.push("-O".to_string());
        args.push(tmp_changelog.clone());
        run_command("wget", &args);
    } else {
        args.push("--quiet".to_string());
        args.push(tmp_changelog.clone());
        run_command("rsync", &args);
    }

    print!("{}OK{}", color.green, color.reset);

    let local = fs::read_to_string(format!("{}/{}", cfg.repo_path, cfg.changelog))
        .unwrap_or_default()
        .lines()
        .next()
        .unwrap_or("")
        .to_string();

    let remote = fs::read_to_string(tmp_changelog)
        .unwrap_or_default()
        .lines()
        .next()
        .unwrap_or("")
        .to_string();

    println!();

    if local == remote {
        println!(
            "\n{}{}Repository is up to date!{}\n",
            color.bold, color.green, color.reset
        );
        false
    } else {
        println!(
            "\n{}{}There are updates available!{}\n",
            color.bold, color.yellow, color.reset
        );
        true
    }
}

/// Converts a git mirror URL into a direct URL pointing to the raw
/// `ChangeLog.txt` file, handling GitHub and GitLab URL formats.
fn parse_git_mirror(mirror: &str, branch: &str) -> Vec<String> {
    let cfg = get_config();
    let clean_mirror = mirror.replace(".git", "");

    if mirror.contains("github") {
        let raw_url = format!(
            "{}/{}",
            clean_mirror.replace("github.com", "raw.githubusercontent.com"),
            branch
        );
        vec![format!("{}/{}", raw_url, cfg.changelog)]
    } else if mirror.contains("gitlab") {
        let mirror_url = format!("{}/-/raw/{}/{}", clean_mirror, branch, cfg.changelog);
        vec![mirror_url]
    } else {
        let mirror_url = format!("{}{}/{}", mirror, branch, cfg.changelog);
        vec![mirror_url]
    }
}

/// Unit tests for mirror URL construction across supported providers.
///
/// Requires `/etc/sbpkg/sbpkg.toml` to be present, as `parse_git_mirror` calls `get_config()` internally.
#[cfg(test)]
mod tests {
    use super::*;

    /// Verifies that GitHub mirror URLs are converted to raw.githubusercontent.com format.
    #[test]
    fn test_parse_git_mirror_github() {
        let urls = parse_git_mirror("https://github.com/Ponce/slackbuilds.git", "current");
        assert_eq!(
            urls[0],
            "https://raw.githubusercontent.com/Ponce/slackbuilds/current/ChangeLog.txt"
        );
    }

    /// Verifies that GitLab mirror URLs are converted to the /-/raw/ format.
    #[test]
    fn test_parse_git_mirror_gitlab() {
        let urls = parse_git_mirror("https://gitlab.com/SlackBuilds.org/slackbuilds.git", "15.0");
        assert_eq!(
            urls[0],
            "https://gitlab.com/SlackBuilds.org/slackbuilds/-/raw/15.0/ChangeLog.txt"
        );
    }

    /// Verifies that rsync mirror URLs are constructed with branch and changelog path.
    #[test]
    fn test_parse_git_mirror_rsync() {
        let urls = parse_git_mirror("rsync://slackbuilds.org/slackbuilds/", "15.0");
        assert_eq!(
            urls[0],
            "rsync://slackbuilds.org/slackbuilds/15.0/ChangeLog.txt"
        );
    }
}
