//! ASP.NET Core security extractor.
//!
//! Detects security misconfigurations in ASP.NET Core applications:
//! - CSRF protection disabled
//! - JWT validation disabled
//! - CORS allows all with credentials
//! - Insecure cookie settings
//! - Developer exception page in production

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

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

/// Extractor for ASP.NET Core security misconfigurations.
pub struct AspNetSecurityExtractor {
    // JSON config patterns (appsettings.json)
    validate_issuer_false: Regex,
    validate_audience_false: Regex,
    validate_lifetime_false: Regex,
    cors_allow_all: Regex,
    log_level_debug: Regex,

    // C# code patterns
    ignore_antiforgery: Regex,
    allow_any_origin_credentials: Regex,
    cookie_secure_none: Regex,
    cookie_httponly_false: Regex,
    cookie_samesite_none: Regex,
    developer_exception_page: Regex,
    validate_issuer_code: Regex,
    validate_audience_code: Regex,
    validate_lifetime_code: Regex,
    validate_signing_key_code: Regex,
}

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

impl AspNetSecurityExtractor {
    /// Create a new ASP.NET Core security extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // JSON config patterns
            validate_issuer_false: Regex::new(r#"["']?ValidateIssuer["']?\s*:\s*false"#)
                .expect("valid regex"),
            validate_audience_false: Regex::new(r#"["']?ValidateAudience["']?\s*:\s*false"#)
                .expect("valid regex"),
            validate_lifetime_false: Regex::new(r#"["']?ValidateLifetime["']?\s*:\s*false"#)
                .expect("valid regex"),
            cors_allow_all: Regex::new(r#"["']?AllowedOrigins["']?\s*:\s*\[\s*["']\*["']\s*\]"#)
                .expect("valid regex"),
            log_level_debug: Regex::new(r#"["']?Default["']?\s*:\s*["']Debug["']"#)
                .expect("valid regex"),

            // C# code patterns
            ignore_antiforgery: Regex::new(r"\[IgnoreAntiforgeryToken\]").expect("valid regex"),
            allow_any_origin_credentials: Regex::new(
                r"AllowAnyOrigin\s*\(\s*\)[^;]*AllowCredentials\s*\(\s*\)",
            )
            .expect("valid regex"),
            cookie_secure_none: Regex::new(r"SecurePolicy\s*=\s*CookieSecurePolicy\.None")
                .expect("valid regex"),
            cookie_httponly_false: Regex::new(r"HttpOnly\s*=\s*false").expect("valid regex"),
            cookie_samesite_none: Regex::new(r"SameSite\s*=\s*SameSiteMode\.None")
                .expect("valid regex"),
            developer_exception_page: Regex::new(r"UseDeveloperExceptionPage\s*\(\s*\)")
                .expect("valid regex"),
            validate_issuer_code: Regex::new(r"ValidateIssuer\s*=\s*false").expect("valid regex"),
            validate_audience_code: Regex::new(r"ValidateAudience\s*=\s*false")
                .expect("valid regex"),
            validate_lifetime_code: Regex::new(r"ValidateLifetime\s*=\s*false")
                .expect("valid regex"),
            validate_signing_key_code: Regex::new(r"ValidateIssuerSigningKey\s*=\s*false")
                .expect("valid regex"),
        }
    }

    fn check_json_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;

            // ValidateIssuer: false
            if let Some(m) = self.validate_issuer_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "jwt", "validate_issuer"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "ASP.NET JWT issuer validation disabled",
                ));
            }

            // ValidateAudience: false
            if let Some(m) = self.validate_audience_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "jwt", "validate_audience"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "ASP.NET JWT audience validation disabled",
                ));
            }

            // ValidateLifetime: false
            if let Some(m) = self.validate_lifetime_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "jwt", "validate_lifetime"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "ASP.NET JWT lifetime validation disabled - expired tokens accepted",
                ));
            }

            // CORS AllowedOrigins: ["*"]
            if let Some(m) = self.cors_allow_all.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "cors", "allow_origin"],
                    "config_value",
                    ObjectValue::Text("*".to_string()),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "ASP.NET CORS allows all origins in config",
                ));
            }

            // LogLevel Debug
            if file.contains("Production") || file.contains("production") {
                if let Some(m) = self.log_level_debug.find(line) {
                    claims.push(build_claim(
                        path_segments,
                        &["aspnet", "logging"],
                        "config_value",
                        ObjectValue::Text("Debug".to_string()),
                        file,
                        line_num,
                        m.as_str(),
                        0.8,
                        "ASP.NET log level set to Debug in production config",
                    ));
                }
            }
        }

        claims
    }

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

        // Multi-line: CORS AllowAnyOrigin with AllowCredentials
        if let Some(m) = self.allow_any_origin_credentials.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["aspnet", "cors", "any_origin_credentials"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                m.as_str(),
                1.0,
                "ASP.NET CORS allows any origin with credentials - security vulnerability",
            ));
        }

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

            // [IgnoreAntiforgeryToken]
            if let Some(m) = self.ignore_antiforgery.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "csrf"],
                    "ignored",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "ASP.NET CSRF protection ignored via [IgnoreAntiforgeryToken]",
                ));
            }

            // Cookie SecurePolicy = None
            if let Some(m) = self.cookie_secure_none.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "cookie", "secure"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "ASP.NET cookie not marked secure",
                ));
            }

            // Cookie HttpOnly = false
            if let Some(m) = self.cookie_httponly_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "cookie", "httponly"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "ASP.NET cookie accessible to JavaScript",
                ));
            }

            // Cookie SameSite = None
            if let Some(m) = self.cookie_samesite_none.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "cookie", "samesite"],
                    "config_value",
                    ObjectValue::Text("None".to_string()),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "ASP.NET cookie SameSite=None - cross-site requests allowed",
                ));
            }

            // UseDeveloperExceptionPage
            if let Some(m) = self.developer_exception_page.find(line) {
                // Check if it's NOT in an IsDevelopment() block
                // This is a heuristic - we look for env.IsDevelopment in nearby lines
                let context_start = line_idx.saturating_sub(5);
                let context_lines: Vec<_> = content.lines().skip(context_start).take(10).collect();
                let context = context_lines.join("\n");

                if !context.contains("IsDevelopment") {
                    claims.push(build_claim(
                        path_segments,
                        &["aspnet", "debug", "developer_exception_page"],
                        "vulnerable",
                        ObjectValue::Boolean(true),
                        file,
                        line_num,
                        m.as_str(),
                        0.85,
                        "ASP.NET UseDeveloperExceptionPage may be exposed in production",
                    ));
                }
            }

            // JWT validation disabled in code
            if let Some(m) = self.validate_issuer_code.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "jwt", "validate_issuer"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "ASP.NET JWT issuer validation disabled in code",
                ));
            }

            if let Some(m) = self.validate_audience_code.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "jwt", "validate_audience"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "ASP.NET JWT audience validation disabled in code",
                ));
            }

            if let Some(m) = self.validate_lifetime_code.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "jwt", "validate_lifetime"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "ASP.NET JWT lifetime validation disabled - expired tokens accepted",
                ));
            }

            if let Some(m) = self.validate_signing_key_code.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["aspnet", "jwt", "validate_signing_key"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "ASP.NET JWT signing key validation disabled",
                ));
            }
        }

        claims
    }
}

impl Extractor for AspNetSecurityExtractor {
    fn name(&self) -> &str {
        "aspnet_security"
    }

    fn languages(&self) -> &[Language] {
        &[Language::CSharp, Language::Json]
    }

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

        // Check if this looks like an ASP.NET file
        let is_aspnet = content.contains("Microsoft.AspNetCore")
            || content.contains("IApplicationBuilder")
            || content.contains("IWebHostBuilder")
            || content.contains("WebApplication")
            || content.contains("AddControllersWithViews")
            || content.contains("AddAuthentication")
            || content.contains("TokenValidationParameters")
            || file.contains("appsettings")
            || file.contains("Startup")
            || file.contains("Program.cs");

        if !is_aspnet {
            return claims;
        }

        match language {
            Language::Json => {
                claims.extend(self.check_json_patterns(path_segments, content, file));
            }
            Language::CSharp => {
                claims.extend(self.check_csharp_patterns(path_segments, content, file));
            }
            _ => {}
        }

        claims
    }
}

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

    #[test]
    fn test_ignore_antiforgery() {
        let extractor = AspNetSecurityExtractor::new();
        let content = r#"
using Microsoft.AspNetCore.Mvc;

[IgnoreAntiforgeryToken]
public class ApiController : Controller
{
    public IActionResult Submit() { }
}
"#;

        let claims = extractor.extract(
            &["csharp".to_string()],
            content,
            Language::CSharp,
            "ApiController.cs",
        );

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

    #[test]
    fn test_cors_any_origin_credentials() {
        let extractor = AspNetSecurityExtractor::new();
        let content = r#"
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll", builder =>
    {
        builder.AllowAnyOrigin()
               .AllowCredentials();
    });
});
"#;

        let claims =
            extractor.extract(&["csharp".to_string()], content, Language::CSharp, "Program.cs");

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

    #[test]
    fn test_jwt_validation_disabled_json() {
        let extractor = AspNetSecurityExtractor::new();
        let content = r#"
{
  "Jwt": {
    "ValidateIssuer": false,
    "ValidateAudience": false,
    "ValidateLifetime": false
  }
}
"#;

        let claims =
            extractor.extract(&["json".to_string()], content, Language::Json, "appsettings.json");

        assert!(claims.iter().any(|c| c.concept_path.contains("validate_issuer")));
        assert!(claims.iter().any(|c| c.concept_path.contains("validate_audience")));
        assert!(claims.iter().any(|c| c.concept_path.contains("validate_lifetime")));
    }

    #[test]
    fn test_jwt_validation_disabled_code() {
        let extractor = AspNetSecurityExtractor::new();
        let content = r#"
using Microsoft.AspNetCore.Authentication.JwtBearer;

builder.Services.AddAuthentication().AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = false,
        ValidateIssuerSigningKey = false
    };
});
"#;

        let claims =
            extractor.extract(&["csharp".to_string()], content, Language::CSharp, "Startup.cs");

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

    #[test]
    fn test_cookie_security() {
        let extractor = AspNetSecurityExtractor::new();
        let content = r#"
using Microsoft.AspNetCore.Builder;

builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.SecurePolicy = CookieSecurePolicy.None;
    options.Cookie.HttpOnly = false;
    options.Cookie.SameSite = SameSiteMode.None;
});
"#;

        let claims =
            extractor.extract(&["csharp".to_string()], content, Language::CSharp, "Startup.cs");

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

    #[test]
    fn test_developer_exception_page() {
        let extractor = AspNetSecurityExtractor::new();
        let content = r#"
using Microsoft.AspNetCore.Builder;

var app = builder.Build();

app.UseDeveloperExceptionPage();
app.UseRouting();
"#;

        let claims =
            extractor.extract(&["csharp".to_string()], content, Language::CSharp, "Program.cs");

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

    #[test]
    fn test_non_aspnet_file_skipped() {
        let extractor = AspNetSecurityExtractor::new();
        let content = r#"
public class MyClass
{
    public bool ValidateIssuer = false;
}
"#;

        let claims =
            extractor.extract(&["csharp".to_string()], content, Language::CSharp, "MyClass.cs");

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