//! Inline claim marker parsing.
//!
//! This module handles parsing `@aphoria:claim` markers that capture claim intent
//! while writing code.
//!
//! ## Supported Syntax
//!
//! ### Basic Marker
//!
//! Marks a location where a claim should be authored:
//!
//! ```text
//! // @aphoria:claim Pool size MUST NOT exceed 50
//! const MAX_POOL_SIZE: u32 = 50;
//! ```
//!
//! ### Marker with Consequence
//!
//! Include the consequence after ` -- `:
//!
//! ```text
//! // @aphoria:claim Pool size MUST NOT exceed 50 -- OOM under sustained load
//! const MAX_POOL_SIZE: u32 = 50;
//! ```
//!
//! ### Marker with Category
//!
//! Specify category in brackets:
//!
//! ```text
//! // @aphoria:claim[safety] Pool size MUST NOT exceed 50 -- OOM
//! const MAX_POOL_SIZE: u32 = 50;
//! ```
//!
//! ### Already Formalized
//!
//! After formalization, marker becomes a reference:
//!
//! ```text
//! // @aphoria:claimed myapp-pool-max-001
//! const MAX_POOL_SIZE: u32 = 50;
//! ```
//!
//! ## Comment Variants
//!
//! The parser supports various comment styles:
//!
//! - `// @aphoria:claim` (Rust, Go, TypeScript, JavaScript, C, C++)
//! - `# @aphoria:claim` (Python, Ruby, YAML, Shell)
//! - `/* @aphoria:claim */` (CSS, block comments)
//! - `-- @aphoria:claim` (SQL)
//! - `<!-- @aphoria:claim -->` (HTML, XML)
//! - `; @aphoria:claim` (Assembly, Lisp)

use regex::Regex;
use serde_json::json;
use stemedb_core::types::ObjectValue;

use crate::types::{Language, Observation};

use super::traits::Extractor;

/// Predicate used for inline marker observations.
pub const INLINE_MARKER_PREDICATE: &str = "inline_marker";

/// Extractor for inline claim markers.
#[derive(Debug)]
pub struct InlineClaimMarkerExtractor {
    patterns: Vec<MarkerPattern>,
}

/// Pattern for a specific comment style.
#[derive(Debug)]
struct MarkerPattern {
    /// Regex to match @aphoria:claim markers.
    claim_regex: Regex,
    /// Regex to match @aphoria:claimed references (already formalized).
    claimed_regex: Regex,
}

/// Parsed marker fields.
#[derive(Debug, Clone)]
struct ParsedMarker {
    /// Optional category in brackets.
    category: Option<String>,
    /// The invariant statement (required).
    invariant: String,
    /// Optional consequence after ` -- `.
    consequence: Option<String>,
    /// Line number (1-indexed).
    line: usize,
}

/// Marker validation errors.
#[derive(Debug)]
enum MarkerValidationError {
    /// Invariant is empty or whitespace-only.
    EmptyInvariant,
}

/// Validates a parsed marker before converting to observation.
fn validate_marker(marker: &ParsedMarker) -> Result<(), MarkerValidationError> {
    if marker.invariant.trim().is_empty() {
        return Err(MarkerValidationError::EmptyInvariant);
    }

    // Future validations can be added here:
    // - Check invariant contains "MUST" or "MUST NOT"
    // - Validate category against known set
    // - Check minimum invariant length

    Ok(())
}

impl InlineClaimMarkerExtractor {
    /// Create a new inline claim marker extractor.
    #[allow(clippy::expect_used)] // Regex patterns are compile-time constants, validated by tests
    pub fn new() -> Self {
        let patterns = vec![
            // Rust, Go, C, TypeScript, JavaScript
            MarkerPattern {
                claim_regex: Regex::new(
                    r"//\s*@aphoria:claim(?:\[(\w+)\])?\s+([^-]+?)(?:\s+--\s+(.+))?\s*$",
                )
                .expect("valid regex"),
                claimed_regex: Regex::new(r"//\s*@aphoria:claimed\s+(\S+)").expect("valid regex"),
            },
            // Python, Ruby, Shell, YAML
            MarkerPattern {
                claim_regex: Regex::new(
                    r"#\s*@aphoria:claim(?:\[(\w+)\])?\s+([^-]+?)(?:\s+--\s+(.+))?\s*$",
                )
                .expect("valid regex"),
                claimed_regex: Regex::new(r"#\s*@aphoria:claimed\s+(\S+)").expect("valid regex"),
            },
            // SQL
            MarkerPattern {
                claim_regex: Regex::new(
                    r"--\s*@aphoria:claim(?:\[(\w+)\])?\s+([^-]+?)(?:\s+--\s+(.+))?\s*$",
                )
                .expect("valid regex"),
                claimed_regex: Regex::new(r"--\s*@aphoria:claimed\s+(\S+)").expect("valid regex"),
            },
            // C-style block comments (simplified - assumes single line)
            MarkerPattern {
                claim_regex: Regex::new(
                    r"/\*\s*@aphoria:claim(?:\[(\w+)\])?\s+([^-]+?)(?:\s+--\s+(.+))?\s*\*/",
                )
                .expect("valid regex"),
                claimed_regex: Regex::new(r"/\*\s*@aphoria:claimed\s+(\S+)\s*\*/")
                    .expect("valid regex"),
            },
            // HTML, XML
            MarkerPattern {
                claim_regex: Regex::new(
                    r"<!--\s*@aphoria:claim(?:\[(\w+)\])?\s+([^-]+?)(?:\s+--\s+(.+))?\s*-->",
                )
                .expect("valid regex"),
                claimed_regex: Regex::new(r"<!--\s*@aphoria:claimed\s+(\S+)\s*-->")
                    .expect("valid regex"),
            },
            // Assembly, Lisp
            MarkerPattern {
                claim_regex: Regex::new(
                    r";\s*@aphoria:claim(?:\[(\w+)\])?\s+([^-]+?)(?:\s+--\s+(.+))?\s*$",
                )
                .expect("valid regex"),
                claimed_regex: Regex::new(r";\s*@aphoria:claimed\s+(\S+)").expect("valid regex"),
            },
        ];

        Self { patterns }
    }

    /// Parse markers from content.
    fn parse_markers(&self, content: &str) -> Vec<ParsedMarker> {
        let mut markers = Vec::new();

        for (line_idx, line) in content.lines().enumerate() {
            let line_num = line_idx + 1; // 1-indexed

            // Skip already-formalized markers
            if self.is_claimed_reference(line) {
                continue;
            }

            // Try each pattern
            for pattern in &self.patterns {
                if let Some(captures) = pattern.claim_regex.captures(line) {
                    // Extract fields
                    let category = captures.get(1).map(|m| m.as_str().to_string());
                    let invariant = captures
                        .get(2)
                        .map(|m| m.as_str().trim().to_string())
                        .unwrap_or_default();
                    let consequence = captures.get(3).map(|m| m.as_str().trim().to_string());

                    let marker =
                        ParsedMarker { category, invariant, consequence, line: line_num };

                    // Validate before adding
                    if validate_marker(&marker).is_ok() {
                        markers.push(marker);
                    } else {
                        tracing::debug!(line = line_num, "Skipping invalid marker");
                    }
                    break; // Found a match, no need to try other patterns
                }
            }
        }

        markers
    }

    /// Check if a line contains an already-formalized reference.
    fn is_claimed_reference(&self, line: &str) -> bool {
        for pattern in &self.patterns {
            if pattern.claimed_regex.is_match(line) {
                return true;
            }
        }
        false
    }
}

impl Default for InlineClaimMarkerExtractor {
    fn default() -> Self {
        Self::new()
    }
}

impl Extractor for InlineClaimMarkerExtractor {
    fn name(&self) -> &str {
        "inline_claim_marker"
    }

    fn languages(&self) -> &[Language] {
        // Supports all languages since it's comment-based
        &[
            Language::Rust,
            Language::Go,
            Language::Python,
            Language::JavaScript,
            Language::TypeScript,
            Language::Cpp,
            Language::Java,
            Language::Ruby,
            Language::Php,
            Language::CSharp,
            Language::Unknown,
        ]
    }

    fn extract(
        &self,
        path_segments: &[String],
        content: &str,
        _language: Language,
        file: &str,
    ) -> Vec<Observation> {
        let markers = self.parse_markers(content);
        let mut observations = Vec::new();

        // Extract file stem for concept path
        let file_stem = std::path::Path::new(file)
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("unknown");

        for marker in markers {
            // Build concept path: project/_markers/file_stem/line
            let mut concept_path = path_segments.to_vec();
            concept_path.push("_markers".to_string());
            concept_path.push(file_stem.to_string());
            concept_path.push(marker.line.to_string());

            // Encode marker data as JSON value
            let value_json = json!({
                "invariant": marker.invariant,
                "consequence": marker.consequence,
                "category": marker.category,
            });

            observations.push(Observation {
                concept_path: format!("code://{}", concept_path.join("/")),
                predicate: INLINE_MARKER_PREDICATE.to_string(),
                value: ObjectValue::Text(value_json.to_string()),
                file: file.to_string(),
                line: marker.line,
                matched_text: marker.invariant.clone(),
                confidence: 1.0, // Explicit markers are high confidence
                description: "Inline claim marker (pending formalization)".to_string(),
            });
        }

        observations
    }

    fn verifiable_predicates(&self) -> Vec<(&str, &str)> {
        // Markers don't verify claims directly; they create pending claims
        vec![]
    }
}

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

    #[test]
    fn test_parse_basic_marker() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
// @aphoria:claim Pool size MUST NOT exceed 50
const MAX_POOL_SIZE: u32 = 50;
        "#;

        let observations = extractor.extract(&[], content, Language::Rust, "test.rs");
        assert_eq!(observations.len(), 1);
        assert_eq!(observations[0].predicate, "inline_marker");
        assert!(observations[0].matched_text.contains("Pool size MUST NOT exceed 50"));
    }

    #[test]
    fn test_parse_marker_with_consequence() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
// @aphoria:claim Pool size MUST NOT exceed 50 -- OOM under sustained load
const MAX_POOL_SIZE: u32 = 50;
        "#;

        let observations = extractor.extract(&[], content, Language::Rust, "test.rs");
        assert_eq!(observations.len(), 1);

        // Parse the JSON value to check consequence
        let value_str = match &observations[0].value {
            ObjectValue::Text(s) => s,
            _ => panic!("Expected Text value"),
        };
        let value: serde_json::Value = serde_json::from_str(value_str).expect("valid json");
        assert_eq!(value["consequence"], "OOM under sustained load");
    }

    #[test]
    fn test_parse_marker_with_category() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
// @aphoria:claim[safety] Pool size MUST NOT exceed 50
const MAX_POOL_SIZE: u32 = 50;
        "#;

        let observations = extractor.extract(&[], content, Language::Rust, "test.rs");
        assert_eq!(observations.len(), 1);

        let value_str = match &observations[0].value {
            ObjectValue::Text(s) => s,
            _ => panic!("Expected Text value"),
        };
        let value: serde_json::Value = serde_json::from_str(value_str).expect("valid json");
        assert_eq!(value["category"], "safety");
    }

    #[test]
    fn test_parse_marker_with_category_and_consequence() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
// @aphoria:claim[safety] Pool size MUST NOT exceed 50 -- OOM
const MAX_POOL_SIZE: u32 = 50;
        "#;

        let observations = extractor.extract(&[], content, Language::Rust, "test.rs");
        assert_eq!(observations.len(), 1);

        let value_str = match &observations[0].value {
            ObjectValue::Text(s) => s,
            _ => panic!("Expected Text value"),
        };
        let value: serde_json::Value = serde_json::from_str(value_str).expect("valid json");
        assert_eq!(value["category"], "safety");
        assert_eq!(value["consequence"], "OOM");
        assert_eq!(value["invariant"], "Pool size MUST NOT exceed 50");
    }

    #[test]
    fn test_skip_claimed_references() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
// @aphoria:claimed myapp-pool-max-001
const MAX_POOL_SIZE: u32 = 50;
        "#;

        let observations = extractor.extract(&[], content, Language::Rust, "test.rs");
        assert_eq!(observations.len(), 0, "Should skip formalized markers");
    }

    #[test]
    fn test_python_style_comments() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
# @aphoria:claim[security] API key MUST be validated -- Unauthorized access
api_key = "sk_test_123"
        "#;

        let observations = extractor.extract(&[], content, Language::Python, "test.py");
        assert_eq!(observations.len(), 1);
        assert!(observations[0].matched_text.contains("API key MUST be validated"));
    }

    #[test]
    fn test_sql_style_comments() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
-- @aphoria:claim Query timeout MUST be under 5s -- Performance degradation
SELECT * FROM users WHERE id = ?;
        "#;

        let observations = extractor.extract(&[], content, Language::Unknown, "test.sql");
        assert_eq!(observations.len(), 1);
    }

    #[test]
    fn test_c_style_block_comments() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
/* @aphoria:claim[safety] Buffer size MUST NOT exceed 1024 -- Overflow */
const char buffer[1024];
        "#;

        let observations = extractor.extract(&[], content, Language::Cpp, "test.c");
        assert_eq!(observations.len(), 1);
    }

    #[test]
    fn test_html_comments() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
<!-- @aphoria:claim CSP header MUST be set -- XSS vulnerability -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
        "#;

        let observations = extractor.extract(&[], content, Language::Unknown, "test.html");
        assert_eq!(observations.len(), 1);
    }

    #[test]
    fn test_multiple_markers() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
// @aphoria:claim[safety] Pool size MUST NOT exceed 50
const MAX_POOL_SIZE: u32 = 50;

// @aphoria:claim[security] TLS MUST be v1.2+
const MIN_TLS_VERSION: &str = "1.2";

// @aphoria:claimed already-formalized-001
const OTHER: u32 = 100;
        "#;

        let observations = extractor.extract(&[], content, Language::Rust, "test.rs");
        assert_eq!(observations.len(), 2, "Should find 2 markers, skip 1 claimed reference");
    }

    #[test]
    fn test_malformed_marker_empty_invariant() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = r#"
// @aphoria:claim
const MAX_POOL_SIZE: u32 = 50;
        "#;

        let observations = extractor.extract(&[], content, Language::Rust, "test.rs");
        assert_eq!(observations.len(), 0, "Should skip markers without invariant");
    }

    #[test]
    fn test_line_numbers() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = "line 1\n// @aphoria:claim Test marker\nline 3\n";

        let observations = extractor.extract(&[], content, Language::Rust, "test.rs");
        assert_eq!(observations.len(), 1);
        assert_eq!(observations[0].line, 2, "Marker should be on line 2");
    }

    #[test]
    fn test_concept_path_format() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = "// @aphoria:claim Test marker\n";

        let observations =
            extractor.extract(&["myapp".to_string()], content, Language::Rust, "src/pool.rs");
        assert_eq!(observations.len(), 1);
        assert!(
            observations[0].concept_path.contains("/_markers/pool/1"),
            "Concept path should include _markers/file_stem/line"
        );
    }

    #[test]
    fn test_json_value_structure() {
        let extractor = InlineClaimMarkerExtractor::new();
        let content = "// @aphoria:claim[safety] Test invariant -- Test consequence\n";

        let observations = extractor.extract(&[], content, Language::Rust, "test.rs");
        assert_eq!(observations.len(), 1);

        let value_str = match &observations[0].value {
            ObjectValue::Text(s) => s,
            _ => panic!("Expected Text value"),
        };
        let value: serde_json::Value = serde_json::from_str(value_str).expect("valid json");

        assert_eq!(value["invariant"], "Test invariant");
        assert_eq!(value["consequence"], "Test consequence");
        assert_eq!(value["category"], "safety");
    }
}
