//! Ruby on Rails security extractor.
//!
//! Detects security misconfigurations in Rails applications:
//! - Force SSL disabled
//! - CSRF protection skipped
//! - SQL injection via string interpolation
//! - Mass assignment vulnerabilities
//! - Unsafe rendering (html_safe, raw)

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

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

/// Extractor for Rails security misconfigurations.
pub struct RailsSecurityExtractor {
    // Config patterns (production.rb)
    force_ssl_false: Regex,
    cookies_same_site_none: Regex,
    session_secure_false: Regex,
    session_httponly_false: Regex,
    forgery_protection_false: Regex,
    log_level_debug: Regex,

    // Code patterns
    skip_verify_authenticity: Regex,
    protect_from_forgery_null: Regex,
    where_interpolation: Regex,
    where_concat: Regex,
    find_by_sql_interpolation: Regex,
    html_safe: Regex,
    render_inline_params: Regex,
    render_html_params: Regex,
    permit_all: Regex,
    mass_assignment_new: Regex,
    secret_key_hardcoded: Regex,
}

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

impl RailsSecurityExtractor {
    /// Create a new Rails security extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Config patterns
            force_ssl_false: Regex::new(r"config\.force_ssl\s*=\s*false").expect("valid regex"),
            cookies_same_site_none: Regex::new(r"cookies_same_site_protection\s*=\s*:none")
                .expect("valid regex"),
            session_secure_false: Regex::new(r"session_store\s*:[^,]+,\s*secure:\s*false")
                .expect("valid regex"),
            session_httponly_false: Regex::new(r"session_store\s*:[^,]+,\s*httponly:\s*false")
                .expect("valid regex"),
            forgery_protection_false: Regex::new(r"allow_forgery_protection\s*=\s*false")
                .expect("valid regex"),
            log_level_debug: Regex::new(r"config\.log_level\s*=\s*:debug").expect("valid regex"),

            // Code patterns
            skip_verify_authenticity: Regex::new(
                r"skip_before_action\s*:verify_authenticity_token",
            )
            .expect("valid regex"),
            protect_from_forgery_null: Regex::new(r"protect_from_forgery\s+with:\s*:null_session")
                .expect("valid regex"),
            where_interpolation: Regex::new(r#"\.where\s*\(.*#\{.*params"#).expect("valid regex"),
            where_concat: Regex::new(r#"\.where\s*\(\s*['"][^'"]*['"]\s*\+[^)]*params"#)
                .expect("valid regex"),
            find_by_sql_interpolation: Regex::new(r#"find_by_sql\s*\(.*#\{.*params"#)
                .expect("valid regex"),
            html_safe: Regex::new(r"\.html_safe").expect("valid regex"),
            render_inline_params: Regex::new(r"render\s+inline:\s*params").expect("valid regex"),
            render_html_params: Regex::new(r"render\s+html:\s*params").expect("valid regex"),
            permit_all: Regex::new(r"params\.permit!").expect("valid regex"),
            mass_assignment_new: Regex::new(r"\.\s*new\s*\(\s*params\s*\[\s*:")
                .expect("valid regex"),
            secret_key_hardcoded: Regex::new(r#"secret_key_base\s*=\s*['"][^'"]{10,}['"]"#)
                .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;

            // Force SSL false
            if let Some(m) = self.force_ssl_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "force_ssl"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Rails force_ssl disabled - HTTPS not enforced",
                ));
            }

            // Cookies same site none
            if let Some(m) = self.cookies_same_site_none.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "cookies", "same_site"],
                    "config_value",
                    ObjectValue::Text("none".to_string()),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Rails cookies same_site set to none",
                ));
            }

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

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

            // Forgery protection false
            if let Some(m) = self.forgery_protection_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "csrf"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Rails CSRF protection disabled globally",
                ));
            }

            // Log level debug in production
            if file.contains("production") {
                if let Some(m) = self.log_level_debug.find(line) {
                    claims.push(build_claim(
                        path_segments,
                        &["rails", "log_level"],
                        "config_value",
                        ObjectValue::Text("debug".to_string()),
                        file,
                        line_num,
                        m.as_str(),
                        0.8,
                        "Rails log level set to debug in production",
                    ));
                }
            }
        }

        claims
    }

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

            // Skip verify authenticity token
            if let Some(m) = self.skip_verify_authenticity.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "csrf"],
                    "skipped",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Rails CSRF protection skipped via skip_before_action",
                ));
            }

            // Protect from forgery null session
            if let Some(m) = self.protect_from_forgery_null.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "csrf"],
                    "null_session",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Rails CSRF protection using null_session strategy",
                ));
            }

            // Where interpolation
            if let Some(m) = self.where_interpolation.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "sql_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Rails SQL injection via .where() with string interpolation",
                ));
            }

            // Where concatenation
            if let Some(m) = self.where_concat.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "sql_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Rails SQL injection via .where() with string concatenation",
                ));
            }

            // Find by SQL interpolation
            if let Some(m) = self.find_by_sql_interpolation.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "sql_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.95,
                    "Rails SQL injection via find_by_sql with interpolation",
                ));
            }

            // html_safe
            if let Some(m) = self.html_safe.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "xss"],
                    "html_safe_used",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.7,
                    "Rails .html_safe used - potential XSS if user input",
                ));
            }

            // Render inline params
            if let Some(m) = self.render_inline_params.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "xss"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Rails XSS via render inline with params",
                ));
            }

            // Render html params
            if let Some(m) = self.render_html_params.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "xss"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Rails XSS via render html with params",
                ));
            }

            // params.permit!
            if let Some(m) = self.permit_all.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "mass_assignment"],
                    "permit_all",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Rails mass assignment via params.permit!",
                ));
            }

            // Mass assignment via new
            if let Some(m) = self.mass_assignment_new.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["rails", "mass_assignment"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.8,
                    "Rails potential mass assignment via .new(params[...])",
                ));
            }

            // Hardcoded secret key
            if let Some(m) = self.secret_key_hardcoded.find(line) {
                // Skip if using ENV
                if !line.contains("ENV[") && !line.contains("Rails.application.credentials") {
                    claims.push(build_claim(
                        path_segments,
                        &["rails", "secret_key"],
                        "hardcoded",
                        ObjectValue::Boolean(true),
                        file,
                        line_num,
                        &m.as_str()[..m.as_str().len().min(50)],
                        0.9,
                        "Rails secret_key_base appears hardcoded",
                    ));
                }
            }
        }

        claims
    }
}

impl Extractor for RailsSecurityExtractor {
    fn name(&self) -> &str {
        "rails_security"
    }

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

    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 Rails file
        let is_rails = content.contains("Rails")
            || content.contains("rails")
            || content.contains("ActionController")
            || content.contains("ApplicationController")
            || content.contains("ActiveRecord")
            || content.contains("< Controller")
            || content.contains("class ") && content.contains("Controller")
            || content.contains("class ") && content.contains("Helper")
            || file.contains("config/environments")
            || file.contains("app/controllers")
            || file.contains("app/helpers");

        if !is_rails {
            return claims;
        }

        match language {
            Language::Ruby => {
                claims.extend(self.check_config_patterns(path_segments, content, file));
                claims.extend(self.check_code_patterns(path_segments, content, file));
            }
            Language::Yaml => {
                // secrets.yml patterns could be added here
            }
            _ => {}
        }

        claims
    }
}

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

    #[test]
    fn test_force_ssl_false() {
        let extractor = RailsSecurityExtractor::new();
        let content = r#"
Rails.application.configure do
  config.force_ssl = false
end
"#;

        let claims = extractor.extract(
            &["ruby".to_string()],
            content,
            Language::Ruby,
            "config/environments/production.rb",
        );

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

    #[test]
    fn test_skip_verify_authenticity_token() {
        let extractor = RailsSecurityExtractor::new();
        let content = r#"
class ApiController < ApplicationController
  skip_before_action :verify_authenticity_token
end
"#;

        let claims = extractor.extract(
            &["ruby".to_string()],
            content,
            Language::Ruby,
            "app/controllers/api_controller.rb",
        );

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

    #[test]
    fn test_sql_injection_where() {
        let extractor = RailsSecurityExtractor::new();
        let content = r#"
class UsersController < ApplicationController
  def search
    User.where("name = '#{params[:name]}'")
  end
end
"#;

        let claims = extractor.extract(
            &["ruby".to_string()],
            content,
            Language::Ruby,
            "app/controllers/users_controller.rb",
        );

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

    #[test]
    fn test_html_safe() {
        let extractor = RailsSecurityExtractor::new();
        let content = r#"
class ApplicationHelper
  def render_content(content)
    content.html_safe
  end
end
"#;

        let claims = extractor.extract(
            &["ruby".to_string()],
            content,
            Language::Ruby,
            "app/helpers/application_helper.rb",
        );

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

    #[test]
    fn test_permit_all() {
        let extractor = RailsSecurityExtractor::new();
        let content = r#"
class UsersController < ApplicationController
  def create
    User.create(params.permit!)
  end
end
"#;

        let claims = extractor.extract(
            &["ruby".to_string()],
            content,
            Language::Ruby,
            "app/controllers/users_controller.rb",
        );

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

    #[test]
    fn test_non_rails_file_skipped() {
        let extractor = RailsSecurityExtractor::new();
        let content = r#"
class MyClass
  def html_safe
    true
  end
end
"#;

        let claims =
            extractor.extract(&["ruby".to_string()], content, Language::Ruby, "lib/my_class.rb");

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