//! FastAPI security extractor.
//!
//! Detects security misconfigurations in FastAPI applications:
//! - CORS with wildcard origin and credentials
//! - Debug mode enabled
//! - Weak password hashing
//! - Hardcoded secrets

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

use super::traits::build_claim;
use super::Extractor;
use crate::types::{ExtractedClaim, Language};

/// Extractor for FastAPI security misconfigurations.
#[allow(dead_code)]
pub struct FastApiSecurityExtractor {
    // CORS patterns
    cors_wildcard_credentials: Regex,

    // Debug mode
    debug_enabled: Regex,

    // Weak crypto
    weak_password_hash: Regex,

    // Hardcoded secrets
    hardcoded_secret: Regex,
    hardcoded_jwt_secret: Regex,

    // Missing auth (heuristic)
    admin_no_auth: Regex,
}

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

impl FastApiSecurityExtractor {
    /// Create a new FastAPI security extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // CORS with wildcard and credentials - multiline aware
            cors_wildcard_credentials: Regex::new(
                r#"allow_origins\s*=\s*\[\s*['"]?\*['"]?\s*\][^)]*allow_credentials\s*=\s*True"#,
            )
            .expect("valid regex"),

            // FastAPI debug mode
            debug_enabled: Regex::new(r"FastAPI\s*\([^)]*debug\s*=\s*True").expect("valid regex"),

            // Weak password hashing
            weak_password_hash: Regex::new(r"CryptContext\s*\([^)]*(?:md5|sha1)")
                .expect("valid regex"),

            // Hardcoded secrets
            hardcoded_secret: Regex::new(r#"SECRET_KEY\s*=\s*['"][^'"]{1,30}['"]"#)
                .expect("valid regex"),
            hardcoded_jwt_secret: Regex::new(r#"JWT_SECRET\s*=\s*['"][^'"]{1,30}['"]"#)
                .expect("valid regex"),

            // Admin routes without auth dependency
            admin_no_auth: Regex::new(
                r#"@(?:app|router)\.(?:get|post|put|delete)\s*\(\s*['"][^'"]*admin[^'"]*['"]"#,
            )
            .expect("valid regex"),
        }
    }
}

impl Extractor for FastApiSecurityExtractor {
    fn name(&self) -> &str {
        "fastapi_security"
    }

    fn languages(&self) -> &[Language] {
        &[Language::Python]
    }

    fn extract(
        &self,
        path_segments: &[String],
        content: &str,
        _language: Language,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();

        // Check if this looks like a FastAPI file
        let is_fastapi = content.contains("FastAPI")
            || content.contains("fastapi")
            || content.contains("APIRouter")
            || content.contains("@app.get")
            || content.contains("@app.post")
            || content.contains("@router.");

        if !is_fastapi {
            return claims;
        }

        // Multi-line pattern: CORS wildcard with credentials
        if let Some(m) = self.cors_wildcard_credentials.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["fastapi", "cors", "wildcard_credentials"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                &m.as_str()[..m.as_str().len().min(80)],
                1.0,
                "FastAPI CORS allows all origins with credentials - security vulnerability",
            ));
        }

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

            // FastAPI debug mode
            if let Some(m) = self.debug_enabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["fastapi", "debug_mode"],
                    "enabled",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "FastAPI debug mode enabled - must be False in production",
                ));
            }

            // Weak password hashing
            if let Some(m) = self.weak_password_hash.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["fastapi", "password_hash"],
                    "weak",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "FastAPI using weak password hash (MD5/SHA1)",
                ));
            }

            // Hardcoded SECRET_KEY
            if let Some(m) = self.hardcoded_secret.find(line) {
                // Skip environment variable references
                if !line.contains("os.environ") && !line.contains("os.getenv") {
                    claims.push(build_claim(
                        path_segments,
                        &["fastapi", "secret_key"],
                        "hardcoded",
                        ObjectValue::Boolean(true),
                        file,
                        line_num,
                        m.as_str(),
                        0.9,
                        "FastAPI SECRET_KEY appears hardcoded - use environment variable",
                    ));
                }
            }

            // Hardcoded JWT_SECRET
            if let Some(m) = self.hardcoded_jwt_secret.find(line) {
                // Skip environment variable references
                if !line.contains("os.environ") && !line.contains("os.getenv") {
                    claims.push(build_claim(
                        path_segments,
                        &["fastapi", "jwt_secret"],
                        "hardcoded",
                        ObjectValue::Boolean(true),
                        file,
                        line_num,
                        m.as_str(),
                        0.9,
                        "FastAPI JWT_SECRET appears hardcoded - use environment variable",
                    ));
                }
            }
        }

        claims
    }
}

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

    #[test]
    fn test_cors_wildcard_credentials() {
        let extractor = FastApiSecurityExtractor::new();
        let content = r#"
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
)
"#;

        let claims =
            extractor.extract(&["python".to_string()], content, Language::Python, "main.py");

        assert!(claims.iter().any(|c| c.concept_path.contains("wildcard_credentials")));
    }

    #[test]
    fn test_debug_enabled() {
        let extractor = FastApiSecurityExtractor::new();
        let content = r#"
from fastapi import FastAPI

app = FastAPI(debug=True)
"#;

        let claims =
            extractor.extract(&["python".to_string()], content, Language::Python, "main.py");

        assert!(claims.iter().any(|c| c.concept_path.contains("debug_mode")));
    }

    #[test]
    fn test_weak_password_hash() {
        let extractor = FastApiSecurityExtractor::new();
        let content = r#"
from fastapi import FastAPI
from passlib.context import CryptContext

app = FastAPI()
pwd_context = CryptContext(schemes=["md5_crypt"])
"#;

        let claims =
            extractor.extract(&["python".to_string()], content, Language::Python, "main.py");

        assert!(claims.iter().any(|c| c.concept_path.contains("password_hash")));
    }

    #[test]
    fn test_hardcoded_secret() {
        let extractor = FastApiSecurityExtractor::new();
        let content = r#"
from fastapi import FastAPI

app = FastAPI()
SECRET_KEY = "mysecretkey"
JWT_SECRET = "jwt-secret-key"
"#;

        let claims =
            extractor.extract(&["python".to_string()], content, Language::Python, "main.py");

        assert!(claims.iter().any(|c| c.concept_path.contains("secret_key")));
        assert!(claims.iter().any(|c| c.concept_path.contains("jwt_secret")));
    }

    #[test]
    fn test_non_fastapi_file_skipped() {
        let extractor = FastApiSecurityExtractor::new();
        let content = r#"
SECRET_KEY = "mysecretkey"
debug = True
"#;

        let claims =
            extractor.extract(&["python".to_string()], content, Language::Python, "random.py");

        // Should not detect since file doesn't look like FastAPI
        assert!(claims.is_empty());
    }
}
