//! Filtering and deterministic sampling of the corpus into a fixture pack.
//!
//! A fixture pack is a stable subset of the corpus a developer can point test
//! generation at. Selection is pure and order-stable: entries are already
//! sorted by key in the corpus, so applying a filter and taking the first `N`
//! is reproducible without any RNG.

use serde::{Deserialize, Serialize};

use super::model::{
    EcosystemCorpus, EcosystemEntry, Maintenance, ProjectRole, TransportKind, CORPUS_SCHEMA_VERSION,
};

/// A filter over corpus entries. Every field is an AND constraint; `None`
/// (or an empty vector) means "do not constrain on this dimension".
#[derive(Debug, Clone, Default)]
pub struct SelectFilter {
    /// Keep only this transport.
    pub transport: Option<TransportKind>,
    /// Keep only this language (case-insensitive).
    pub language: Option<String>,
    /// Keep only this role.
    pub role: Option<ProjectRole>,
    /// Keep only entries carrying this category tag (case-insensitive).
    pub category: Option<String>,
    /// Keep only entries with at least this many advertised tools.
    pub min_tools: Option<u32>,
    /// Keep only actively maintained entries.
    pub maintained_only: bool,
    /// Keep only entries usable as fixtures (a real source URL).
    pub fixture_ready_only: bool,
}

impl SelectFilter {
    /// Whether a single entry satisfies every constraint.
    pub fn matches(&self, entry: &EcosystemEntry) -> bool {
        if let Some(t) = self.transport {
            if entry.transport != t {
                return false;
            }
        }
        if let Some(lang) = &self.language {
            match &entry.language {
                Some(l) if l.eq_ignore_ascii_case(lang) => {}
                _ => return false,
            }
        }
        if let Some(role) = self.role {
            if entry.role != role {
                return false;
            }
        }
        if let Some(cat) = &self.category {
            if !entry.categories.iter().any(|c| c.eq_ignore_ascii_case(cat)) {
                return false;
            }
        }
        if let Some(min) = self.min_tools {
            if entry.tool_count.unwrap_or(0) < min {
                return false;
            }
        }
        if self.maintained_only && entry.maintenance != Maintenance::Active {
            return false;
        }
        if self.fixture_ready_only && !entry.is_fixture_ready() {
            return false;
        }
        true
    }
}

/// A selected subset of the corpus, ready to drive fixture generation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FixturePack {
    /// Schema version, shared with the corpus.
    pub schema_version: String,
    /// A human-readable summary of the filter that produced this pack.
    pub filter_summary: String,
    /// How many corpus entries matched before the limit was applied.
    pub matched: usize,
    /// The selected entries (already sorted by key).
    pub entries: Vec<EcosystemEntry>,
}

/// Apply a filter and an optional limit to a corpus.
///
/// Because the corpus entries are already sorted by key, taking the first `N`
/// matches is deterministic across runs and machines.
pub fn select(
    corpus: &EcosystemCorpus,
    filter: &SelectFilter,
    limit: Option<usize>,
) -> FixturePack {
    let matched: Vec<EcosystemEntry> = corpus
        .entries
        .iter()
        .filter(|e| filter.matches(e))
        .cloned()
        .collect();
    let matched_count = matched.len();
    let entries = match limit {
        Some(n) => matched.into_iter().take(n).collect(),
        None => matched,
    };
    FixturePack {
        schema_version: CORPUS_SCHEMA_VERSION.to_string(),
        filter_summary: summarize(filter, limit),
        matched: matched_count,
        entries,
    }
}

/// Render a one-line, deterministic description of the active filter.
fn summarize(filter: &SelectFilter, limit: Option<usize>) -> String {
    let mut parts = Vec::new();
    if let Some(t) = filter.transport {
        parts.push(format!("transport={t:?}"));
    }
    if let Some(l) = &filter.language {
        parts.push(format!("language={l}"));
    }
    if let Some(r) = filter.role {
        parts.push(format!("role={r:?}"));
    }
    if let Some(c) = &filter.category {
        parts.push(format!("category={c}"));
    }
    if let Some(m) = filter.min_tools {
        parts.push(format!("min_tools={m}"));
    }
    if filter.maintained_only {
        parts.push("maintained".to_string());
    }
    if filter.fixture_ready_only {
        parts.push("fixture-ready".to_string());
    }
    if let Some(n) = limit {
        parts.push(format!("limit={n}"));
    }
    if parts.is_empty() {
        "all entries".to_string()
    } else {
        parts.join(", ")
    }
}
