//! Evidence source detection from commit messages and file paths.
//!
//! Parses commit messages, file paths, and project directories to detect
//! references to RFCs, OWASP categories, ADRs, and specification documents.

use std::path::Path;
use std::sync::OnceLock;

use regex::Regex;

use super::types::{EvidenceSource, PatternEvidence};

/// Get the RFC detection regex.
///
/// Matches patterns like:
/// - "RFC 7519"
/// - "RFC7519"
/// - "rfc-7519"
/// - "RFC 7519 Section 4.1"
/// - "RFC 7519 sec. 4.1.2"
fn rfc_pattern() -> &'static Regex {
    static PATTERN: OnceLock<Regex> = OnceLock::new();
    PATTERN.get_or_init(|| {
        // These regex patterns are compile-time constants validated by unit tests.
        // Invalid patterns will cause test failures before deployment.
        Regex::new(r"(?i)\bRFC[- ]?(\d{3,5})(?:\s+(?:Section|sec\.?)\s+(\d+(?:\.\d+)*))?")
            .unwrap_or_else(|_| unreachable!("RFC regex is a valid compile-time constant"))
    })
}

/// Get the OWASP detection regex.
///
/// Matches patterns like:
/// - "OWASP A03:2021"
/// - "A03:2021"
/// - "OWASP A03"
/// - "A01"
fn owasp_pattern() -> &'static Regex {
    static PATTERN: OnceLock<Regex> = OnceLock::new();
    PATTERN.get_or_init(|| {
        Regex::new(r"(?i)\b(?:OWASP[- ]?)?([A-Z]\d{2})(?::(\d{4}))?")
            .unwrap_or_else(|_| unreachable!("OWASP regex is a valid compile-time constant"))
    })
}

/// Get the ADR detection regex for commit messages.
///
/// Matches patterns like:
/// - "ADR-042"
/// - "ADR 042"
/// - "adr:042"
/// - "adr_42"
fn adr_commit_pattern() -> &'static Regex {
    static PATTERN: OnceLock<Regex> = OnceLock::new();
    PATTERN.get_or_init(|| {
        Regex::new(r"(?i)\bADR[- :_]?(\d+)")
            .unwrap_or_else(|_| unreachable!("ADR regex is a valid compile-time constant"))
    })
}

/// Get the requirement ID detection regex.
///
/// Matches patterns like:
/// - "REQ-API-001"
/// - "REQ-001"
/// - "REQUIREMENT-123"
fn requirement_pattern() -> &'static Regex {
    static PATTERN: OnceLock<Regex> = OnceLock::new();
    PATTERN.get_or_init(|| {
        Regex::new(r"(?i)\b(REQ(?:UIREMENT)?-[A-Z0-9-]+)")
            .unwrap_or_else(|_| unreachable!("Requirement regex is a valid compile-time constant"))
    })
}

/// Evidence detector for extracting evidence sources from text and files.
pub struct EvidenceDetector;

impl EvidenceDetector {
    /// Detect evidence sources from a commit message.
    ///
    /// Parses the message for RFC references, OWASP categories, and ADR links.
    /// The commit hash is included as a fallback Commit source.
    #[must_use]
    pub fn from_commit_message(message: &str, commit_hash: &str) -> PatternEvidence {
        let mut sources = Vec::new();

        // Detect RFC references
        for cap in rfc_pattern().captures_iter(message) {
            if let Some(number_match) = cap.get(1) {
                if let Ok(number) = number_match.as_str().parse::<u32>() {
                    let section = cap.get(2).map(|m| m.as_str().to_string());
                    sources.push(EvidenceSource::Rfc { number, section });
                }
            }
        }

        // Detect OWASP references
        for cap in owasp_pattern().captures_iter(message) {
            if let Some(id_match) = cap.get(1) {
                let id = id_match.as_str().to_uppercase();
                let year = cap.get(2).and_then(|m| m.as_str().parse::<u16>().ok());
                sources.push(EvidenceSource::Owasp { id, year });
            }
        }

        // Detect ADR references
        for cap in adr_commit_pattern().captures_iter(message) {
            if let Some(id_match) = cap.get(1) {
                sources.push(EvidenceSource::Adr { id: id_match.as_str().to_string(), path: None });
            }
        }

        // Always include commit as fallback source
        let message_excerpt = if message.len() > 100 {
            Some(format!("{}...", &message[..100]))
        } else if !message.is_empty() {
            Some(message.to_string())
        } else {
            None
        };

        sources.push(EvidenceSource::Commit { hash: commit_hash.to_string(), message_excerpt });

        PatternEvidence::from_sources(sources)
    }

    /// Detect evidence source from a file path.
    ///
    /// Identifies ADR files, spec files, and decision logs based on path patterns.
    #[must_use]
    pub fn from_file_path(path: &Path) -> Option<EvidenceSource> {
        let path_str = path.to_string_lossy().to_lowercase();
        let file_name = path.file_name()?.to_string_lossy().to_lowercase();

        // ADR files: docs/adr/*.md, docs/decisions/*.md, adr-*.md
        if path_str.contains("/adr/")
            || path_str.contains("/decisions/")
            || file_name.starts_with("adr-")
            || file_name.starts_with("adr_")
        {
            // Extract ADR ID from filename if possible
            let id = Self::extract_adr_id_from_filename(&file_name);
            return Some(EvidenceSource::Adr {
                id: id.unwrap_or_else(|| "unknown".to_string()),
                path: Some(path.to_string_lossy().to_string()),
            });
        }

        // Spec files: specs/*.md, *.spec.md, *.spec.yaml
        if path_str.contains("/specs/")
            || path_str.starts_with("specs/")
            || file_name.contains(".spec.")
            || file_name.starts_with("spec-")
        {
            return Some(EvidenceSource::Spec {
                path: path.to_string_lossy().to_string(),
                requirement_id: None,
            });
        }

        // Decision logs: decision-log.md, DECISIONS.md
        if file_name == "decision-log.md"
            || file_name == "decisions.md"
            || file_name == "decision_log.md"
        {
            return Some(EvidenceSource::DecisionLog {
                path: path.to_string_lossy().to_string(),
                entry_id: None,
            });
        }

        None
    }

    /// Scan a project directory for evidence sources.
    ///
    /// Walks the directory tree looking for ADRs, specs, and decision logs.
    #[must_use]
    pub fn scan_project_directory(project_root: &Path) -> Vec<EvidenceSource> {
        let mut sources = Vec::new();

        // Common evidence directories
        let evidence_dirs =
            ["docs/adr", "docs/decisions", "docs/adrs", "adr", "decisions", "specs", "spec"];

        for dir_name in evidence_dirs {
            let dir_path = project_root.join(dir_name);
            if dir_path.is_dir() {
                if let Ok(entries) = std::fs::read_dir(&dir_path) {
                    for entry in entries.flatten() {
                        let path = entry.path();
                        if path.is_file() {
                            if let Some(source) = Self::from_file_path(&path) {
                                sources.push(source);
                            }
                        }
                    }
                }
            }
        }

        // Check for decision logs at root
        let decision_log_names = ["decision-log.md", "DECISIONS.md", "decision_log.md"];
        for name in decision_log_names {
            let log_path = project_root.join(name);
            if log_path.is_file() {
                sources.push(EvidenceSource::DecisionLog {
                    path: log_path.to_string_lossy().to_string(),
                    entry_id: None,
                });
            }
        }

        sources
    }

    /// Extract requirement IDs from text content.
    ///
    /// Useful for finding requirement references in spec files.
    #[must_use]
    pub fn extract_requirement_ids(content: &str) -> Vec<String> {
        requirement_pattern()
            .captures_iter(content)
            .filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
            .collect()
    }

    /// Extract ADR ID from filename.
    fn extract_adr_id_from_filename(filename: &str) -> Option<String> {
        // Try patterns like: adr-042.md, 042-decision.md, adr_42.md
        let patterns = [r"(?i)adr[-_]?(\d+)", r"^(\d+)[-_]"];

        for pattern in patterns {
            if let Ok(re) = Regex::new(pattern) {
                if let Some(cap) = re.captures(filename) {
                    if let Some(id) = cap.get(1) {
                        return Some(id.as_str().to_string());
                    }
                }
            }
        }

        None
    }
}

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

    #[test]
    fn test_rfc_detection_simple() {
        let evidence =
            EvidenceDetector::from_commit_message("Implement JWT per RFC 7519", "abc123");

        let rfc_sources: Vec<_> =
            evidence.sources.iter().filter(|s| matches!(s, EvidenceSource::Rfc { .. })).collect();

        assert_eq!(rfc_sources.len(), 1);
        if let EvidenceSource::Rfc { number, section } = rfc_sources[0] {
            assert_eq!(*number, 7519);
            assert!(section.is_none());
        }
    }

    #[test]
    fn test_rfc_detection_with_section() {
        let evidence =
            EvidenceDetector::from_commit_message("See RFC 7519 Section 4.1 for details", "abc123");

        let rfc_sources: Vec<_> =
            evidence.sources.iter().filter(|s| matches!(s, EvidenceSource::Rfc { .. })).collect();

        assert_eq!(rfc_sources.len(), 1);
        if let EvidenceSource::Rfc { number, section } = rfc_sources[0] {
            assert_eq!(*number, 7519);
            assert_eq!(section.as_deref(), Some("4.1"));
        }
    }

    #[test]
    fn test_rfc_detection_compact() {
        let evidence = EvidenceDetector::from_commit_message("RFC7519 compliance", "abc123");

        let rfc_sources: Vec<_> =
            evidence.sources.iter().filter(|s| matches!(s, EvidenceSource::Rfc { .. })).collect();

        assert_eq!(rfc_sources.len(), 1);
    }

    #[test]
    fn test_owasp_detection() {
        let evidence =
            EvidenceDetector::from_commit_message("Fix OWASP A03:2021 injection issue", "abc123");

        let owasp_sources: Vec<_> =
            evidence.sources.iter().filter(|s| matches!(s, EvidenceSource::Owasp { .. })).collect();

        assert_eq!(owasp_sources.len(), 1);
        if let EvidenceSource::Owasp { id, year } = owasp_sources[0] {
            assert_eq!(id, "A03");
            assert_eq!(*year, Some(2021));
        }
    }

    #[test]
    fn test_owasp_detection_without_year() {
        let evidence = EvidenceDetector::from_commit_message("Address A01 vulnerability", "abc123");

        let owasp_sources: Vec<_> =
            evidence.sources.iter().filter(|s| matches!(s, EvidenceSource::Owasp { .. })).collect();

        assert_eq!(owasp_sources.len(), 1);
        if let EvidenceSource::Owasp { id, year } = owasp_sources[0] {
            assert_eq!(id, "A01");
            assert!(year.is_none());
        }
    }

    #[test]
    fn test_adr_detection() {
        let evidence =
            EvidenceDetector::from_commit_message("Implement ADR-042 for auth flow", "abc123");

        let adr_sources: Vec<_> =
            evidence.sources.iter().filter(|s| matches!(s, EvidenceSource::Adr { .. })).collect();

        assert_eq!(adr_sources.len(), 1);
        if let EvidenceSource::Adr { id, path } = adr_sources[0] {
            assert_eq!(id, "042");
            assert!(path.is_none());
        }
    }

    #[test]
    fn test_commit_always_included() {
        let evidence = EvidenceDetector::from_commit_message("Just a regular commit", "abc123");

        let commit_sources: Vec<_> = evidence
            .sources
            .iter()
            .filter(|s| matches!(s, EvidenceSource::Commit { .. }))
            .collect();

        assert_eq!(commit_sources.len(), 1);
    }

    #[test]
    fn test_file_path_adr() {
        let path = Path::new("docs/adr/042-use-jwt.md");
        let source = EvidenceDetector::from_file_path(path);

        assert!(source.is_some());
        if let Some(EvidenceSource::Adr { id, path: _ }) = source {
            assert_eq!(id, "042");
        }
    }

    #[test]
    fn test_file_path_spec() {
        let path = Path::new("specs/api-design.md");
        let source = EvidenceDetector::from_file_path(path);

        assert!(source.is_some());
        assert!(matches!(source, Some(EvidenceSource::Spec { .. })));
    }

    #[test]
    fn test_file_path_decision_log() {
        let path = Path::new("decision-log.md");
        let source = EvidenceDetector::from_file_path(path);

        assert!(source.is_some());
        assert!(matches!(source, Some(EvidenceSource::DecisionLog { .. })));
    }

    #[test]
    fn test_file_path_no_match() {
        let path = Path::new("src/main.rs");
        let source = EvidenceDetector::from_file_path(path);

        assert!(source.is_none());
    }

    #[test]
    fn test_multiple_sources_in_message() {
        let evidence = EvidenceDetector::from_commit_message(
            "Implement RFC 7519 and fix OWASP A03:2021 per ADR-042",
            "abc123",
        );

        // Should have RFC, OWASP, ADR, and Commit
        assert!(evidence.source_count() >= 4);
    }

    #[test]
    fn test_extract_requirement_ids() {
        let content = "This implements REQ-API-001 and REQ-AUTH-002.";
        let ids = EvidenceDetector::extract_requirement_ids(content);

        assert_eq!(ids.len(), 2);
        assert!(ids.contains(&"REQ-API-001".to_string()));
        assert!(ids.contains(&"REQ-AUTH-002".to_string()));
    }

    #[test]
    fn test_evidence_level_from_commit() {
        let evidence = EvidenceDetector::from_commit_message("Regular commit", "abc123");
        assert_eq!(evidence.effective_level(), super::super::types::EvidenceLevel::Commit);
    }

    #[test]
    fn test_evidence_level_from_rfc() {
        let evidence = EvidenceDetector::from_commit_message("Implement RFC 7519", "abc123");
        assert_eq!(evidence.effective_level(), super::super::types::EvidenceLevel::Standard);
    }
}
