//! Claim matching with type coercion for evaluation.
//!
//! The matcher compares extracted claims against expected claims, supporting:
//! - Tail-path matching for subjects
//! - Type coercion (string -> boolean, string -> number)
//! - Confidence thresholds

use stemedb_core::types::ObjectValue;
use tracing::debug;

use super::fixture::ExpectedClaim;
use crate::types::ExtractedClaim;

/// Result of matching expected claims against extracted claims.
#[derive(Debug, Clone, Default)]
pub struct MatchResult {
    /// Expected claims that were found in extracted claims.
    pub matched: Vec<(ExpectedClaim, ExtractedClaim)>,

    /// Expected claims that were NOT found.
    pub unmatched: Vec<ExpectedClaim>,
}

impl MatchResult {
    /// Number of true positives (matched expected claims).
    pub fn true_positives(&self) -> usize {
        self.matched.len()
    }

    /// Number of false negatives (unmatched expected claims).
    pub fn false_negatives(&self) -> usize {
        self.unmatched.len()
    }
}

/// Matches extracted claims against expected claims.
#[derive(Debug, Clone)]
pub struct ClaimMatcher {
    /// Tolerance for floating-point comparisons.
    pub float_tolerance: f64,
}

impl Default for ClaimMatcher {
    fn default() -> Self {
        Self { float_tolerance: 0.001 }
    }
}

impl ClaimMatcher {
    /// Create a new claim matcher with default settings.
    pub fn new() -> Self {
        Self::default()
    }

    /// Check if extracted claims satisfy must_contain requirements.
    ///
    /// Returns matched and unmatched expected claims.
    pub fn check_must_contain(
        &self,
        extracted: &[ExtractedClaim],
        expected: &[ExpectedClaim],
    ) -> MatchResult {
        let mut matched = Vec::new();
        let mut unmatched = Vec::new();

        for exp in expected {
            if let Some(claim) = self.find_matching_claim(extracted, exp) {
                matched.push((exp.clone(), claim.clone()));
            } else {
                unmatched.push(exp.clone());
            }
        }

        MatchResult { matched, unmatched }
    }

    /// Check if any extracted claims match must_not_contain requirements.
    ///
    /// Returns violations: (forbidden claim, matched extracted claim).
    pub fn check_must_not_contain(
        &self,
        extracted: &[ExtractedClaim],
        forbidden: &[ExpectedClaim],
    ) -> Vec<(ExpectedClaim, ExtractedClaim)> {
        let mut violations = Vec::new();

        for forbid in forbidden {
            if let Some(claim) = self.find_matching_claim(extracted, forbid) {
                violations.push((forbid.clone(), claim.clone()));
            }
        }

        violations
    }

    /// Find an extracted claim that matches an expected claim.
    fn find_matching_claim<'a>(
        &self,
        extracted: &'a [ExtractedClaim],
        expected: &ExpectedClaim,
    ) -> Option<&'a ExtractedClaim> {
        extracted.iter().find(|claim| {
            self.subject_matches(&claim.concept_path, &expected.subject)
                && claim.predicate == expected.predicate
                && self.value_matches(&claim.value, &expected.value)
                && self.confidence_ok(claim.confidence, expected.min_confidence)
        })
    }

    /// Check if subjects match using tail-path matching.
    ///
    /// Matching uses the last 2 path segments, so:
    /// - `code://rust/auth/tls/cert_verification` matches `tls/cert_verification`
    fn subject_matches(&self, extracted: &str, expected: &str) -> bool {
        let ext_tail = self.tail_path(extracted, 2);
        let exp_tail = self.tail_path(expected, 2);

        let matches = ext_tail == exp_tail;
        if matches {
            debug!(extracted = %extracted, expected = %expected, "Subject matched");
        }
        matches
    }

    /// Get the last N segments of a path.
    fn tail_path<'a>(&self, path: &'a str, n: usize) -> Vec<&'a str> {
        path.split('/').rev().take(n).collect::<Vec<_>>().into_iter().rev().collect()
    }

    /// Check if values match, with type coercion.
    fn value_matches(&self, extracted: &ObjectValue, expected: &serde_json::Value) -> bool {
        match (extracted, expected) {
            // Direct boolean match
            (ObjectValue::Boolean(e), serde_json::Value::Bool(x)) => *e == *x,

            // Direct number match
            (ObjectValue::Number(e), serde_json::Value::Number(x)) => {
                x.as_f64().map(|n| (e - n).abs() < self.float_tolerance).unwrap_or(false)
            }

            // Direct string match
            (ObjectValue::Text(e), serde_json::Value::String(x)) => e == x,

            // Coercion: extracted boolean, expected string
            (ObjectValue::Boolean(e), serde_json::Value::String(s)) => {
                self.coerce_to_bool(s).map(|b| *e == b).unwrap_or(false)
            }

            // Coercion: extracted string, expected boolean
            (ObjectValue::Text(e), serde_json::Value::Bool(x)) => {
                self.coerce_to_bool(e).map(|b| b == *x).unwrap_or(false)
            }

            // Coercion: extracted number, expected string
            (ObjectValue::Number(e), serde_json::Value::String(s)) => {
                s.parse::<f64>().map(|n| (e - n).abs() < self.float_tolerance).unwrap_or(false)
            }

            // Coercion: extracted string, expected number
            (ObjectValue::Text(e), serde_json::Value::Number(x)) => {
                if let (Ok(extracted_num), Some(expected_num)) = (e.parse::<f64>(), x.as_f64()) {
                    (extracted_num - expected_num).abs() < self.float_tolerance
                } else {
                    false
                }
            }

            // Array handling (match any element)
            (ObjectValue::Text(e), serde_json::Value::Array(arr)) => {
                arr.iter().any(|v| if let Some(s) = v.as_str() { e == s } else { false })
            }

            _ => false,
        }
    }

    /// Coerce a string to boolean.
    fn coerce_to_bool(&self, s: &str) -> Option<bool> {
        match s.to_lowercase().as_str() {
            "true" | "yes" | "on" | "enabled" | "1" => Some(true),
            "false" | "no" | "off" | "disabled" | "0" => Some(false),
            _ => None,
        }
    }

    /// Check if confidence meets the threshold.
    fn confidence_ok(&self, confidence: f32, min_confidence: Option<f32>) -> bool {
        match min_confidence {
            Some(min) => confidence >= min,
            None => true,
        }
    }
}

/// Count extra claims (false positives).
///
/// Extracted claims that don't match any expected claim.
pub fn count_false_positives(
    extracted: &[ExtractedClaim],
    expected: &[ExpectedClaim],
    acceptable_variants: &[ExpectedClaim],
    matcher: &ClaimMatcher,
) -> usize {
    let all_expected: Vec<_> = expected.iter().chain(acceptable_variants.iter()).cloned().collect();

    extracted
        .iter()
        .filter(|claim| {
            !all_expected.iter().any(|exp| {
                matcher.subject_matches(&claim.concept_path, &exp.subject)
                    && claim.predicate == exp.predicate
                    && matcher.value_matches(&claim.value, &exp.value)
            })
        })
        .count()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_extracted_claim(subject: &str, predicate: &str, value: ObjectValue) -> ExtractedClaim {
        ExtractedClaim {
            concept_path: subject.to_string(),
            predicate: predicate.to_string(),
            value,
            file: "test.py".to_string(),
            line: 1,
            matched_text: "test".to_string(),
            confidence: 0.9,
            description: String::new(),
        }
    }

    fn make_expected_claim(
        subject: &str,
        predicate: &str,
        value: serde_json::Value,
    ) -> ExpectedClaim {
        ExpectedClaim {
            subject: subject.to_string(),
            predicate: predicate.to_string(),
            value,
            min_confidence: None,
            rationale: None,
        }
    }

    #[test]
    fn test_exact_boolean_match() {
        let matcher = ClaimMatcher::new();
        let extracted = vec![make_extracted_claim(
            "code://python/tls/cert_verification",
            "enabled",
            ObjectValue::Boolean(false),
        )];
        let expected = vec![make_expected_claim(
            "tls/cert_verification",
            "enabled",
            serde_json::Value::Bool(false),
        )];

        let result = matcher.check_must_contain(&extracted, &expected);
        assert_eq!(result.matched.len(), 1);
        assert!(result.unmatched.is_empty());
    }

    #[test]
    fn test_tail_path_matching() {
        let matcher = ClaimMatcher::new();

        // Full path vs short path
        let extracted = vec![make_extracted_claim(
            "code://rust/myapp/auth/jwt/audience_validation",
            "enabled",
            ObjectValue::Boolean(false),
        )];
        let expected = vec![make_expected_claim(
            "jwt/audience_validation",
            "enabled",
            serde_json::Value::Bool(false),
        )];

        let result = matcher.check_must_contain(&extracted, &expected);
        assert_eq!(result.matched.len(), 1);
    }

    #[test]
    fn test_boolean_string_coercion() {
        let matcher = ClaimMatcher::new();

        // Extracted boolean, expected string "false"
        let extracted =
            vec![make_extracted_claim("tls/verify", "enabled", ObjectValue::Boolean(false))];
        let expected = vec![make_expected_claim(
            "tls/verify",
            "enabled",
            serde_json::Value::String("false".to_string()),
        )];

        let result = matcher.check_must_contain(&extracted, &expected);
        assert_eq!(result.matched.len(), 1);
    }

    #[test]
    fn test_string_boolean_coercion() {
        let matcher = ClaimMatcher::new();

        // Extracted string "yes", expected boolean true
        let extracted = vec![make_extracted_claim(
            "feature/debug",
            "enabled",
            ObjectValue::Text("yes".to_string()),
        )];
        let expected =
            vec![make_expected_claim("feature/debug", "enabled", serde_json::Value::Bool(true))];

        let result = matcher.check_must_contain(&extracted, &expected);
        assert_eq!(result.matched.len(), 1);
    }

    #[test]
    fn test_number_matching() {
        let matcher = ClaimMatcher::new();

        let extracted =
            vec![make_extracted_claim("db/pool_size", "value", ObjectValue::Number(50.0))];
        let expected = vec![make_expected_claim("db/pool_size", "value", serde_json::json!(50))];

        let result = matcher.check_must_contain(&extracted, &expected);
        assert_eq!(result.matched.len(), 1);
    }

    #[test]
    fn test_must_not_contain_violation() {
        let matcher = ClaimMatcher::new();

        let extracted = vec![make_extracted_claim(
            "tls/cert_verification",
            "enabled",
            ObjectValue::Boolean(true),
        )];
        let forbidden = vec![make_expected_claim(
            "tls/cert_verification",
            "enabled",
            serde_json::Value::Bool(true),
        )];

        let violations = matcher.check_must_not_contain(&extracted, &forbidden);
        assert_eq!(violations.len(), 1);
    }

    #[test]
    fn test_confidence_threshold() {
        let matcher = ClaimMatcher::new();

        let extracted = vec![{
            let mut claim =
                make_extracted_claim("tls/verify", "enabled", ObjectValue::Boolean(false));
            claim.confidence = 0.5; // Low confidence
            claim
        }];

        // With high min_confidence, should not match
        let expected = vec![ExpectedClaim {
            subject: "tls/verify".to_string(),
            predicate: "enabled".to_string(),
            value: serde_json::Value::Bool(false),
            min_confidence: Some(0.8),
            rationale: None,
        }];

        let result = matcher.check_must_contain(&extracted, &expected);
        assert!(result.matched.is_empty());
        assert_eq!(result.unmatched.len(), 1);
    }

    #[test]
    fn test_false_positive_counting() {
        let matcher = ClaimMatcher::new();

        let extracted = vec![
            make_extracted_claim("tls/verify", "enabled", ObjectValue::Boolean(false)),
            make_extracted_claim(
                "extra/claim",
                "unexpected",
                ObjectValue::Text("value".to_string()),
            ),
        ];

        let expected =
            vec![make_expected_claim("tls/verify", "enabled", serde_json::Value::Bool(false))];

        let fp_count = count_false_positives(&extracted, &expected, &[], &matcher);
        assert_eq!(fp_count, 1); // The "extra/claim" is a false positive
    }
}
