use std::collections::HashMap;

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

// ── 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 `value` is equal to `pattern` or is a descendant of it in a `/`-separated hierarchy.
///
/// - Absolute pattern (`/a/b`): `value` must start with `/a/b` at a segment boundary.
/// - Bare segment (`b`): `value` must contain `b` as an exact segment anywhere.
fn path_matches(value: &str, pattern: &str) -> bool {
    if pattern.starts_with('/') {
        value == pattern
            || (value.starts_with(pattern)
                && value[pattern.len()..].starts_with('/'))
    } else {
        value.split('/').any(|seg| seg == pattern)
    }
}

// ── 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
    #[arg(long)]
    pub min_count: Option<usize>,

    /// Maximum number of ingroup genomes containing the k-mer
    #[arg(long)]
    pub max_count: Option<usize>,

    /// Minimum fraction of ingroup genomes containing the k-mer [0.0–1.0]
    #[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
    #[arg(long)]
    pub min_outgroup_count: Option<usize>,

    /// Maximum number of outgroup genomes containing the k-mer
    #[arg(long)]
    pub max_outgroup_count: Option<usize>,

    /// 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();
        vec![Box::new(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,
            },
        ))]
    }
}

pub struct GroupFilterParams {
    pub threshold:          u32,
    pub min_count:          Option<usize>,
    pub max_count:          Option<usize>,
    pub min_frac:           Option<f64>,
    pub max_frac:           Option<f64>,
    pub min_outgroup_count: Option<usize>,
    pub max_outgroup_count: Option<usize>,
    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,
) -> GroupQuorumFilter {
    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();

    GroupQuorumFilter {
        ingroup_idx,
        outgroup_idx,
        threshold:          p.threshold,
        min_count:          p.min_count.unwrap_or(0),
        max_count:          p.max_count.unwrap_or(in_size),
        min_frac:           p.min_frac.unwrap_or(0.0),
        max_frac:           p.max_frac.unwrap_or(1.0),
        min_outgroup_count: p.min_outgroup_count.unwrap_or(0),
        max_outgroup_count: p.max_outgroup_count.unwrap_or(out_size),
        min_outgroup_frac:  p.min_outgroup_frac.unwrap_or(0.0),
        max_outgroup_frac:  p.max_outgroup_frac.unwrap_or(1.0),
    }
}
