//! Next.js security extractor.
//!
//! Detects security misconfigurations in Next.js applications:
//! - CVE-2025-29927: Middleware-only authentication bypass
//! - Server Actions without authentication
//! - Sensitive data exposed to client components
//! - Missing security headers
//! - Powered-by header enabled

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

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

/// Extractor for Next.js security misconfigurations.
#[allow(dead_code)]
pub struct NextJsSecurityExtractor {
    // Config patterns (next.config.js)
    powered_by_header: Regex,

    // Middleware patterns (CVE-2025-29927)
    middleware_export: Regex,
    middleware_auth_check: Regex,

    // Server Action patterns
    use_server: Regex,
    server_action_db: Regex,

    // Client component patterns
    use_client: Regex,
    env_secret_in_client: Regex,

    // Sensitive data patterns
    sensitive_props: Regex,
}

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

impl NextJsSecurityExtractor {
    /// Create a new Next.js security extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Config
            powered_by_header: Regex::new(r"poweredByHeader\s*:\s*true").expect("valid regex"),

            // Middleware
            middleware_export: Regex::new(r"export\s+(?:async\s+)?function\s+middleware")
                .expect("valid regex"),
            middleware_auth_check: Regex::new(
                r"(?:isAuthenticated|checkAuth|verifyToken|getSession|auth\(\))",
            )
            .expect("valid regex"),

            // Server Actions
            use_server: Regex::new(r#"['"]use server['"]"#).expect("valid regex"),
            server_action_db: Regex::new(
                r#"async\s+function\s+\w+[^}]*(?:db\.|prisma\.|sql\.|delete|update|insert)"#,
            )
            .expect("valid regex"),

            // Client components
            use_client: Regex::new(r#"['"]use client['"]"#).expect("valid regex"),
            env_secret_in_client: Regex::new(
                r"process\.env\.(?:\w*(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)\w*)",
            )
            .expect("valid regex"),

            // Sensitive data
            sensitive_props: Regex::new(r"(?:password|ssn|secret|token|apiKey|api_key)\s*[=:]")
                .expect("valid regex"),
        }
    }
}

impl Extractor for NextJsSecurityExtractor {
    fn name(&self) -> &str {
        "nextjs_security"
    }

    fn languages(&self) -> &[Language] {
        &[Language::JavaScript, Language::TypeScript]
    }

    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 Next.js file
        let is_nextjs = content.contains("next")
            || content.contains("Next")
            || file.contains("next.config")
            || file.contains("middleware")
            || content.contains("'use server'")
            || content.contains("\"use server\"")
            || content.contains("'use client'")
            || content.contains("\"use client\"")
            || content.contains("getServerSideProps")
            || content.contains("getStaticProps");

        if !is_nextjs {
            return claims;
        }

        // Check for middleware with auth (CVE-2025-29927 warning)
        let is_middleware_file = file.contains("middleware");
        let has_middleware_export = self.middleware_export.is_match(content);
        let has_auth_check = self.middleware_auth_check.is_match(content);

        if is_middleware_file && has_middleware_export && has_auth_check {
            // Find the middleware export line
            for (line_idx, line) in content.lines().enumerate() {
                if self.middleware_export.is_match(line) {
                    claims.push(build_claim(
                        path_segments,
                        &["nextjs", "middleware", "auth_only"],
                        "vulnerable",
                        ObjectValue::Boolean(true),
                        file,
                        line_idx + 1,
                        line.trim(),
                        0.8,
                        "Next.js middleware-only auth may be vulnerable to CVE-2025-29927 bypass",
                    ));
                    break;
                }
            }
        }

        // Check for 'use server' with DB operations without auth
        let has_use_server = self.use_server.is_match(content);
        if has_use_server {
            // Look for server actions that modify data without auth checks
            if self.server_action_db.is_match(content) && !has_auth_check {
                for (line_idx, line) in content.lines().enumerate() {
                    if self.use_server.is_match(line) {
                        claims.push(build_claim(
                            path_segments,
                            &["nextjs", "server_action", "no_auth"],
                            "vulnerable",
                            ObjectValue::Boolean(true),
                            file,
                            line_idx + 1,
                            line.trim(),
                            0.7,
                            "Next.js Server Action modifies data without visible auth check",
                        ));
                        break;
                    }
                }
            }
        }

        // Check for 'use client' with env secrets
        let has_use_client = self.use_client.is_match(content);
        if has_use_client {
            for (line_idx, line) in content.lines().enumerate() {
                if let Some(m) = self.env_secret_in_client.find(line) {
                    claims.push(build_claim(
                        path_segments,
                        &["nextjs", "client_component", "exposed_secret"],
                        "vulnerable",
                        ObjectValue::Boolean(true),
                        file,
                        line_idx + 1,
                        m.as_str(),
                        0.9,
                        "Next.js client component accesses secret environment variable",
                    ));
                }
            }
        }

        // Config file checks
        if file.contains("next.config") {
            for (line_idx, line) in content.lines().enumerate() {
                // Powered by header enabled
                if let Some(m) = self.powered_by_header.find(line) {
                    claims.push(build_claim(
                        path_segments,
                        &["nextjs", "config", "powered_by"],
                        "enabled",
                        ObjectValue::Boolean(true),
                        file,
                        line_idx + 1,
                        m.as_str(),
                        0.6,
                        "Next.js X-Powered-By header enabled - information disclosure",
                    ));
                }
            }
        }

        claims
    }
}

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

    #[test]
    fn test_middleware_auth_warning() {
        let extractor = NextJsSecurityExtractor::new();
        let content = r#"
import { NextResponse } from 'next/server';

export function middleware(request) {
    if (!isAuthenticated(request)) {
        return NextResponse.redirect('/login');
    }
    return NextResponse.next();
}
"#;

        let claims =
            extractor.extract(&["ts".to_string()], content, Language::TypeScript, "middleware.ts");

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

    #[test]
    fn test_server_action_no_auth() {
        let extractor = NextJsSecurityExtractor::new();
        let content = r#"
'use server';

export async function deleteUser(id: string) {
    await db.users.delete({ where: { id } });
}
"#;

        let claims =
            extractor.extract(&["ts".to_string()], content, Language::TypeScript, "actions.ts");

        assert!(claims.iter().any(
            |c| c.concept_path.contains("server_action") && c.concept_path.contains("no_auth")
        ));
    }

    #[test]
    fn test_client_env_secret() {
        let extractor = NextJsSecurityExtractor::new();
        let content = r#"
'use client';

export function Dashboard() {
    const apiKey = process.env.API_SECRET_KEY;
    return <div>Dashboard</div>;
}
"#;

        let claims =
            extractor.extract(&["tsx".to_string()], content, Language::TypeScript, "Dashboard.tsx");

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

    #[test]
    fn test_powered_by_header() {
        let extractor = NextJsSecurityExtractor::new();
        let content = r#"
/** @type {import('next').NextConfig} */
const nextConfig = {
    poweredByHeader: true,
    reactStrictMode: true,
}

module.exports = nextConfig
"#;

        let claims =
            extractor.extract(&["js".to_string()], content, Language::JavaScript, "next.config.js");

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

    #[test]
    fn test_non_nextjs_file_skipped() {
        let extractor = NextJsSecurityExtractor::new();
        let content = r#"
export function middleware(request) {
    return request;
}
"#;

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

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