//! Inline ignore comment parsing.
//!
//! This module handles parsing `// aphoria:ignore` comments that suppress
//! specific findings.
//!
//! ## Supported Syntax
//!
//! ### Single-Line Ignore
//!
//! Ignores the finding on the same line:
//!
//! ```text
//! let password = "test123"; // aphoria:ignore - Test credential
//! ```
//!
//! ### Next-Line Ignore
//!
//! Ignores the finding on the following line:
//!
//! ```text
//! // aphoria:ignore-next-line - Intentional for testing
//! .danger_accept_invalid_certs(true)
//! ```
//!
//! ### Block Ignore
//!
//! Ignores all findings within a block:
//!
//! ```text
//! // aphoria:ignore-block - Test fixtures
//! fn test_patterns() {
//!     let key = "sk_test_abc123";
//!     let password = "hunter2";
//! }
//! // aphoria:end-ignore
//! ```
//!
//! ## Comment Variants
//!
//! The parser supports various comment styles:
//!
//! - `// aphoria:ignore` (Rust, Go, TypeScript, JavaScript, C, C++)
//! - `# aphoria:ignore` (Python, Ruby, YAML, Shell)
//! - `/* aphoria:ignore */` (CSS, block comments)
//! - `-- aphoria:ignore` (SQL)
//! - `<!-- aphoria:ignore -->` (HTML, XML)

use std::collections::HashSet;

use regex::Regex;

/// Parses ignore comments from file content and tracks ignored line numbers.
#[derive(Debug)]
pub struct IgnoreCommentParser {
    /// Lines that should be ignored (1-indexed to match ExtractedClaim.line).
    ignored_lines: HashSet<usize>,
}

impl IgnoreCommentParser {
    /// Parse ignore comments from file content.
    ///
    /// Returns a parser with the set of ignored line numbers.
    pub fn parse(content: &str) -> Self {
        let mut ignored_lines = HashSet::new();

        // Track if we're in an ignore block
        let mut in_block = false;

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

            // Check for block start/end
            if contains_block_start(line) {
                in_block = true;
                // The block start line itself is not ignored (it's a comment)
                continue;
            }

            if contains_block_end(line) {
                in_block = false;
                // The block end line itself is not ignored (it's a comment)
                continue;
            }

            // If we're in a block, ignore this line
            if in_block {
                ignored_lines.insert(line_num);
                continue;
            }

            // Check for same-line ignore
            if contains_same_line_ignore(line) {
                ignored_lines.insert(line_num);
                continue;
            }

            // Check for next-line ignore (look at previous line)
            if line_idx > 0 {
                let prev_line = content.lines().nth(line_idx - 1).unwrap_or("");
                if contains_next_line_ignore(prev_line) {
                    ignored_lines.insert(line_num);
                }
            }
        }

        Self { ignored_lines }
    }

    /// Check if a line number should be ignored.
    ///
    /// Line numbers are 1-indexed (matching ExtractedClaim.line).
    pub fn is_ignored(&self, line: usize) -> bool {
        self.ignored_lines.contains(&line)
    }

    /// Get the set of ignored line numbers.
    #[allow(dead_code)]
    pub fn ignored_lines(&self) -> &HashSet<usize> {
        &self.ignored_lines
    }

    /// Get the count of ignored lines.
    #[allow(dead_code)]
    pub fn ignored_count(&self) -> usize {
        self.ignored_lines.len()
    }
}

/// Check if a line contains a same-line ignore comment.
fn contains_same_line_ignore(line: &str) -> bool {
    // Match variations:
    // // aphoria:ignore
    // # aphoria:ignore
    // /* aphoria:ignore */
    // -- aphoria:ignore
    // aphoria:ignore (bare, for XML comments etc.)
    //
    // But NOT:
    // // aphoria:ignore-next-line
    // // aphoria:ignore-block
    // // aphoria:end-ignore

    // Using lazy_static would be better, but we'll keep it simple
    let patterns = [
        r"//\s*aphoria:ignore(?:\s|$|-\s)",
        r"#\s*aphoria:ignore(?:\s|$|-\s)",
        r"/\*\s*aphoria:ignore(?:\s|$|-\s)",
        r"--\s*aphoria:ignore(?:\s|$|-\s)",
        r"<!--\s*aphoria:ignore(?:\s|$|-\s)",
    ];

    for pattern in &patterns {
        if let Ok(re) = Regex::new(pattern) {
            if re.is_match(line) {
                // Make sure it's not ignore-next-line or ignore-block
                if !line.contains("ignore-next-line") && !line.contains("ignore-block") {
                    return true;
                }
            }
        }
    }

    false
}

/// Check if a line contains a next-line ignore comment.
fn contains_next_line_ignore(line: &str) -> bool {
    let patterns = [
        r"//\s*aphoria:ignore-next-line",
        r"#\s*aphoria:ignore-next-line",
        r"/\*\s*aphoria:ignore-next-line",
        r"--\s*aphoria:ignore-next-line",
        r"<!--\s*aphoria:ignore-next-line",
    ];

    for pattern in &patterns {
        if let Ok(re) = Regex::new(pattern) {
            if re.is_match(line) {
                return true;
            }
        }
    }

    false
}

/// Check if a line contains a block start marker.
fn contains_block_start(line: &str) -> bool {
    let patterns = [
        r"//\s*aphoria:ignore-block",
        r"#\s*aphoria:ignore-block",
        r"/\*\s*aphoria:ignore-block",
        r"--\s*aphoria:ignore-block",
        r"<!--\s*aphoria:ignore-block",
    ];

    for pattern in &patterns {
        if let Ok(re) = Regex::new(pattern) {
            if re.is_match(line) {
                return true;
            }
        }
    }

    false
}

/// Check if a line contains a block end marker.
fn contains_block_end(line: &str) -> bool {
    let patterns = [
        r"//\s*aphoria:end-ignore",
        r"#\s*aphoria:end-ignore",
        r"/\*\s*aphoria:end-ignore",
        r"--\s*aphoria:end-ignore",
        r"<!--\s*aphoria:end-ignore",
    ];

    for pattern in &patterns {
        if let Ok(re) = Regex::new(pattern) {
            if re.is_match(line) {
                return true;
            }
        }
    }

    false
}

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

    #[test]
    fn test_same_line_ignore() {
        let content = r#"
let password = "test123"; // aphoria:ignore - Test credential
let real_password = "hunter2";
        "#;

        let parser = IgnoreCommentParser::parse(content);
        assert!(parser.is_ignored(2), "Line 2 should be ignored");
        assert!(!parser.is_ignored(3), "Line 3 should NOT be ignored");
    }

    #[test]
    fn test_same_line_ignore_with_reason() {
        let content = r#"
.danger_accept_invalid_certs(true) // aphoria:ignore - Required for self-signed certs
.danger_accept_invalid_certs(true)
        "#;

        let parser = IgnoreCommentParser::parse(content);
        assert!(parser.is_ignored(2), "Line 2 should be ignored");
        assert!(!parser.is_ignored(3), "Line 3 should NOT be ignored");
    }

    #[test]
    fn test_next_line_ignore() {
        let content = r#"
// aphoria:ignore-next-line - Intentional for testing
.danger_accept_invalid_certs(true)
.danger_accept_invalid_certs(true)
        "#;

        let parser = IgnoreCommentParser::parse(content);
        assert!(!parser.is_ignored(2), "Comment line itself not ignored");
        assert!(parser.is_ignored(3), "Line 3 should be ignored");
        assert!(!parser.is_ignored(4), "Line 4 should NOT be ignored");
    }

    #[test]
    fn test_block_ignore() {
        let content = r#"
// aphoria:ignore-block - Test fixtures
fn test_patterns() {
    let key = "sk_test_abc123";
    let password = "hunter2";
}
// aphoria:end-ignore
let real_key = "sk_live_real";
        "#;

        let parser = IgnoreCommentParser::parse(content);
        assert!(!parser.is_ignored(2), "Block start not ignored");
        assert!(parser.is_ignored(3), "Line 3 should be ignored");
        assert!(parser.is_ignored(4), "Line 4 should be ignored");
        assert!(parser.is_ignored(5), "Line 5 should be ignored");
        assert!(parser.is_ignored(6), "Line 6 should be ignored");
        assert!(!parser.is_ignored(7), "Block end not ignored");
        assert!(!parser.is_ignored(8), "Line 8 after block NOT ignored");
    }

    #[test]
    fn test_python_style_comments() {
        let content = r#"
password = "test123"  # aphoria:ignore - Test
# aphoria:ignore-next-line
api_key = "sk_test_123"
        "#;

        let parser = IgnoreCommentParser::parse(content);
        assert!(parser.is_ignored(2), "Python same-line ignore");
        assert!(parser.is_ignored(4), "Python next-line ignore");
    }

    #[test]
    fn test_c_style_block_comments() {
        let content = r#"
/* aphoria:ignore-block */
const char* password = "test";
const char* key = "secret";
/* aphoria:end-ignore */
const char* real = "production";
        "#;

        let parser = IgnoreCommentParser::parse(content);
        assert!(parser.is_ignored(3), "C-style block line 3");
        assert!(parser.is_ignored(4), "C-style block line 4");
        assert!(!parser.is_ignored(6), "After block end");
    }

    #[test]
    fn test_sql_style_comments() {
        let content = r#"
SELECT * FROM users; -- aphoria:ignore - Debug query
-- aphoria:ignore-next-line
SELECT password FROM secrets;
        "#;

        let parser = IgnoreCommentParser::parse(content);
        assert!(parser.is_ignored(2), "SQL same-line ignore");
        assert!(parser.is_ignored(4), "SQL next-line ignore");
    }

    #[test]
    fn test_empty_content() {
        let parser = IgnoreCommentParser::parse("");
        assert!(parser.ignored_lines().is_empty());
    }

    #[test]
    fn test_no_ignore_comments() {
        let content = r#"
let password = "test123";
let api_key = "sk_test_abc";
        "#;

        let parser = IgnoreCommentParser::parse(content);
        assert!(!parser.is_ignored(2));
        assert!(!parser.is_ignored(3));
    }

    #[test]
    fn test_ignore_does_not_match_other_patterns() {
        let content = r#"
// aphoria:ignore-next-line should not trigger same-line
.danger_accept_invalid_certs(true)
// aphoria:ignore-block should not trigger same-line
let x = 1;
// aphoria:end-ignore should not trigger same-line
        "#;

        let parser = IgnoreCommentParser::parse(content);
        // These comment lines should NOT be treated as same-line ignores
        assert!(!parser.is_ignored(2), "ignore-next-line is not same-line");
        // But the NEXT line should be ignored
        assert!(parser.is_ignored(3), "Next line after ignore-next-line");
        // Block content should be ignored
        assert!(parser.is_ignored(5), "Content in block");
    }

    #[test]
    fn test_multiple_ignore_regions() {
        let content = r#"
let a = 1; // aphoria:ignore
let b = 2;
// aphoria:ignore-next-line
let c = 3;
// aphoria:ignore-block
let d = 4;
// aphoria:end-ignore
let e = 5;
        "#;

        let parser = IgnoreCommentParser::parse(content);
        assert!(parser.is_ignored(2), "a ignored");
        assert!(!parser.is_ignored(3), "b not ignored");
        assert!(parser.is_ignored(5), "c ignored via next-line");
        assert!(parser.is_ignored(7), "d ignored via block");
        assert!(!parser.is_ignored(9), "e not ignored");
    }
}
