//! Django security extractor.
//!
//! Detects security misconfigurations in Django applications:
//! - Debug mode enabled in production
//! - Permissive ALLOWED_HOSTS
//! - Insecure cookie settings
//! - CSRF protection disabled
//! - Weak password hashers
//! - SQL injection via raw queries

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

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

/// Extractor for Django security misconfigurations.
pub struct DjangoSecurityExtractor {
    // Config patterns (settings.py)
    debug_enabled: Regex,
    allowed_hosts_wildcard: Regex,
    allowed_hosts_empty: Regex,
    session_cookie_secure_false: Regex,
    csrf_cookie_secure_false: Regex,
    session_cookie_httponly_false: Regex,
    secure_ssl_redirect_false: Regex,
    secure_hsts_disabled: Regex,
    x_frame_options_disabled: Regex,
    xss_filter_disabled: Regex,
    content_type_nosniff_disabled: Regex,
    weak_password_hasher: Regex,

    // Code patterns
    csrf_exempt: Regex,
    raw_sql_fstring: Regex,
    raw_sql_percent: Regex,
    extra_where: Regex,
    hardcoded_secret_key: Regex,
    eval_exec: Regex,
}

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

impl DjangoSecurityExtractor {
    /// Create a new Django security extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Config patterns
            debug_enabled: Regex::new(r"(?i)^\s*DEBUG\s*=\s*True").expect("valid regex"),
            allowed_hosts_wildcard: Regex::new(r#"(?i)ALLOWED_HOSTS\s*=\s*\[\s*['"]?\*['"]?\s*\]"#)
                .expect("valid regex"),
            allowed_hosts_empty: Regex::new(r"(?i)ALLOWED_HOSTS\s*=\s*\[\s*\]")
                .expect("valid regex"),
            session_cookie_secure_false: Regex::new(r"(?i)SESSION_COOKIE_SECURE\s*=\s*False")
                .expect("valid regex"),
            csrf_cookie_secure_false: Regex::new(r"(?i)CSRF_COOKIE_SECURE\s*=\s*False")
                .expect("valid regex"),
            session_cookie_httponly_false: Regex::new(r"(?i)SESSION_COOKIE_HTTPONLY\s*=\s*False")
                .expect("valid regex"),
            secure_ssl_redirect_false: Regex::new(r"(?i)SECURE_SSL_REDIRECT\s*=\s*False")
                .expect("valid regex"),
            secure_hsts_disabled: Regex::new(r"(?i)SECURE_HSTS_SECONDS\s*=\s*0")
                .expect("valid regex"),
            x_frame_options_disabled: Regex::new(
                r#"(?i)X_FRAME_OPTIONS\s*=\s*['"]?(?:ALLOWALL|None)['"]?"#,
            )
            .expect("valid regex"),
            xss_filter_disabled: Regex::new(r"(?i)SECURE_BROWSER_XSS_FILTER\s*=\s*False")
                .expect("valid regex"),
            content_type_nosniff_disabled: Regex::new(
                r"(?i)SECURE_CONTENT_TYPE_NOSNIFF\s*=\s*False",
            )
            .expect("valid regex"),
            weak_password_hasher: Regex::new(r"(?i)(?:MD5PasswordHasher|SHA1PasswordHasher)")
                .expect("valid regex"),

            // Code patterns
            csrf_exempt: Regex::new(r"@csrf_exempt").expect("valid regex"),
            raw_sql_fstring: Regex::new(r#"\.objects\.raw\s*\(\s*f["']"#).expect("valid regex"),
            raw_sql_percent: Regex::new(r#"\.objects\.raw\s*\([^)]*%\s*"#).expect("valid regex"),
            extra_where: Regex::new(r"\.extra\s*\(\s*(?:where|select)\s*=").expect("valid regex"),
            hardcoded_secret_key: Regex::new(r#"(?i)SECRET_KEY\s*=\s*['"][^'"]{1,50}['"]"#)
                .expect("valid regex"),
            eval_exec: Regex::new(r"(?:eval|exec)\s*\(\s*(?:request\.|params)")
                .expect("valid regex"),
        }
    }

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

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

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

            // ALLOWED_HOSTS = ['*']
            if let Some(m) = self.allowed_hosts_wildcard.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "allowed_hosts"],
                    "config_value",
                    ObjectValue::Text("*".to_string()),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Django ALLOWED_HOSTS allows all hosts - security vulnerability",
                ));
            }

            // ALLOWED_HOSTS = [] (empty in production is dangerous)
            if let Some(m) = self.allowed_hosts_empty.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "allowed_hosts"],
                    "config_value",
                    ObjectValue::Text("empty".to_string()),
                    file,
                    line_num,
                    m.as_str(),
                    0.8,
                    "Django ALLOWED_HOSTS is empty - may be insecure in production",
                ));
            }

            // SESSION_COOKIE_SECURE = False
            if let Some(m) = self.session_cookie_secure_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "session_cookie", "secure"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Django session cookie not marked secure - sent over HTTP",
                ));
            }

            // CSRF_COOKIE_SECURE = False
            if let Some(m) = self.csrf_cookie_secure_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "csrf_cookie", "secure"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Django CSRF cookie not marked secure - sent over HTTP",
                ));
            }

            // SESSION_COOKIE_HTTPONLY = False
            if let Some(m) = self.session_cookie_httponly_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "session_cookie", "httponly"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Django session cookie accessible to JavaScript - XSS risk",
                ));
            }

            // SECURE_SSL_REDIRECT = False
            if let Some(m) = self.secure_ssl_redirect_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "ssl_redirect"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Django HTTPS redirect disabled",
                ));
            }

            // SECURE_HSTS_SECONDS = 0
            if let Some(m) = self.secure_hsts_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "hsts"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Django HSTS disabled - browsers won't enforce HTTPS",
                ));
            }

            // X_FRAME_OPTIONS disabled
            if let Some(m) = self.x_frame_options_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "x_frame_options"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Django X-Frame-Options disabled - clickjacking vulnerability",
                ));
            }

            // XSS filter disabled
            if let Some(m) = self.xss_filter_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "xss_filter"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    0.8,
                    "Django XSS filter disabled",
                ));
            }

            // Content-Type nosniff disabled
            if let Some(m) = self.content_type_nosniff_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "content_type_nosniff"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    0.8,
                    "Django Content-Type nosniff disabled - MIME sniffing vulnerability",
                ));
            }

            // Weak password hasher
            if let Some(m) = self.weak_password_hasher.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "password_hasher"],
                    "algorithm",
                    ObjectValue::Text(m.as_str().to_string()),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Django using weak password hasher (MD5/SHA1)",
                ));
            }
        }

        claims
    }

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

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

            // @csrf_exempt decorator
            if let Some(m) = self.csrf_exempt.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "csrf"],
                    "exempt",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Django CSRF protection disabled via @csrf_exempt",
                ));
            }

            // Raw SQL with f-string
            if let Some(m) = self.raw_sql_fstring.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "sql_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Django raw SQL with f-string interpolation - SQL injection risk",
                ));
            }

            // Raw SQL with % formatting
            if let Some(m) = self.raw_sql_percent.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "sql_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Django raw SQL with % formatting - SQL injection risk",
                ));
            }

            // extra() with user input
            if let Some(m) = self.extra_where.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "orm_extra"],
                    "used",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.7,
                    "Django .extra() used - potential SQL injection if user input included",
                ));
            }

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

            // eval/exec with request
            if let Some(m) = self.eval_exec.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["django", "code_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Django eval/exec with user input - critical code injection vulnerability",
                ));
            }
        }

        claims
    }
}

impl Extractor for DjangoSecurityExtractor {
    fn name(&self) -> &str {
        "django_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 Django file
        let is_django = content.contains("django")
            || content.contains("Django")
            || file.contains("settings")
            || content.contains("ALLOWED_HOSTS")
            || content.contains("INSTALLED_APPS");

        if !is_django {
            return claims;
        }

        claims.extend(self.check_config_patterns(path_segments, content, file));
        claims.extend(self.check_code_patterns(path_segments, content, file));

        claims
    }
}

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

    #[test]
    fn test_debug_enabled() {
        let extractor = DjangoSecurityExtractor::new();
        let content = r#"
# Django settings
DEBUG = True
ALLOWED_HOSTS = ['localhost']
"#;

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

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

    #[test]
    fn test_allowed_hosts_wildcard() {
        let extractor = DjangoSecurityExtractor::new();
        let content = r#"
# Django settings
DEBUG = False
ALLOWED_HOSTS = ['*']
"#;

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

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

    #[test]
    fn test_insecure_cookies() {
        let extractor = DjangoSecurityExtractor::new();
        let content = r#"
# Django settings
ALLOWED_HOSTS = ['example.com']
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = False
"#;

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

        assert!(claims.iter().any(|c| c.concept_path.contains("session_cookie/secure")));
        assert!(claims.iter().any(|c| c.concept_path.contains("csrf_cookie/secure")));
        assert!(claims.iter().any(|c| c.concept_path.contains("session_cookie/httponly")));
    }

    #[test]
    fn test_csrf_exempt() {
        let extractor = DjangoSecurityExtractor::new();
        let content = r#"
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def my_view(request):
    pass
"#;

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

        assert!(claims.iter().any(|c| c.concept_path.contains("csrf") && c.predicate == "exempt"));
    }

    #[test]
    fn test_raw_sql_injection() {
        let extractor = DjangoSecurityExtractor::new();
        let content = r#"
from django.db import models

def get_user(user_id):
    return User.objects.raw(f"SELECT * FROM users WHERE id = {user_id}")
"#;

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

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

    #[test]
    fn test_weak_password_hasher() {
        let extractor = DjangoSecurityExtractor::new();
        let content = r#"
# Django settings
ALLOWED_HOSTS = ['example.com']
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.MD5PasswordHasher',
]
"#;

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

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

    #[test]
    fn test_non_django_file_skipped() {
        let extractor = DjangoSecurityExtractor::new();
        let content = r#"
DEBUG = True
"#;

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

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