//! Spring Boot security extractor.
//!
//! Detects security misconfigurations in Spring Boot applications:
//! - CSRF protection disabled
//! - Security disabled
//! - Permissive access controls
//! - Dev tools in production
//! - Actuator endpoints exposed
//! - Session fixation vulnerabilities

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

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

/// Extractor for Spring Boot security misconfigurations.
#[allow(dead_code)]
pub struct SpringSecurityExtractor {
    // Config patterns (YAML/Properties)
    security_disabled: Regex,
    csrf_disabled_config: Regex,
    frame_options_disabled: Regex,
    xss_protection_disabled: Regex,
    content_type_disabled: Regex,
    actuator_exposed: Regex,
    devtools_enabled: Regex,
    health_details_exposed: Regex,

    // Java code patterns
    csrf_disabled_java: Regex,
    permit_all_wildcard: Regex,
    any_request_permit_all: Regex,
    frame_options_disabled_java: Regex,
    session_fixation_none: Regex,
    weak_remember_me: Regex,
    authenticated_none: Regex,
    http_basic_disabled: Regex,
    form_login_disabled: Regex,
    headers_disabled: Regex,
}

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

impl SpringSecurityExtractor {
    /// Create a new Spring Boot security extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Config patterns
            security_disabled: Regex::new(r"(?i)security[.\s:]*basic[.\s:]*enabled[.\s:=]+false")
                .expect("valid regex"),
            csrf_disabled_config: Regex::new(r"(?i)csrf[.\s:]*enabled[.\s:=]+false")
                .expect("valid regex"),
            frame_options_disabled: Regex::new(
                r"(?i)frame-options[.\s:=]+(?:DISABLE|disable|none)",
            )
            .expect("valid regex"),
            xss_protection_disabled: Regex::new(r"(?i)xss-protection[.\s:=]+false")
                .expect("valid regex"),
            content_type_disabled: Regex::new(
                r"(?i)content-type-options[.\s:=]+(?:DISABLE|disable|none)",
            )
            .expect("valid regex"),
            actuator_exposed: Regex::new(r#"(?i)exposure[.\s:]*include[.\s:=]+['"]?\*['"]?"#)
                .expect("valid regex"),
            devtools_enabled: Regex::new(r"(?i)devtools[.\s:]*restart[.\s:]*enabled[.\s:=]+true")
                .expect("valid regex"),
            health_details_exposed: Regex::new(r"(?i)show-details[.\s:=]+(?:always|ALWAYS)")
                .expect("valid regex"),

            // Java code patterns
            csrf_disabled_java: Regex::new(r"\.csrf\s*\(\s*\)\s*\.\s*disable\s*\(\s*\)")
                .expect("valid regex"),
            permit_all_wildcard: Regex::new(
                r#"\.antMatchers\s*\(\s*['"]/\*\*['"]\s*\)\s*\.\s*permitAll\s*\(\s*\)"#,
            )
            .expect("valid regex"),
            any_request_permit_all: Regex::new(
                r"\.anyRequest\s*\(\s*\)\s*\.\s*permitAll\s*\(\s*\)",
            )
            .expect("valid regex"),
            frame_options_disabled_java: Regex::new(
                r"\.frameOptions\s*\(\s*\)\s*\.\s*disable\s*\(\s*\)",
            )
            .expect("valid regex"),
            session_fixation_none: Regex::new(r"\.sessionFixation\s*\(\s*\)\s*\.\s*none\s*\(\s*\)")
                .expect("valid regex"),
            weak_remember_me: Regex::new(
                r#"\.rememberMe\s*\(\s*\)[^;]*\.key\s*\(\s*['"][^'"]{1,20}['"]\s*\)"#,
            )
            .expect("valid regex"),
            authenticated_none: Regex::new(r"\.authenticated\s*\(\s*\)\s*\.\s*disable\s*\(\s*\)")
                .expect("valid regex"),
            http_basic_disabled: Regex::new(r"\.httpBasic\s*\(\s*\)\s*\.\s*disable\s*\(\s*\)")
                .expect("valid regex"),
            form_login_disabled: Regex::new(r"\.formLogin\s*\(\s*\)\s*\.\s*disable\s*\(\s*\)")
                .expect("valid regex"),
            headers_disabled: Regex::new(r"\.headers\s*\(\s*\)\s*\.\s*disable\s*\(\s*\)")
                .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;

            // Security disabled
            if let Some(m) = self.security_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["spring", "security", "basic"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Spring Boot security disabled - authentication bypassed",
                ));
            }

            // CSRF disabled in config
            if let Some(m) = self.csrf_disabled_config.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["spring", "csrf"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Spring Boot CSRF protection disabled via config",
                ));
            }

            // Frame options disabled
            if let Some(m) = self.frame_options_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["spring", "headers", "frame_options"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Spring Boot X-Frame-Options disabled - clickjacking vulnerability",
                ));
            }

            // XSS protection disabled
            if let Some(m) = self.xss_protection_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["spring", "headers", "xss_protection"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Spring Boot XSS protection disabled",
                ));
            }

            // Content-Type options disabled
            if let Some(m) = self.content_type_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["spring", "headers", "content_type_options"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Spring Boot Content-Type nosniff disabled",
                ));
            }

            // Actuator endpoints exposed
            if let Some(m) = self.actuator_exposed.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["spring", "actuator", "exposure"],
                    "config_value",
                    ObjectValue::Text("*".to_string()),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Spring Boot actuator endpoints exposed to all",
                ));
            }

            // Dev tools enabled
            if let Some(m) = self.devtools_enabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["spring", "devtools"],
                    "enabled",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Spring Boot dev tools enabled - should be disabled in production",
                ));
            }

            // Health details exposed
            if let Some(m) = self.health_details_exposed.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["spring", "actuator", "health_details"],
                    "exposed",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.8,
                    "Spring Boot health endpoint exposes detailed info",
                ));
            }
        }

        claims
    }

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

        // Multi-line patterns
        if let Some(m) = self.csrf_disabled_java.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["spring", "csrf"],
                "enabled",
                ObjectValue::Boolean(false),
                file,
                line_num,
                m.as_str(),
                1.0,
                "Spring Boot CSRF disabled programmatically",
            ));
        }

        if let Some(m) = self.permit_all_wildcard.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["spring", "auth", "permit_all"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                m.as_str(),
                1.0,
                "Spring Boot permits all requests to /** - auth bypassed",
            ));
        }

        if let Some(m) = self.any_request_permit_all.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["spring", "auth", "any_request_permit_all"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                m.as_str(),
                1.0,
                "Spring Boot permits any request - auth bypassed",
            ));
        }

        if let Some(m) = self.frame_options_disabled_java.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["spring", "headers", "frame_options"],
                "enabled",
                ObjectValue::Boolean(false),
                file,
                line_num,
                m.as_str(),
                1.0,
                "Spring Boot X-Frame-Options disabled in code",
            ));
        }

        if let Some(m) = self.session_fixation_none.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["spring", "session", "fixation_protection"],
                "enabled",
                ObjectValue::Boolean(false),
                file,
                line_num,
                m.as_str(),
                1.0,
                "Spring Boot session fixation protection disabled",
            ));
        }

        if let Some(m) = self.weak_remember_me.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["spring", "remember_me", "weak_key"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                &m.as_str()[..m.as_str().len().min(60)],
                0.9,
                "Spring Boot remember-me uses weak key",
            ));
        }

        if let Some(m) = self.headers_disabled.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["spring", "headers"],
                "enabled",
                ObjectValue::Boolean(false),
                file,
                line_num,
                m.as_str(),
                1.0,
                "Spring Boot security headers disabled",
            ));
        }

        claims
    }
}

impl Extractor for SpringSecurityExtractor {
    fn name(&self) -> &str {
        "spring_security"
    }

    fn languages(&self) -> &[Language] {
        &[Language::Java, Language::Yaml, Language::Properties]
    }

    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 Spring file
        let is_spring = content.contains("spring")
            || content.contains("Spring")
            || content.contains("@EnableWebSecurity")
            || content.contains("WebSecurityConfigurerAdapter")
            || content.contains("SecurityFilterChain")
            || content.contains("HttpSecurity")
            || file.contains("application")
            || file.contains("security");

        if !is_spring {
            return claims;
        }

        match language {
            Language::Java => {
                claims.extend(self.check_java_patterns(path_segments, content, file));
            }
            Language::Yaml | Language::Properties => {
                claims.extend(self.check_config_patterns(path_segments, content, file));
            }
            _ => {}
        }

        claims
    }
}

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

    #[test]
    fn test_csrf_disabled_java() {
        let extractor = SpringSecurityExtractor::new();
        let content = r#"
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
    }
}
"#;

        let claims = extractor.extract(
            &["java".to_string()],
            content,
            Language::Java,
            "SecurityConfig.java",
        );

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

    #[test]
    fn test_permit_all_wildcard() {
        let extractor = SpringSecurityExtractor::new();
        let content = r#"
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) {
        http.authorizeRequests()
            .antMatchers("/**").permitAll();
        return http.build();
    }
}
"#;

        let claims = extractor.extract(
            &["java".to_string()],
            content,
            Language::Java,
            "SecurityConfig.java",
        );

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

    #[test]
    fn test_security_disabled_properties() {
        let extractor = SpringSecurityExtractor::new();
        // Use properties-style inline format that matches line-by-line
        let content = r#"
spring.security.basic.enabled=false
"#;

        let claims = extractor.extract(
            &["properties".to_string()],
            content,
            Language::Properties,
            "application.properties",
        );

        assert!(claims.iter().any(|c| c.concept_path.contains("security/basic")));
    }

    #[test]
    fn test_actuator_exposed() {
        let extractor = SpringSecurityExtractor::new();
        // Use properties-style format
        let content = r#"
management.endpoints.web.exposure.include=*
"#;

        let claims = extractor.extract(
            &["properties".to_string()],
            content,
            Language::Properties,
            "application.properties",
        );

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

    #[test]
    fn test_devtools_enabled() {
        let extractor = SpringSecurityExtractor::new();
        let content = r#"
spring.devtools.restart.enabled=true
"#;

        let claims = extractor.extract(
            &["properties".to_string()],
            content,
            Language::Properties,
            "application.properties",
        );

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

    #[test]
    fn test_session_fixation_none() {
        let extractor = SpringSecurityExtractor::new();
        let content = r#"
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) {
        http.sessionManagement()
            .sessionFixation().none();
        return http.build();
    }
}
"#;

        let claims = extractor.extract(
            &["java".to_string()],
            content,
            Language::Java,
            "SecurityConfig.java",
        );

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

    #[test]
    fn test_non_spring_file_skipped() {
        let extractor = SpringSecurityExtractor::new();
        let content = r#"
public class MyService {
    public void doSomething() {
        http.csrf().disable();
    }
}
"#;

        let claims =
            extractor.extract(&["java".to_string()], content, Language::Java, "MyService.java");

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