//! Compile-once, match-many matcher API.
//!
//! This module exposes a first-class boundary between *pattern
//! validation/compilation* and *repeated matching*:
//!
//! - [`MatcherOptions`] is the explicit, serialisable construction
//!   configuration.
//! - [`MatcherBuilder`] validates patterns up front and rejects invalid
//!   (blank or too-short) and duplicate patterns.
//! - [`CompiledMatcher`] is an immutable, `Send + Sync` matcher that can be
//!   reused across many queries and threads without rebuilding the automaton.
//!
//! Compiled internals are intentionally **not** serialisable; persist the
//! source patterns plus [`MatcherOptions`] instead and rebuild on load.
//!
//! Results are identical to [`crate::matcher::find_matches`] semantics
//! (leftmost-longest, ASCII case-insensitive by default, minimum pattern
//! length enforced).

use std::sync::Arc;

use aho_corasick::{AhoCorasick, MatchKind};
use serde::{Deserialize, Serialize};
use terraphim_types::NormalizedTerm;

use crate::matcher::Matched;
use crate::{Result, TerraphimAutomataError};

/// Default minimum pattern length, mirroring `find_matches`.
pub const DEFAULT_MIN_PATTERN_LENGTH: usize = 2;

/// Explicit construction configuration for a [`CompiledMatcher`].
///
/// This is the only configuration that needs persisting alongside the source
/// patterns; the compiled automaton itself can always be rebuilt from
/// patterns + options.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MatcherOptions {
    /// ASCII case-insensitive matching (defaults to `true`, matching the
    /// existing `find_matches` behaviour).
    pub case_insensitive: bool,
    /// Minimum accepted pattern length in bytes. Shorter patterns are
    /// rejected at build time.
    pub min_pattern_length: usize,
}

impl Default for MatcherOptions {
    fn default() -> Self {
        Self {
            case_insensitive: true,
            min_pattern_length: DEFAULT_MIN_PATTERN_LENGTH,
        }
    }
}

/// Validates patterns and compiles them into an immutable [`CompiledMatcher`].
///
/// The builder itself is cheap; the expensive automaton construction happens
/// exactly once in [`MatcherBuilder::build`].
#[derive(Debug, Clone)]
pub struct MatcherBuilder {
    options: MatcherOptions,
    patterns: Vec<(String, NormalizedTerm)>,
}

impl MatcherBuilder {
    /// Create a builder with explicit construction options.
    pub fn new(options: MatcherOptions) -> Self {
        Self {
            options,
            patterns: Vec::new(),
        }
    }

    /// Validate and add a pattern with its normalized term.
    ///
    /// Returns an error for blank patterns, patterns shorter than the
    /// configured minimum length, and duplicate patterns.
    pub fn insert(&mut self, pattern: String, term: NormalizedTerm) -> Result<&mut Self> {
        let trimmed = pattern.trim();
        let trimmed_len = pattern.len();
        if trimmed.is_empty() {
            return Err(TerraphimAutomataError::InvalidPattern {
                pattern: pattern.clone(),
                reason: "pattern is blank".to_string(),
            });
        }
        if trimmed_len < self.options.min_pattern_length {
            return Err(TerraphimAutomataError::InvalidPattern {
                pattern: pattern.clone(),
                reason: format!(
                    "pattern length {} is below the configured minimum {}",
                    trimmed_len, self.options.min_pattern_length
                ),
            });
        }
        if self
            .patterns
            .iter()
            .any(|(existing, _)| existing == &pattern)
        {
            return Err(TerraphimAutomataError::DuplicatePattern(pattern));
        }
        self.patterns.push((pattern, term));
        Ok(self)
    }

    /// Add a pattern, consuming the builder (chaining-friendly).
    pub fn with_pattern(mut self, pattern: String, term: NormalizedTerm) -> Result<Self> {
        self.insert(pattern, term)?;
        Ok(self)
    }

    /// Number of validated patterns staged so far.
    pub fn len(&self) -> usize {
        self.patterns.len()
    }

    /// Whether no patterns are staged.
    pub fn is_empty(&self) -> bool {
        self.patterns.is_empty()
    }

    /// Compile the staged patterns into an immutable [`CompiledMatcher`].
    pub fn build(self) -> Result<CompiledMatcher> {
        let ac = AhoCorasick::builder()
            .match_kind(MatchKind::LeftmostLongest)
            .ascii_case_insensitive(self.options.case_insensitive)
            .build(self.patterns.iter().map(|(p, _)| p.as_str()))?;
        Ok(CompiledMatcher {
            inner: Arc::new(CompiledMatcherInner {
                ac,
                patterns: self.patterns,
                options: self.options,
            }),
        })
    }
}

/// Shared immutable state of a [`CompiledMatcher`].
#[derive(Debug)]
struct CompiledMatcherInner {
    ac: AhoCorasick,
    patterns: Vec<(String, NormalizedTerm)>,
    options: MatcherOptions,
}

/// An immutable compiled matcher: build once, query many times.
///
/// Cloning is cheap (shared `Arc` internals). The matcher is `Send + Sync`
/// through ordinary Rust auto-traits (no unsafe impls), so concurrent
/// read-only use is safe. Result buffers may be supplied by the caller via
/// [`CompiledMatcher::push_matches`] to reuse allocations across queries.
#[derive(Debug, Clone)]
pub struct CompiledMatcher {
    inner: Arc<CompiledMatcherInner>,
}

// Auto-traits (Send/Sync) are derived from the fields; assert them so any
// future non-thread-safe field fails compilation here rather than at call sites.
const _: () = {
    const fn assert_send_sync<T: Send + Sync>() {}
    let _ = assert_send_sync::<CompiledMatcher>;
};

impl CompiledMatcher {
    /// Compile a matcher from a [`terraphim_types::Thesaurus`].
    ///
    /// This is the compiled counterpart of [`crate::matcher::find_matches`]:
    /// the thesaurus is consulted once, and the returned matcher reproduces
    /// identical results for any number of subsequent queries.
    pub fn from_thesaurus(
        thesaurus: &terraphim_types::Thesaurus,
        options: MatcherOptions,
    ) -> Result<Self> {
        let mut builder = MatcherBuilder::new(options);
        for (key, term) in thesaurus.into_iter() {
            builder.insert(key.to_string(), term.clone())?;
        }
        builder.build()
    }

    /// The construction configuration used to compile this matcher.
    pub fn options(&self) -> &MatcherOptions {
        &self.inner.options
    }

    /// The validated source patterns backing this matcher, in insertion order.
    ///
    /// Persist these (with [`MatcherOptions`]) to rebuild the matcher later.
    pub fn patterns(&self) -> &[(String, NormalizedTerm)] {
        &self.inner.patterns
    }

    /// Number of compiled patterns.
    pub fn len(&self) -> usize {
        self.inner.patterns.len()
    }

    /// Whether the matcher compiled zero patterns.
    pub fn is_empty(&self) -> bool {
        self.inner.patterns.is_empty()
    }

    /// Find matches in `text`, allocating a fresh result vector.
    ///
    /// Results are identical to [`crate::matcher::find_matches`] for the same
    /// patterns and options.
    pub fn find_matches(&self, text: &str, return_positions: bool) -> Result<Vec<Matched>> {
        let mut out = Vec::new();
        self.push_matches_impl(text, return_positions, &mut out)?;
        Ok(out)
    }

    /// Allocation-aware variant: append matches to a caller-owned buffer so
    /// the buffer's capacity is reused across queries.
    ///
    /// The buffer is not cleared first; call `buffer.clear()` between queries
    /// if a fresh result set is required. Because the buffer is caller-owned,
    /// concurrent queries must each use their own buffer.
    pub fn push_matches(&self, text: &str, buffer: &mut Vec<Matched>) -> Result<()> {
        self.push_matches_impl(text, false, buffer)
    }

    /// Iterate matches lazily without collecting them.
    pub fn find_iter<'text>(
        &'text self,
        text: &'text str,
    ) -> impl Iterator<Item = Matched> + use<'text> {
        self.inner
            .ac
            .find_iter(text)
            // Same word-boundary rule as the reference `find_matches`, so the
            // compiled matcher cannot drift from it (see matcher.rs).
            .filter(move |mat| crate::matcher::is_word_boundary_match(text, mat.start(), mat.end()))
            .map(move |mat| self.matched_for(mat, false))
    }

    fn push_matches_impl(
        &self,
        text: &str,
        return_positions: bool,
        out: &mut Vec<Matched>,
    ) -> Result<()> {
        for mat in self.inner.ac.find_iter(text) {
            // Mirrors the reference `find_matches` boundary rule.
            if !crate::matcher::is_word_boundary_match(text, mat.start(), mat.end()) {
                continue;
            }
            out.push(self.matched_for(mat, return_positions));
        }
        Ok(())
    }

    fn matched_for(&self, mat: aho_corasick::Match, return_positions: bool) -> Matched {
        let (term, normalized_term) = &self.inner.patterns[mat.pattern().as_usize()];
        Matched {
            term: term.clone(),
            normalized_term: normalized_term.clone(),
            pos: if return_positions {
                Some((mat.start(), mat.end()))
            } else {
                None
            },
        }
    }
}