use std::collections::HashMap;

use clap::Args;
use obikindex::GenomeInfo;
use obikpartitionner::{GroupQuorumFilter, KmerFilter};
use obitaxonomy::{TaxPath, TaxPattern};

// ── Operator ──────────────────────────────────────────────────────────────────

enum PredOp { Wildcard, Eq, Ne, Matches, NotMatches }

// ── MetaPred ──────────────────────────────────────────────────────────────────

/// A single predicate on genome metadata: `key OP val1|val2|…`
///
/// Operators: `=` (exact), `!=` (not equal), `~` (path ancestor), `!~` (not ancestor).
/// Multiple values separated by `|` are OR'd.
pub struct MetaPred {
    key:    String,
    op:     PredOp,
    values: Vec<String>,
}

impl MetaPred {
    /// Parse a predicate string of the form `key=v1|v2`, `key!=v`, `key~path`, `key!~path`.
    /// The special values `*` and `all` (case-insensitive) match every genome.
    pub fn parse(s: &str) -> Result<Self, String> {
        let t = s.trim();
        if t == "*" || t.eq_ignore_ascii_case("all") {
            return Ok(Self { key: String::new(), op: PredOp::Wildcard, values: vec![] });
        }

        let (op, key, rhs) =
            if let Some(pos) = s.find("!=") {
                (PredOp::Ne,         &s[..pos], &s[pos+2..])
            } else if let Some(pos) = s.find("!~") {
                (PredOp::NotMatches, &s[..pos], &s[pos+2..])
            } else if let Some(pos) = s.find('=') {
                (PredOp::Eq,         &s[..pos], &s[pos+1..])
            } else if let Some(pos) = s.find('~') {
                (PredOp::Matches,    &s[..pos], &s[pos+1..])
            } else {
                return Err(format!("no operator found in predicate: {s}"));
            };

        let key = key.trim().to_string();
        if key.is_empty() { return Err(format!("empty key in predicate: {s}")); }

        let values: Vec<String> = rhs.split('|').map(|v| v.trim().to_string()).collect();
        if values.iter().any(|v| v.is_empty()) {
            return Err(format!("empty value in predicate: {s}"));
        }
        Ok(Self { key, op, values })
    }

    /// Evaluate against one genome's metadata.
    /// Returns `None` when the key is absent (NA propagation).
    fn eval(&self, meta: &HashMap<String, String>) -> Option<bool> {
        if matches!(self.op, PredOp::Wildcard) { return Some(true); }
        let value = meta.get(&self.key)?;
        Some(match self.op {
            PredOp::Wildcard   => unreachable!(),
            PredOp::Eq         => self.values.iter().any(|v| v == value),
            PredOp::Ne         => self.values.iter().all(|v| v != value),
            PredOp::Matches    => self.values.iter().any(|v| path_matches(value, v)),
            PredOp::NotMatches => self.values.iter().all(|v| !path_matches(value, v)),
        })
    }
}

// ── Path matching ─────────────────────────────────────────────────────────────

/// True if the stored taxonomy `value` matches `pattern`.
///
/// `value` must be a valid `TaxPath` (starts with `taxonomy:/`).
/// `pattern` is a `TaxPattern` query (see `obitaxonomy::TaxPattern` for syntax).
/// Returns `false` if either fails to parse.
fn path_matches(value: &str, pattern: &str) -> bool {
    let Ok(path) = TaxPath::parse(value)    else { return false };
    let Ok(pat)  = TaxPattern::parse(pattern) else { return false };
    pat.matches(&path)
}

// ── Three-value group evaluation ──────────────────────────────────────────────

/// AND of all predicates (ingroup semantics).
/// Short-circuits on `Some(false)`; propagates `None` if no predicate returns `false`.
fn eval_and(preds: &[MetaPred], meta: &HashMap<String, String>) -> Option<bool> {
    let mut has_na = false;
    for pred in preds {
        match pred.eval(meta) {
            Some(false) => return Some(false),
            Some(true)  => {}
            None        => has_na = true,
        }
    }
    if has_na { None } else { Some(true) }
}

/// OR of all predicates (outgroup semantics).
/// Short-circuits on `Some(true)`; propagates `None` if no predicate returns `true`.
fn eval_or(preds: &[MetaPred], meta: &HashMap<String, String>) -> Option<bool> {
    let mut has_na = false;
    for pred in preds {
        match pred.eval(meta) {
            Some(true)  => return Some(true),
            Some(false) => {}
            None        => has_na = true,
        }
    }
    if has_na { None } else { Some(false) }
}

// ── Genome classification ─────────────────────────────────────────────────────

enum Membership { Ingroup, Outgroup, Uncategorized }

fn classify(
    genomes: &[GenomeInfo],
    ingroup:  &[MetaPred],
    outgroup: &[MetaPred],
) -> Vec<Membership> {
    genomes.iter().map(|g| {
        let in_r  = if ingroup.is_empty()  { None } else { eval_and(ingroup,  &g.meta) };
        let out_r = if outgroup.is_empty() { None } else { eval_or(outgroup,  &g.meta) };

        // Ingroup wins over outgroup.
        if in_r == Some(true)  { return Membership::Ingroup;  }
        if out_r == Some(true) { return Membership::Outgroup; }
        Membership::Uncategorized
    }).collect()
}

// ── Public constructor ────────────────────────────────────────────────────────

/// Build a `GroupQuorumFilter` from parsed predicates and genome metadata.
///
/// - No groups defined: `ingroup_idx` = all genomes (implicit ingroup).
/// - `ingroup` predicates only: outgroup indices are empty.
/// - `outgroup` predicates only: ingroup indices are empty.
/// - Both defined: ingroup wins on overlap; uncategorized genomes are ignored.
/// CLI args for ingroup/outgroup filtering — embeddable in any command via `#[command(flatten)]`.
#[derive(Args)]
pub struct FilterArgs {
    /// Ingroup predicate (repeatable; AND). Forms: `key=v1|v2`, `key!=v`, `key~path`, `key!~path`, `*`/`all`
    #[arg(long, value_name = "PRED")]
    pub ingroup: Vec<String>,

    /// Outgroup predicate (repeatable; OR). Forms: `key=v1|v2`, `key!=v`, `key~path`, `key!~path`, `*`/`all`
    #[arg(long, value_name = "PRED")]
    pub outgroup: Vec<String>,

    /// Minimum number of ingroup genomes containing the k-mer
    /// (negative: offset from group size, e.g. -1 = all but one)
    #[arg(long, allow_hyphen_values = true)]
    pub min_count: Option<isize>,

    /// Maximum number of ingroup genomes containing the k-mer
    /// (negative: offset from group size, e.g. -1 = all but one)
    #[arg(long, allow_hyphen_values = true)]
    pub max_count: Option<isize>,

    /// Minimum fraction of ingroup genomes containing the k-mer [0.0–1.0]
    /// (default 1.0 when --ingroup is set, 0.0 otherwise)
    #[arg(long)]
    pub min_frac: Option<f64>,

    /// Maximum fraction of ingroup genomes containing the k-mer [0.0–1.0]
    #[arg(long)]
    pub max_frac: Option<f64>,

    /// Minimum number of outgroup genomes containing the k-mer
    /// (negative: offset from outgroup size, e.g. -1 = all but one)
    #[arg(long, allow_hyphen_values = true)]
    pub min_outgroup_count: Option<isize>,

    /// Maximum number of outgroup genomes containing the k-mer
    /// (default 0 when --outgroup is set, no constraint otherwise;
    /// negative: offset from outgroup size, e.g. -1 = all but one)
    #[arg(long, allow_hyphen_values = true)]
    pub max_outgroup_count: Option<isize>,

    /// Minimum fraction of outgroup genomes containing the k-mer [0.0–1.0]
    #[arg(long)]
    pub min_outgroup_frac: Option<f64>,

    /// Maximum fraction of outgroup genomes containing the k-mer [0.0–1.0]
    #[arg(long)]
    pub max_outgroup_frac: Option<f64>,

    /// Per-genome count threshold to consider a genome as "containing" the k-mer (default 0)
    #[arg(long, default_value = "0")]
    pub presence_threshold: u32,
}

impl FilterArgs {
    /// Parse predicates and build a filter list ready to pass to `iter_partition_kmers`.
    pub fn build_filters(&self, genomes: &[GenomeInfo]) -> Vec<Box<dyn KmerFilter>> {
        let ingroup_preds: Vec<MetaPred> = self.ingroup.iter()
            .map(|s| MetaPred::parse(s).unwrap_or_else(|e| {
                eprintln!("error in --ingroup: {e}");
                std::process::exit(1);
            }))
            .collect();
        let outgroup_preds: Vec<MetaPred> = self.outgroup.iter()
            .map(|s| MetaPred::parse(s).unwrap_or_else(|e| {
                eprintln!("error in --outgroup: {e}");
                std::process::exit(1);
            }))
            .collect();
        let filter = build_group_filter(
            genomes,
            &ingroup_preds,
            &outgroup_preds,
            GroupFilterParams {
                threshold:          self.presence_threshold,
                min_count:          self.min_count,
                max_count:          self.max_count,
                min_frac:           self.min_frac,
                max_frac:           self.max_frac,
                min_outgroup_count: self.min_outgroup_count,
                max_outgroup_count: self.max_outgroup_count,
                min_outgroup_frac:  self.min_outgroup_frac,
                max_outgroup_frac:  self.max_outgroup_frac,
            },
        ).unwrap_or_else(|e| {
            eprintln!("error in filter parameters: {e}");
            std::process::exit(1);
        });
        vec![Box::new(filter)]
    }
}

/// Returns indices of genomes matching `pred_str` (single predicate).
pub fn matching_genome_indices(pred_str: &str, genomes: &[GenomeInfo]) -> Result<Vec<usize>, String> {
    let pred = MetaPred::parse(pred_str)?;
    Ok(genomes.iter().enumerate()
        .filter_map(|(i, g)| {
            if pred.eval(&g.meta) == Some(true) { Some(i) } else { std::option::Option::None }
        })
        .collect())
}

pub struct GroupFilterParams {
    pub threshold:          u32,
    pub min_count:          Option<isize>,
    pub max_count:          Option<isize>,
    pub min_frac:           Option<f64>,
    pub max_frac:           Option<f64>,
    pub min_outgroup_count: Option<isize>,
    pub max_outgroup_count: Option<isize>,
    pub min_outgroup_frac:  Option<f64>,
    pub max_outgroup_frac:  Option<f64>,
}

pub fn build_group_filter(
    genomes:        &[GenomeInfo],
    ingroup_preds:  &[MetaPred],
    outgroup_preds: &[MetaPred],
    p:              GroupFilterParams,
) -> Result<GroupQuorumFilter, String> {
    let (ingroup_idx, outgroup_idx) = if ingroup_preds.is_empty() && outgroup_preds.is_empty() {
        ((0..genomes.len()).collect(), vec![])
    } else {
        let members = classify(genomes, ingroup_preds, outgroup_preds);
        let in_idx: Vec<usize>  = members.iter().enumerate()
            .filter(|(_, m)| matches!(m, Membership::Ingroup))
            .map(|(i, _)| i).collect();
        let out_idx: Vec<usize> = members.iter().enumerate()
            .filter(|(_, m)| matches!(m, Membership::Outgroup))
            .map(|(i, _)| i).collect();
        (in_idx, out_idx)
    };

    let in_size  = ingroup_idx.len();
    let out_size = outgroup_idx.len();

    let ingroup_quorum_explicit  = p.min_count.is_some() || p.max_count.is_some()
        || p.min_frac.is_some() || p.max_frac.is_some();
    let outgroup_quorum_explicit = p.min_outgroup_count.is_some() || p.max_outgroup_count.is_some()
        || p.min_outgroup_frac.is_some() || p.max_outgroup_frac.is_some();

    let default_min_frac           = if !ingroup_preds.is_empty()  && !ingroup_quorum_explicit  { 1.0      } else { 0.0      };
    let default_max_outgroup_count = if !outgroup_preds.is_empty() && !outgroup_quorum_explicit { 0        } else { out_size };

    // Resolve a signed count: negative means an offset from the group size
    // (e.g. -1 = all but one), floored at 1 so the negative form always keeps
    // constraining the group — even a singleton group, where n-1 would be 0
    // and would otherwise drop the constraint entirely.
    let resolve = |v: isize, size: usize| -> usize {
        if v < 0 { (size as isize + v).max(1) as usize } else { v as usize }
    };

    let min_count          = p.min_count.map(|v| resolve(v, in_size)).unwrap_or(0);
    let max_count          = p.max_count.map(|v| resolve(v, in_size)).unwrap_or(in_size);
    let min_frac           = p.min_frac.unwrap_or(default_min_frac);
    let max_frac           = p.max_frac.unwrap_or(1.0);
    let min_outgroup_count = p.min_outgroup_count.map(|v| resolve(v, out_size)).unwrap_or(0);
    let max_outgroup_count = p.max_outgroup_count.map(|v| resolve(v, out_size)).unwrap_or(default_max_outgroup_count);
    let min_outgroup_frac  = p.min_outgroup_frac.unwrap_or(0.0);
    let max_outgroup_frac  = p.max_outgroup_frac.unwrap_or(1.0);

    for (v, lo, hi) in [
        ("--min-frac/--max-frac",                        min_frac,           max_frac),
        ("--min-outgroup-frac/--max-outgroup-frac",       min_outgroup_frac,  max_outgroup_frac),
    ] {
        if !(0.0..=1.0).contains(&lo) || !(0.0..=1.0).contains(&hi) {
            return Err(format!("{v}: fraction values must be in [0.0, 1.0]"));
        }
        if lo > hi {
            return Err(format!("{v}: min ({lo}) is greater than max ({hi})"));
        }
    }
    if min_count > max_count {
        return Err(format!("--min-count/--max-count: min ({min_count}) is greater than max ({max_count})"));
    }
    if min_outgroup_count > max_outgroup_count {
        return Err(format!("--min-outgroup-count/--max-outgroup-count: min ({min_outgroup_count}) is greater than max ({max_outgroup_count})"));
    }

    Ok(GroupQuorumFilter {
        ingroup_idx,
        outgroup_idx,
        threshold: p.threshold,
        min_count,
        max_count,
        min_frac,
        max_frac,
        min_outgroup_count,
        max_outgroup_count,
        min_outgroup_frac,
        max_outgroup_frac,
    })
}
