//! Laravel security extractor.
//!
//! Detects security misconfigurations in Laravel applications:
//! - APP_DEBUG enabled in production
//! - Empty or weak APP_KEY
//! - Mass assignment vulnerabilities
//! - SQL injection via DB::raw
//! - CSRF protection bypassed
//! - Insecure session/cookie settings

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

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

/// Extractor for Laravel security misconfigurations.
#[allow(dead_code)]
pub struct LaravelSecurityExtractor {
    // .env patterns
    app_debug_true: Regex,
    app_key_empty: Regex,
    session_secure_false: Regex,
    session_http_only_false: Regex,

    // PHP config patterns
    debug_hardcoded: Regex,
    key_hardcoded: Regex,
    cors_wildcard_credentials: Regex,

    // PHP code patterns
    csrf_except_all: Regex,
    csrf_except_api: Regex,
    mass_assignment_all: Regex,
    mass_assignment_fill: Regex,
    db_raw_interpolation: Regex,
    db_select_interpolation: Regex,
    eval_request: Regex,
    exec_request: Regex,
    shell_exec_request: Regex,
}

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

impl LaravelSecurityExtractor {
    /// Create a new Laravel security extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // .env patterns
            app_debug_true: Regex::new(r"(?i)^APP_DEBUG\s*=\s*true").expect("valid regex"),
            app_key_empty: Regex::new(r"(?i)^APP_KEY\s*=\s*$").expect("valid regex"),
            session_secure_false: Regex::new(r"(?i)^SESSION_SECURE_COOKIE\s*=\s*false")
                .expect("valid regex"),
            session_http_only_false: Regex::new(r"(?i)^SESSION_HTTP_ONLY\s*=\s*false")
                .expect("valid regex"),

            // PHP config patterns
            debug_hardcoded: Regex::new(r#"['"]debug['"]\s*=>\s*true"#).expect("valid regex"),
            key_hardcoded: Regex::new(r#"['"]key['"]\s*=>\s*['"][^'"]{1,50}['"]"#)
                .expect("valid regex"),
            cors_wildcard_credentials: Regex::new(
                r#"['"]allowed_origins['"]\s*=>\s*\[\s*['"]?\*['"]?\s*\][^]]*['"]supports_credentials['"]\s*=>\s*true"#,
            )
            .expect("valid regex"),

            // PHP code patterns
            csrf_except_all: Regex::new(r#"protected\s+\$except\s*=\s*\[\s*['"]?\*['"]?\s*\]"#)
                .expect("valid regex"),
            csrf_except_api: Regex::new(r#"\$except\s*=\s*\[[^\]]*['"]api/\*['"]"#)
                .expect("valid regex"),
            mass_assignment_all: Regex::new(r"::\s*create\s*\(\s*\$request->all\s*\(\s*\)\s*\)")
                .expect("valid regex"),
            mass_assignment_fill: Regex::new(r"->fill\s*\(\s*\$request->all\s*\(\s*\)\s*\)")
                .expect("valid regex"),
            db_raw_interpolation: Regex::new(r#"DB::raw\s*\([^)]*\.\s*\$"#)
                .expect("valid regex"),
            db_select_interpolation: Regex::new(r#"DB::select\s*\(\s*['"][^'"]*\{\$"#)
                .expect("valid regex"),
            eval_request: Regex::new(r"eval\s*\(\s*\$request").expect("valid regex"),
            exec_request: Regex::new(r"exec\s*\(\s*\$request").expect("valid regex"),
            shell_exec_request: Regex::new(r"shell_exec\s*\(\s*\$request").expect("valid regex"),
        }
    }

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

            // APP_DEBUG=true
            if let Some(m) = self.app_debug_true.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "debug_mode"],
                    "enabled",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Laravel APP_DEBUG enabled - must be false in production",
                ));
            }

            // APP_KEY empty
            if let Some(m) = self.app_key_empty.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "app_key"],
                    "missing",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Laravel APP_KEY is empty - encryption will fail",
                ));
            }

            // SESSION_SECURE_COOKIE=false
            if let Some(m) = self.session_secure_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "session_cookie", "secure"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Laravel session cookie not marked secure",
                ));
            }

            // SESSION_HTTP_ONLY=false
            if let Some(m) = self.session_http_only_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "session_cookie", "httponly"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Laravel session cookie accessible to JavaScript",
                ));
            }
        }

        claims
    }

    fn check_php_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 hardcoded
            if let Some(m) = self.debug_hardcoded.find(line) {
                // Skip if using env()
                if !line.contains("env(") {
                    claims.push(build_claim(
                        path_segments,
                        &["laravel", "debug_mode"],
                        "hardcoded",
                        ObjectValue::Boolean(true),
                        file,
                        line_num,
                        m.as_str(),
                        0.9,
                        "Laravel debug mode hardcoded to true",
                    ));
                }
            }

            // CSRF except all
            if let Some(m) = self.csrf_except_all.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "csrf"],
                    "exempt",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Laravel CSRF protection disabled for all routes",
                ));
            }

            // CSRF except API
            if let Some(m) = self.csrf_except_api.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "csrf", "api_exempt"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.7,
                    "Laravel CSRF protection disabled for API routes",
                ));
            }

            // Mass assignment via create()
            if let Some(m) = self.mass_assignment_all.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "mass_assignment"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Laravel mass assignment via ::create($request->all())",
                ));
            }

            // Mass assignment via fill()
            if let Some(m) = self.mass_assignment_fill.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "mass_assignment"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Laravel mass assignment via ->fill($request->all())",
                ));
            }

            // DB::raw interpolation
            if let Some(m) = self.db_raw_interpolation.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "sql_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Laravel SQL injection via DB::raw() with interpolation",
                ));
            }

            // DB::select interpolation
            if let Some(m) = self.db_select_interpolation.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "sql_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Laravel SQL injection via DB::select() with interpolation",
                ));
            }

            // Command injection
            if let Some(m) = self.eval_request.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "code_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Laravel code injection via eval() with request data",
                ));
            }

            if let Some(m) = self.exec_request.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "command_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Laravel command injection via exec() with request data",
                ));
            }

            if let Some(m) = self.shell_exec_request.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["laravel", "command_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Laravel command injection via shell_exec() with request data",
                ));
            }
        }

        claims
    }
}

impl Extractor for LaravelSecurityExtractor {
    fn name(&self) -> &str {
        "laravel_security"
    }

    fn languages(&self) -> &[Language] {
        &[Language::Php, Language::Dotenv]
    }

    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 Laravel file
        let is_laravel = content.contains("Laravel")
            || content.contains("laravel")
            || content.contains("Illuminate")
            || content.contains("APP_KEY")
            || content.contains("APP_DEBUG")
            || file.contains("artisan")
            || file.contains("app/Http");

        if !is_laravel {
            return claims;
        }

        match language {
            Language::Dotenv => {
                claims.extend(self.check_env_patterns(path_segments, content, file));
            }
            Language::Php => {
                claims.extend(self.check_php_patterns(path_segments, content, file));
            }
            _ => {}
        }

        claims
    }
}

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

    #[test]
    fn test_app_debug_true() {
        let extractor = LaravelSecurityExtractor::new();
        let content = r#"
APP_NAME=Laravel
APP_ENV=production
APP_KEY=base64:abcdef...
APP_DEBUG=true
"#;

        let claims = extractor.extract(&["env".to_string()], content, Language::Dotenv, ".env");

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

    #[test]
    fn test_app_key_empty() {
        let extractor = LaravelSecurityExtractor::new();
        let content = r#"
APP_NAME=Laravel
APP_KEY=
APP_DEBUG=false
"#;

        let claims = extractor.extract(&["env".to_string()], content, Language::Dotenv, ".env");

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

    #[test]
    fn test_mass_assignment() {
        let extractor = LaravelSecurityExtractor::new();
        let content = r#"
<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    public function store(Request $request)
    {
        return User::create($request->all());
    }
}
"#;

        let claims =
            extractor.extract(&["php".to_string()], content, Language::Php, "UserController.php");

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

    #[test]
    fn test_csrf_exempt_all() {
        let extractor = LaravelSecurityExtractor::new();
        let content = r#"
<?php
namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    protected $except = ['*'];
}
"#;

        let claims =
            extractor.extract(&["php".to_string()], content, Language::Php, "VerifyCsrfToken.php");

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

    #[test]
    fn test_db_raw_injection() {
        let extractor = LaravelSecurityExtractor::new();
        let content = r#"
<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class SearchController extends Controller
{
    public function search(Request $request)
    {
        return DB::raw("SELECT * FROM users WHERE name = '" . $request->name . "'");
    }
}
"#;

        let claims =
            extractor.extract(&["php".to_string()], content, Language::Php, "SearchController.php");

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

    #[test]
    fn test_non_laravel_file_skipped() {
        let extractor = LaravelSecurityExtractor::new();
        let content = r#"
<?php
$debug = true;
"#;

        let claims = extractor.extract(&["php".to_string()], content, Language::Php, "random.php");

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