use std::{
    fs::{self},
    path::PathBuf,
    str::FromStr,
};

use anyhow::Context;
use chrono::{DateTime, Utc};
use log::warn;
use serde::{Deserialize, Serialize};

use glob::glob;
use rayon::prelude::*;

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ModBases {
    pub id: String,
    pub time_point: String,
    pub mod_type: ModType,
    pub pileup: PathBuf,
    pub pileup_modif: DateTime<Utc>,
    pub dmr_files: Vec<Dmr>,
}

#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
pub enum ModType {
    #[serde(rename = "5mC_5hmC")]
    Mod5mC5hmC,
}

impl FromStr for ModType {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "5mC_5hmC" => Ok(ModType::Mod5mC5hmC),
            _ => Err(anyhow::anyhow!("Unknown ModType: {}", s)),
        }
    }
}

impl ModBases {
    pub fn new(path: PathBuf) -> anyhow::Result<Self> {
        let dir = path
            .parent()
            .context(format!("Can't open the parent dir of {}", path.display()))?;
        let filename = path
            .file_name()
            .context(format!("Can't parse filename for {}", path.display()))?
            .to_string_lossy()
            .to_string();

        let parts: Vec<&str> = filename.split("_").collect();

        if parts.len() < 4 {
            anyhow::bail!("Error in modPileup filename format {}", path.display());
        }

        let id = parts.first().unwrap().to_string();
        let time_point = parts.get(1).unwrap().to_string();
        let modif = parts.get(2..parts.len() - 1).unwrap().join("_");
        let mod_type = ModType::from_str(&modif)?;

        let entries = fs::read_dir(dir)?;

        let dmr_files: Vec<Dmr> = entries
            .filter_map(|entry| {
                let entry = entry.ok()?;
                let path = entry.path();

                if path.is_file()
                    && path
                        .file_name()
                        .and_then(|name| name.to_str())
                        .map(|name| name.contains("_DMR_"))
                        .unwrap_or(false)
                {
                    let filename = path
                        .file_stem()
                        .unwrap_or_default()
                        .to_string_lossy()
                        .to_string()
                        .replace(".bed", "");
                    match Dmr::from_str(&filename) {
                        Ok(mut dmr) => {
                            dmr.path = path.clone();
                            Some(dmr)
                        }
                        Err(e) => {
                            warn!("{e}");
                            None
                        }
                    }
                } else {
                    None
                }
            })
            .collect();

        let pileup_modif: DateTime<Utc> = path
            .metadata()
            .context(format!("Can't access metadata for {}", path.display()))?
            .modified()
            .context(format!(
                "Can't access modified date metadata for {}",
                path.display()
            ))?
            .into();

        Ok(Self {
            id,
            time_point,
            mod_type,
            pileup: path,
            pileup_modif,
            dmr_files,
        })
    }
}

#[derive(Debug)]
pub struct ModBasesCollection {
    pub modbases: Vec<ModBases>,
}

impl ModBasesCollection {
    pub fn new(result_dir: &str) -> Self {
        load_modbases_collection(result_dir)
    }

    pub fn get_id(&self, id: &str, modif: ModType) -> Vec<&ModBases> {
        self.modbases
            .iter()
            .filter(|m| m.id == id && m.mod_type == modif)
            .collect()
    }

    pub fn get_diag_mrd(&self, id: &str, modif: ModType) -> anyhow::Result<(&ModBases, &ModBases)> {
        let v = self.get_id(id, modif);
        if v.len() != 2 {
            anyhow::bail!("Error not found or too many files");
        } else {
            match (v[0].time_point.as_str(), v[1].time_point.as_str()) {
                ("diag", "mrd") => Ok((v[0], v[1])),
                ("mrd", "diag") => Ok((v[1], v[0])),
                _ => anyhow::bail!("Error in DMR files"),
            }
        }
    }
}

pub fn load_modbases_collection(result_dir: &str) -> ModBasesCollection {
    let pattern = format!("{result_dir}/*/*/*/*_modPileup.bed.gz");
    let modbases = glob(&pattern)
        .expect("Failed to read glob pattern")
        .par_bridge()
        .filter_map(|entry| {
            match entry {
                Ok(path) => match ModBases::new(path) {
                    Ok(modb) => return Some(modb),
                    Err(e) => warn!("{e}"),
                },
                Err(e) => warn!("Error: {:?}", e),
            }
            None
        })
        .collect();

    ModBasesCollection { modbases }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Dmr {
    pub base: String,
    pub vs: String,
    pub path: PathBuf,
}

impl FromStr for Dmr {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if !s.contains("_DMR_") {
            anyhow::bail!("Error paring DMR not `_DMR_` in filename {s}");
        }

        let spl: Vec<&str> = s.split("_").collect();
        Ok(Self {
            base: spl[spl.len() - 2].to_string(),
            vs: spl[spl.len() - 1].to_string(),
            path: PathBuf::default(),
        })
    }
}
