use std::{
    fs::{self},
    path::{Path, PathBuf},
    thread,
};

use anyhow::Context;
use log::{error, info, warn};

use crate::{
    commands::bcftools::{bcftools_concat, bcftools_keep_pass, BcftoolsConfig},
    runners::{run_wait, CommandRun, RunReport},
};

#[derive(Debug, Clone)]
pub struct NanomonSVConfig {
    pub bin: String,
    pub result_dir: String,
    pub reference: String,
    pub thread: u8,
}

impl Default for NanomonSVConfig {
    fn default() -> Self {
        Self {
            bin: "nanomonsv".to_string(),
            result_dir: "/data/longreads_basic_pipe".to_string(),
            reference: "/data/ref/hs1/chm13v2.0.fa".to_string(),
            thread: 155,
        }
    }
}

#[derive(Debug)]
pub struct NanomonSV {
    pub id: String,
    pub diag_bam: String,
    pub mrd_bam: String,
    pub diag_out_dir: String,
    pub mrd_out_dir: String,
    pub log: String,
    pub log_dir: String,
    pub config: NanomonSVConfig,
}

impl NanomonSV {
    pub fn new(id: &str, diag_bam: &str, mrd_bam: &str, config: NanomonSVConfig) -> Self {
        let diag_out_dir = format!("{}/{id}/diag/nanomonsv", &config.result_dir);
        let mrd_out_dir = format!("{}/{id}/mrd/nanomonsv", &config.result_dir);
        let log_dir = format!("{}/{id}/log/nanomonsv", &config.result_dir);

        NanomonSV {
            id: id.to_string(),
            diag_bam: diag_bam.to_string(),
            mrd_bam: mrd_bam.to_string(),
            diag_out_dir,
            mrd_out_dir,
            log: String::default(),
            config,
            log_dir,
        }
    }

    pub fn run(&mut self) -> anyhow::Result<()> {
        // Create direcories
        if !Path::new(&self.diag_out_dir).exists() {
            fs::create_dir_all(&self.diag_out_dir)
                .context(format!("Can't create {}", &self.diag_out_dir))?;
        }
        if !Path::new(&self.mrd_out_dir).exists() {
            fs::create_dir_all(&self.mrd_out_dir)
                .context(format!("Can't create {}", &self.mrd_out_dir))?;
        }
        if !Path::new(&self.log_dir).exists() {
            fs::create_dir_all(&self.log_dir).context(format!("Can't create {}", &self.log_dir))?;
        }

        // Parse
        info!("Nanomonsv Parse");
        let diag_out_prefix = format!("{}/{}_diag", self.diag_out_dir, self.id);
        let mrd_out_prefix = format!("{}/{}_mrd", self.mrd_out_dir, self.id);

        let diag_info_vcf = format!("{diag_out_prefix}.bp_info.sorted.bed.gz");
        let mrd_info_vcf = format!("{mrd_out_prefix}.bp_info.sorted.bed.gz");

        let mut threads_handles = Vec::new();
        if !Path::new(&diag_info_vcf).exists() {
            let diag_bam = self.diag_bam.clone();
            let diag_out_prefix = diag_out_prefix.clone();
            let config = self.config.clone();
            let log_dir = self.log_dir.clone();
            let handle = thread::spawn(move || {
                let report = nanomonsv_parse(&diag_bam, &diag_out_prefix, config).unwrap();
                report
                    .save_to_file(&format!("{log_dir}/nanomonsv_parse_diag_"))
                    .unwrap();
            });
            threads_handles.push(handle);
        }

        if !Path::new(&mrd_info_vcf).exists() {
            let mrd_out_prefix = mrd_out_prefix.clone();
            let mrd_bam = self.mrd_bam.clone();
            let config = self.config.clone();
            let log_dir = self.log_dir.clone();
            let handle = thread::spawn(move || {
                let report = nanomonsv_parse(&mrd_bam, &mrd_out_prefix, config).unwrap();
                report
                    .save_to_file(&format!("{log_dir}/nanomonsv_parse_mrd_"))
                    .unwrap();
            });
            threads_handles.push(handle);
        }

        // Wait for all threads to finish
        for handle in threads_handles {
            handle.join().expect("Thread panicked");
        }

        // Get
        info!("Nanomonsv Get");
        let diag_result_vcf = format!("{diag_out_prefix}.nanomonsv.result.vcf");
        let mrd_result_vcf = format!("{mrd_out_prefix}.nanomonsv.result.vcf");

        if !Path::new(&mrd_result_vcf).exists() {
            let report = nanomonsv_get(
                &self.mrd_bam,
                &mrd_out_prefix,
                None,
                None,
                self.config.clone(),
            )
            .context(format!(
                "Error while running NanomonSV get for {mrd_result_vcf}"
            ))?;
            report
                .save_to_file(&format!("{}/nanomonsv_get_mrd_", self.log_dir))
                .unwrap();
        }

        if !Path::new(&diag_result_vcf).exists() {
            let report = nanomonsv_get(
                &self.diag_bam,
                &diag_out_prefix,
                Some(&self.mrd_bam),
                Some(&mrd_out_prefix),
                self.config.clone(),
            )
            .context(format!(
                "Error while running NanomonSV get for {diag_result_vcf}"
            ))?;
            report
                .save_to_file(&format!("{}/nanomonsv_get_diag_", self.log_dir,))
                .unwrap();
        }

        let vcf_passed = format!("{diag_out_prefix}_nanomonsv_PASSED.vcf.gz");
        if !Path::new(&vcf_passed).exists() {
            let report =
                bcftools_keep_pass(&diag_result_vcf, &vcf_passed, BcftoolsConfig::default())
                    .context(format!("Can't index {vcf_passed}"))?;
            report
                .save_to_file(&format!("{}/bcftools_pass_", self.log_dir,))
                .context("Can't save report")?;
        }
        Ok(())
    }
}

#[derive(Debug)]
pub struct NanomonSVSolo {
    pub id: String,
    pub bam: String,
    pub time_point: String,
    pub out_dir: String,
    pub log: String,
    pub log_dir: String,
    pub config: NanomonSVConfig,
}

impl NanomonSVSolo {
    pub fn new(id: &str, bam: &str, time_point: &str, config: NanomonSVConfig) -> Self {
        let out_dir = format!("{}/{id}/{time_point}/nanomonsv-solo", &config.result_dir);
        let log_dir = format!("{}/{id}/log/nanomonsv", &config.result_dir);
        NanomonSVSolo {
            id: id.to_string(),
            bam: bam.to_string(),
            out_dir,
            time_point: time_point.to_string(),
            log: String::default(),
            log_dir,
            config,
        }
    }

    pub fn run(&mut self) -> anyhow::Result<()> {
        // Create direcories
        if !Path::new(&self.out_dir).exists() {
            fs::create_dir_all(&self.out_dir).context(format!("Can't create {}", &self.out_dir))?;
        }

        if !Path::new(&self.log_dir).exists() {
            fs::create_dir_all(&self.log_dir).context(format!("Can't create {}", &self.log_dir))?;
        }

        // Parse
        info!("Nanomonsv Parse");
        let out_prefix = format!("{}/{}_{}", self.out_dir, self.id, self.time_point);

        let info_vcf = format!("{out_prefix}.bp_info.sorted.bed.gz");
        if !Path::new(&info_vcf).exists() {
            let report = nanomonsv_parse(&self.bam, &out_prefix, self.config.clone()).context(
                format!("Error while running NanomonSV parse for {}", self.bam),
            )?;
            let log_file = format!("{}/nanomonsv_parse_{}_", self.log_dir, self.time_point);
            report
                .save_to_file(&log_file)
                .context(format!("Can't write logs into {log_file}"))?;
        }

        // Get
        info!("Nanomonsv Get");
        let result_vcf = format!("{out_prefix}.nanomonsv.result.vcf");

        if !Path::new(&result_vcf).exists() {
            let report = nanomonsv_get(&self.bam, &out_prefix, None, None, self.config.clone())
                .context(format!(
                    "Error while running NanomonSV get for {result_vcf}"
                ))?;
            let log_file = format!("{}/nanomonsv_get_{}_", self.log_dir, self.time_point);
            report
                .save_to_file(&log_file)
                .context(format!("Error while writing log into {log_file}"))?;
        }

        let vcf_passed = format!("{out_prefix}_nanomonsv-solo_PASSED.vcf.gz");
        if !Path::new(&vcf_passed).exists() {
            let report = bcftools_keep_pass(&result_vcf, &vcf_passed, BcftoolsConfig::default())
                .context(format!(
                    "Error while running bcftools keep PASS for {result_vcf}"
                ))?;

            let log_file = format!("{}/bcftools_pass_", self.log_dir);
            report
                .save_to_file(&log_file)
                .context(format!("Error while writing log into {log_file}"))?;
        }
        Ok(())
    }
}

pub fn nanomonsv_parse(
    bam: &str,
    out_prefix: &str,
    config: NanomonSVConfig,
) -> anyhow::Result<RunReport> {
    let args = vec![
        "parse",
        "--reference_fasta",
        &config.reference,
        bam,
        out_prefix,
    ];
    let mut cmd_run = CommandRun::new(&config.bin, &args);
    let res = run_wait(&mut cmd_run)?;
    Ok(res)
}

pub fn nanomonsv_get(
    bam: &str,
    out_prefix: &str,
    ctrl_bam: Option<&str>,
    ctrl_prefix: Option<&str>,
    config: NanomonSVConfig,
) -> anyhow::Result<RunReport> {
    let threads = config.thread.to_string();
    let mut args = vec!["get"];

    if let (Some(ctrl_bam), Some(ctrl_prefix)) = (ctrl_bam, ctrl_prefix) {
        args.extend(vec![
            "--control_prefix",
            ctrl_prefix,
            "--control_bam",
            ctrl_bam,
        ])
    }

    args.extend(vec![
        "--process",
        &threads,
        out_prefix,
        bam,
        &config.reference,
    ]);
    let mut cmd_run = CommandRun::new(&config.bin, &args);
    let res = run_wait(&mut cmd_run)?;
    Ok(res)
}

pub fn nanomonsv_create_pon(config: NanomonSVConfig, pon_path: &str) -> anyhow::Result<()> {
    let mut passed_mrd = Vec::new();
    for mrd_dir in find_nanomonsv_dirs(&PathBuf::from(&config.result_dir), "mrd", 0, 3) {
        let mut passed = None;
        let mut passed_csi = None;
        let mut result = None;

        for entry in fs::read_dir(&mrd_dir)
            .with_context(|| format!("Failed to read directory: {}", mrd_dir.display()))?
        {
            let entry =
                entry.with_context(|| format!("Failed to read entry in: {}", mrd_dir.display()))?;
            let file_name = entry.file_name().to_string_lossy().to_string();
            let path = entry.path();

            if file_name.ends_with("_mrd_nanomonsv_PASSED.vcf.gz") {
                passed = Some(path);
            } else if file_name.ends_with("_mrd_nanomonsv_PASSED.vcf.gz.csi") {
                passed_csi = Some(path);
            } else if file_name.ends_with("_mrd.nanomonsv.result.vcf") {
                result = Some(path);
            }
        }

        match (result, passed.clone(), passed_csi) {
            (None, None, None) => error!("No result in {}", mrd_dir.display()),
            (Some(p), None, None) => {
                let output = replace_filename_suffix(
                    &p,
                    "_mrd.nanomonsv.result.vcf",
                    "_mrd_nanomonsv_PASSED.vcf.gz",
                );
                info!("Do pass for {} to {}", p.display(), output.display());

                if let Err(r) = bcftools_keep_pass(
                    p.to_str().unwrap(),
                    output.to_str().unwrap(),
                    BcftoolsConfig::default(),
                ) {
                    error!("{r}");
                } else {
                    passed_mrd.push(output);
                }
            }
            (Some(_), Some(p), None) => warn!("Do csi for {}", p.display()),
            (Some(_), Some(p), Some(_)) => passed_mrd.push(p),
            _ => {} // All files found
        }
    }

    println!("{} vcf to concat", passed_mrd.len());
    bcftools_concat(
        passed_mrd
            .iter()
            .map(|p| p.to_string_lossy().to_string())
            .collect(),
        pon_path,
        BcftoolsConfig::default(),
    )?;

    Ok(())
}

pub fn find_nanomonsv_dirs(
    root: &Path,
    time_point: &str,
    depth: usize,
    max_depth: usize,
) -> Vec<PathBuf> {
    if depth >= max_depth {
        return Vec::new();
    }

    let entries: Vec<_> = match fs::read_dir(root) {
        Ok(entries) => entries.filter_map(Result::ok).collect(),
        Err(_) => return Vec::new(),
    };

    let mut result: Vec<PathBuf> = entries
        .iter()
        .filter_map(|entry| {
            let path = entry.path();
            if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
                && path
                    .to_string_lossy()
                    .contains(&format!("{}/nanomonsv", time_point))
            {
                Some(path)
            } else {
                None
            }
        })
        .collect();

    result.extend(
        entries
            .iter()
            .filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
            .flat_map(|dir| find_nanomonsv_dirs(&dir.path(), time_point, depth + 1, max_depth)),
    );

    result
}

pub fn replace_filename_suffix(path: &Path, from: &str, to: &str) -> PathBuf {
    let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");

    let new_file_name = file_name.replace(from, to);

    path.with_file_name(new_file_name)
}
