//! Flask security extractor.
//!
//! Detects security misconfigurations in Flask applications:
//! - Weak or missing secret key
//! - Debug mode enabled
//! - Insecure session cookie settings
//! - CSRF protection disabled
//! - SQL injection in 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 Flask security misconfigurations.
#[allow(dead_code)]
pub struct FlaskSecurityExtractor {
    // Config patterns
    weak_secret_key: Regex,
    empty_secret_key: Regex,
    session_cookie_secure_false: Regex,
    session_cookie_httponly_false: Regex,
    session_cookie_samesite_none: Regex,
    csrf_disabled: Regex,
    debug_enabled: Regex,
    debug_run: Regex,

    // Code patterns
    sql_fstring: Regex,
    sql_concat: Regex,
    unsafe_file_save: Regex,
    hardcoded_secret: Regex,
}

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

impl FlaskSecurityExtractor {
    /// Create a new Flask security extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Config patterns
            weak_secret_key: Regex::new(
                r#"(?:app\.secret_key|SECRET_KEY)\s*=\s*['"][^'"]{0,20}['"]"#,
            )
            .expect("valid regex"),
            empty_secret_key: Regex::new(r#"(?:app\.secret_key|SECRET_KEY)\s*=\s*(?:None|''|"")"#)
                .expect("valid regex"),
            session_cookie_secure_false: Regex::new(
                r#"SESSION_COOKIE_SECURE['\"]?\s*[=:]\s*False"#,
            )
            .expect("valid regex"),
            session_cookie_httponly_false: Regex::new(
                r#"SESSION_COOKIE_HTTPONLY['\"]?\s*[=:]\s*False"#,
            )
            .expect("valid regex"),
            session_cookie_samesite_none: Regex::new(
                r#"SESSION_COOKIE_SAMESITE['\"]?\s*[=:]\s*None"#,
            )
            .expect("valid regex"),
            csrf_disabled: Regex::new(r#"WTF_CSRF_ENABLED['\"]?\s*\]\s*=\s*False"#)
                .expect("valid regex"),
            debug_enabled: Regex::new(r#"(?:app\.debug|DEBUG['\"]?)\s*=\s*True"#)
                .expect("valid regex"),
            debug_run: Regex::new(r"app\.run\s*\([^)]*debug\s*=\s*True").expect("valid regex"),

            // Code patterns
            sql_fstring: Regex::new(r#"(?:db\.execute|cursor\.execute)\s*\(\s*f["']"#)
                .expect("valid regex"),
            sql_concat: Regex::new(r#"(?:db\.execute|cursor\.execute)\s*\([^)]*\+[^)]*request\."#)
                .expect("valid regex"),
            unsafe_file_save: Regex::new(r"\.save\s*\([^)]*\+[^)]*filename").expect("valid regex"),
            hardcoded_secret: Regex::new(r#"app\.secret_key\s*=\s*['"][^'"]{5,}['"]"#)
                .expect("valid regex"),
        }
    }
}

impl Extractor for FlaskSecurityExtractor {
    fn name(&self) -> &str {
        "flask_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 Flask file
        let is_flask = content.contains("flask")
            || content.contains("Flask")
            || content.contains("@app.route")
            || content.contains("Blueprint")
            || file.contains("flask");

        if !is_flask {
            return claims;
        }

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

            // Empty or None secret key
            if let Some(m) = self.empty_secret_key.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["flask", "secret_key"],
                    "missing",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Flask secret_key is empty or None - sessions will not work securely",
                ));
            }
            // Weak secret key (short)
            else if let Some(m) = self.weak_secret_key.find(line) {
                // Skip if it's an environment variable reference
                if !line.contains("os.environ") && !line.contains("os.getenv") {
                    claims.push(build_claim(
                        path_segments,
                        &["flask", "secret_key"],
                        "weak",
                        ObjectValue::Boolean(true),
                        file,
                        line_num,
                        m.as_str(),
                        0.9,
                        "Flask secret_key appears weak or hardcoded",
                    ));
                }
            }

            // Session cookie secure = False
            if let Some(m) = self.session_cookie_secure_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["flask", "session_cookie", "secure"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Flask session 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,
                    &["flask", "session_cookie", "httponly"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Flask session cookie accessible to JavaScript - XSS risk",
                ));
            }

            // Session cookie samesite = None
            if let Some(m) = self.session_cookie_samesite_none.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["flask", "session_cookie", "samesite"],
                    "config_value",
                    ObjectValue::Text("none".to_string()),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Flask session cookie sameSite=None - cross-site requests allowed",
                ));
            }

            // CSRF disabled
            if let Some(m) = self.csrf_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["flask", "csrf"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Flask-WTF CSRF protection disabled",
                ));
            }

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

            // Debug enabled via app.run()
            if let Some(m) = self.debug_run.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["flask", "debug_mode"],
                    "enabled",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Flask app.run(debug=True) - must be False in production",
                ));
            }

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

            // SQL injection via concatenation
            if let Some(m) = self.sql_concat.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["flask", "sql_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Flask SQL query with string concatenation - SQL injection risk",
                ));
            }

            // Unsafe file save (path traversal)
            if let Some(m) = self.unsafe_file_save.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["flask", "path_traversal"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Flask file save with unsanitized filename - path traversal risk",
                ));
            }
        }

        claims
    }
}

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

    #[test]
    fn test_weak_secret_key() {
        let extractor = FlaskSecurityExtractor::new();
        let content = r#"
from flask import Flask
app = Flask(__name__)
app.secret_key = 'dev'
"#;

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

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

    #[test]
    fn test_empty_secret_key() {
        let extractor = FlaskSecurityExtractor::new();
        let content = r#"
from flask import Flask
app = Flask(__name__)
app.secret_key = None
"#;

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

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

    #[test]
    fn test_debug_enabled() {
        let extractor = FlaskSecurityExtractor::new();
        let content = r#"
from flask import Flask
app = Flask(__name__)
app.debug = True
"#;

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

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

    #[test]
    fn test_debug_run() {
        let extractor = FlaskSecurityExtractor::new();
        let content = r#"
from flask import Flask
app = Flask(__name__)

if __name__ == '__main__':
    app.run(debug=True)
"#;

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

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

    #[test]
    fn test_csrf_disabled() {
        let extractor = FlaskSecurityExtractor::new();
        let content = r#"
from flask import Flask
app = Flask(__name__)
app.config['WTF_CSRF_ENABLED'] = False
"#;

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

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

    #[test]
    fn test_sql_injection() {
        let extractor = FlaskSecurityExtractor::new();
        let content = r#"
from flask import Flask, request
app = Flask(__name__)

@app.route('/user')
def get_user():
    db.execute(f"SELECT * FROM users WHERE id = {request.args.get('id')}")
"#;

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

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

    #[test]
    fn test_non_flask_file_skipped() {
        let extractor = FlaskSecurityExtractor::new();
        let content = r#"
app.secret_key = 'dev'
DEBUG = True
"#;

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

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