use core::panic;
use log::{debug, error, info};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::process::Command;

use crate::yaml::Config;

pub fn aggregate(config: Config) {
    for (dest, repo_cfg) in config.repos.iter() {
        info!("Start aggregation of {}", dest);

        // Ensure the directory exists and is an initialized Git repository
        let fetched_remotes = init_and_get_remotes(dest);

        // Set up target branch if specified, otherwise default to master/main tracking
        let target_branch = match &repo_cfg.target {
            Some(t) => {
                let target_url = repo_cfg.remotes.get(&t.remote).unwrap_or_else(|| {
                    panic!("Target remote '{}' not found in remotes list", t.remote);
                });
                add_or_update_remote(dest, &t.remote, target_url, &fetched_remotes);
                &t.branch
            }
            None => "master",
        };

        // Switch to the target branch immediately
        run_git(dest, vec!["checkout", "-B", target_branch]);

        // Set up all other remotes defined in configuration
        for (name, url) in repo_cfg.remotes.iter() {
            add_or_update_remote(dest, name, url, &fetched_remotes);
        }

        // Enable sparse-checkout
        match repo_cfg.sparse {
            Some(ref folder_list) => {
                info!("Configuring sparse-checkout");
                let (stdout, _stderr) = run_git(dest, vec!["config", "core.sparsecheckout"]);

                match stdout {
                    Some(_) => {
                        debug!("Spase checkout is already enabled")
                    }
                    None => {
                        debug!("Enabling sparse checkout");
                        run_git(dest, vec!["sparse-checkout", "init", "--cone"]);
                    }
                }

                // Set folders for sparse-ckeckout
                let mut args = vec!["sparse-checkout", "set"];
                let mut folders = folder_list.iter().map(|s| s.as_str()).collect();

                args.append(&mut folders);

                debug!("Setting sparse-ckeckout folder list");
                run_git(dest, args);
            }
            None => {
                debug!("No folders given to configure sparse-ckeckout, skipping");
            }
        }

        // Fetch the absolute BASE (the very first merge item) to initialize FETCH_HEAD correctly
        if let Some((first_merge, remaining_merges)) = repo_cfg.merges.split_first() {
            let mut first_fetch = vec!["fetch".to_string()];
            if let Some(d) = first_merge.depth {
                first_fetch.push("--depth".to_string());
                first_fetch.push(d.to_string());
            }
            first_fetch.push(first_merge.remote.clone());
            first_fetch.push(first_merge.reference.clone());

            let first_fetch_ref: Vec<&str> = first_fetch.iter().map(|s| s.as_str()).collect();
            run_git(dest, first_fetch_ref);

            info!(
                "Reset branch to {} {}",
                first_merge.remote, first_merge.reference
            );

            // Establish the base lineage using FETCH_HEAD from the first fetch
            run_git(dest, vec!["reset", "--hard", "FETCH_HEAD"]);
            run_git(dest, vec!["clean", "-ffd"]);

            // Sequentially fetch, evaluate ancestry, and pull the remaining references
            for merge in remaining_merges {
                // Fetch the explicit ref right now so FETCH_HEAD updates specifically to this step
                let mut fetch_args = vec![
                    "fetch".to_string(),
                    merge.remote.clone(),
                    merge.reference.clone(),
                ];
                if let Some(d) = merge.depth {
                    fetch_args.push("--depth".to_string());
                    fetch_args.push(d.to_string());
                }

                let fetch_args_ref: Vec<&str> = fetch_args.iter().map(|s| s.as_str()).collect();
                run_git(dest, fetch_args_ref);

                // Receive a warning when trying to merge unrelated repos
                let unrelated = is_unrelated_history(dest);

                if unrelated {
                    error!(
                        "Repository '{}' is merging an unrelated history from {}/{} into your current branch!",
                        dest, merge.remote, merge.reference
                    );

                    // Resolve index to undo failed aggregation
                    info!("Resolving failed merge in '{}'...", dest);
                    let abort_merge_args = vec!["reset", "--hard", "HEAD"];
                    run_git(dest, abort_merge_args);
                    info!("Removed untracked files in '{}'", dest);
                    let abort_merge_args = vec!["clean", "-fd"];
                    run_git(dest, abort_merge_args);

                    std::process::exit(1);
                }

                println!("Pull {}, {}", merge.remote, merge.reference);

                let mut pull_args = vec![
                    "pull".to_string(),
                    "--ff".to_string(),
                    "--no-rebase".to_string(),
                    "--no-edit".to_string(),
                    "--allow-unrelated-histories".to_string(),
                ];

                if let Some(d) = merge.depth {
                    pull_args.push("--depth".to_string());
                    pull_args.push(d.to_string());
                }
                pull_args.push(merge.remote.clone());
                pull_args.push(merge.reference.clone());

                let pull_args_ref: Vec<&str> = pull_args.iter().map(|s| s.as_str()).collect();
                run_git(dest, pull_args_ref);
            }
        }

        info!("End aggregation of {}", dest);
    }
}

// Checks if the incoming history (FETCH_HEAD) is completely unrelated to HEAD by tracking commit alignment
fn is_unrelated_history(cwd: &str) -> bool {
    let unique_output = Command::new("git")
        .current_dir(cwd)
        .args(vec!["rev-list", "HEAD..FETCH_HEAD", "--count"])
        .output();

    let total_output = Command::new("git")
        .current_dir(cwd)
        .args(vec!["rev-list", "FETCH_HEAD", "--count"])
        .output();

    if let (Ok(unique_res), Ok(total_res)) = (unique_output, total_output)
        && unique_res.status.success()
        && total_res.status.success()
    {
        let unique_str = String::from_utf8_lossy(&unique_res.stdout)
            .trim()
            .to_string();
        let total_str = String::from_utf8_lossy(&total_res.stdout)
            .trim()
            .to_string();

        if let (Ok(unique_count), Ok(total_count)) =
            (unique_str.parse::<u32>(), total_str.parse::<u32>())
        {
            // If every incoming commit is foreign to HEAD, it's an unrelated timeline
            return unique_count == total_count;
        }
    }
    false
}

// Run git command and return output
fn run_git(cwd: &str, args: Vec<&str>) -> (Option<String>, Option<String>) {
    match Command::new("git").current_dir(cwd).args(&args).output() {
        Ok(res) => {
            // Process stdout
            let stdout = if !res.stdout.is_empty() {
                Some(String::from_utf8_lossy(&res.stdout).into_owned())
            } else {
                None
            };

            // Process stderr
            let stderr = if !res.stderr.is_empty() {
                Some(String::from_utf8_lossy(&res.stderr).into_owned())
            } else {
                None
            };

            // Log if any of them exist
            if let Some(ref out) = stdout {
                debug!("stdout: {}", out);
            }
            if let Some(ref err) = stderr {
                debug!("stderr: {}", err);
            }

            (stdout, stderr)
        }
        Err(err) => {
            panic!("Failed to execute git command: {}", err);
        }
    }
}

fn add_or_update_remote(
    dest: &str,
    name: &str,
    url: &str,
    existing_remotes: &HashMap<String, String>,
) {
    match existing_remotes.get(name) {
        Some(existing_url) if existing_url == url => {}
        Some(_) => {
            run_git(dest, vec!["remote", "set-url", name, url]);
        }
        None => {
            run_git(dest, vec!["remote", "add", name, url]);
        }
    }
}

fn init_and_get_remotes(dest: &str) -> HashMap<String, String> {
    let folder = Path::new(dest);
    if !folder.exists() {
        fs::create_dir_all(folder).unwrap_or_else(|err| {
            panic!("Error creating folder {}: {}", dest, err);
        });
    }

    if !folder.join(".git").exists() {
        run_git(dest, vec!["init"]);
    }

    let (stdout, _stderr) = run_git(dest, vec!["remote", "-v"]);
    let mut remotes = HashMap::new();

    if let Some(stdout) = stdout {
        for line in stdout.lines() {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 2 {
                let name = parts[0].to_string();
                let url = parts[1].to_string();
                remotes.insert(name, url);
            }
        }
    }

    remotes
}
