use anyhow::Context;
use log::warn;
use std::{
    cmp::Ordering,
    collections::HashMap,
    fmt, fs,
    iter::Sum,
    ops::{Add, Div},
    path::{Path, PathBuf},
};

pub fn find_unique_file(dir_path: &str, suffix: &str) -> anyhow::Result<String> {
    let mut matching_files = Vec::new();

    for entry in
        fs::read_dir(dir_path).with_context(|| format!("Failed to read directory: {}", dir_path))?
    {
        let entry = entry.with_context(|| "Failed to read directory entry")?;
        let path = entry.path();

        if path.is_file()
            && path
                .file_name()
                .and_then(|name| name.to_str())
                .map(|name| name.ends_with(suffix))
                .unwrap_or(false)
        {
            matching_files.push(path);
        }
    }

    match matching_files.len() {
        0 => Err(anyhow::anyhow!("No file found ending with '{}'", suffix))
            .with_context(|| format!("In directory: {}", dir_path)),
        1 => Ok(matching_files[0].to_string_lossy().into_owned()),
        _ => Err(anyhow::anyhow!(
            "Multiple files found ending with '{}'",
            suffix
        ))
        .with_context(|| format!("In directory: {}", dir_path)),
    }
}

pub fn path_prefix(out: &str) -> anyhow::Result<String> {
    let out_path = Path::new(&out);

    let out_dir = out_path
        .parent()
        .ok_or_else(|| anyhow::anyhow!("Can't parse the dir of {}", out_path.display()))?;

    let name = out_path
        .file_name()
        .and_then(|name| name.to_str())
        .ok_or_else(|| anyhow::anyhow!("Can't parse the file name of {}", out_path.display()))?;

    let stem = name
        .split_once('.')
        .map(|(stem, _)| stem)
        .ok_or_else(|| anyhow::anyhow!("Can't parse the file stem of {}", name))?;

    Ok(format!("{}/{stem}", out_dir.display()))
}

pub fn force_or_not(path: &str, force: bool) -> anyhow::Result<()> {
    let path = Path::new(path);
    let mut output_exists = path.exists();
    let dir = path
        .parent()
        .context(format!("Can't parse the parent dir of {}", path.display()))?;

    if force && output_exists {
        fs::remove_dir_all(dir)?;
        fs::create_dir_all(dir)?;
        output_exists = false;
    }

    if output_exists {
        warn!("{} already exists.", path.display())
    }
    Ok(())
}

use rayon::prelude::*;
use std::cmp::Ord;

pub struct VectorIntersection<T> {
    pub common: Vec<T>,
    pub only_in_first: Vec<T>,
    pub only_in_second: Vec<T>,
}

impl<T> fmt::Display for VectorIntersection<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let total_items = self.common.len() + self.only_in_first.len() + self.only_in_second.len();

        writeln!(f, "Total items: {}", total_items)?;
        writeln!(
            f,
            "Common: {} ({:.2}%)",
            self.common.len(),
            percentage(self.common.len(), total_items)
        )?;
        writeln!(
            f,
            "Only in first: {} ({:.2}%)",
            self.only_in_first.len(),
            percentage(self.only_in_first.len(), total_items)
        )?;
        writeln!(
            f,
            "Only in second: {} ({:.2}%)",
            self.only_in_second.len(),
            percentage(self.only_in_second.len(), total_items)
        )
    }
}

fn percentage(part: usize, total: usize) -> f64 {
    if total == 0 {
        0.0
    } else {
        (part as f64 / total as f64) * 100.0
    }
}

impl<T> Default for VectorIntersection<T> {
    fn default() -> Self {
        Self {
            common: Vec::new(),
            only_in_first: Vec::new(),
            only_in_second: Vec::new(),
        }
    }
}

impl<T: Ord + Clone> VectorIntersection<T> {
    fn merge(&mut self, other: &mut Self) {
        self.common.append(&mut other.common);
        self.only_in_first.append(&mut other.only_in_first);
        self.only_in_second.append(&mut other.only_in_second);
    }
}

fn intersection<T: Ord + Clone>(vec1: &[T], vec2: &[T]) -> VectorIntersection<T> {
    let mut result = VectorIntersection::default();
    let mut i = 0;
    let mut j = 0;

    while i < vec1.len() && j < vec2.len() {
        match vec1[i].cmp(&vec2[j]) {
            Ordering::Less => {
                result.only_in_first.push(vec1[i].clone());
                i += 1;
            }
            Ordering::Greater => {
                result.only_in_second.push(vec2[j].clone());
                j += 1;
            }
            Ordering::Equal => {
                let val = &vec1[i];
                let mut count1 = 1;
                let mut count2 = 1;

                // Count occurrences in vec1
                while i + 1 < vec1.len() && &vec1[i + 1] == val {
                    i += 1;
                    count1 += 1;
                }

                // Count occurrences in vec2
                while j + 1 < vec2.len() && &vec2[j + 1] == val {
                    j += 1;
                    count2 += 1;
                }

                // Add to common
                result
                    .common
                    .extend(std::iter::repeat(val.clone()).take(count1.min(count2)));

                // Add excess to only_in_first or only_in_second
                match count1.cmp(&count2) {
                    Ordering::Greater => {
                        result
                            .only_in_first
                            .extend(std::iter::repeat(val.clone()).take(count1 - count2));
                    }
                    Ordering::Less => {
                        result
                            .only_in_second
                            .extend(std::iter::repeat(val.clone()).take(count2 - count1));
                    }
                    Ordering::Equal => {
                        // No excess elements, do nothing
                    }
                }
                i += 1;
                j += 1;
            }
        }
    }

    result.only_in_first.extend(vec1[i..].iter().cloned());
    result.only_in_second.extend(vec2[j..].iter().cloned());

    result
}

pub fn par_intersection<T: Ord + Send + Sync + Clone>(
    vec1: &[T],
    vec2: &[T],
) -> VectorIntersection<T> {
    let chunk_size = (vec1.len() / rayon::current_num_threads()).max(1);

    vec1.par_chunks(chunk_size)
        .map(|chunk| {
            let start = vec2.partition_point(|x| x < &chunk[0]);
            let end = vec2.partition_point(|x| x <= &chunk[chunk.len() - 1]);

            // Ensure start is not greater than end
            if start <= end {
                intersection(chunk, &vec2[start..end])
            } else {
                // If start > end, there's no intersection for this chunk
                VectorIntersection::default()
            }
        })
        .reduce(VectorIntersection::default, |mut acc, mut x| {
            acc.merge(&mut x);
            acc
        })
}

pub fn get_temp_file_path() -> std::io::Result<PathBuf> {
    let temp_path = tempfile::Builder::new()
        .prefix("my-temp-")
        .suffix(".tmp")
        .rand_bytes(5)
        .tempfile()?
        .into_temp_path();

    Ok(temp_path.to_path_buf())
}

pub fn estimate_shannon_entropy(dna_sequence: &str) -> f64 {
    let m = dna_sequence.len() as f64;

    // Early return for empty sequences
    if m == 0.0 {
        return 0.0;
    }

    // Count occurrences of each base
    let mut bases = HashMap::<char, usize>::new();
    for base in dna_sequence.chars() {
        *bases.entry(base).or_insert(0) += 1;
    }

    // Calculate Shannon entropy
    let shannon_entropy_value: f64 = bases
        .values()
        .map(|&n_i| {
            let p_i = n_i as f64 / m;
            if p_i > 0.0 {
                -p_i * p_i.log2()
            } else {
                0.0 // Avoid log2(0)
            }
        })
        .sum();

    shannon_entropy_value
}

pub fn mean<T>(values: &[T]) -> f64
where
    T: Copy + Add<Output = T> + Div<Output = T> + Sum + Into<f64>,
{
    let count = values.len();

    if count == 0 {
        return 0.0;
    }

    // Calculate the sum of the values
    let sum: T = values.iter().copied().sum();

    // Calculate and return the average as f64
   sum.into() / count as f64
}
