use crate::analysis::{ParsedFile, ParsedFunction};
use crate::model::{Finding, Severity};

pub(crate) const BINDING_LOCATION: &str = file!();

use super::common::{identifier_tokens, import_alias_lookup};

const WEAK_CRYPTO_IMPORTS: &[&str] = &["crypto/md5", "crypto/sha1", "crypto/des", "crypto/rc4"];

pub(super) fn crypto_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    let import_aliases = import_alias_lookup(&file.imports);
    let mut findings = Vec::new();

    for call in &function.calls {
        let Some(receiver) = &call.receiver else {
            continue;
        };
        let Some(import_path) = import_aliases.get(receiver) else {
            continue;
        };
        if !WEAK_CRYPTO_IMPORTS.contains(&import_path.as_str()) {
            continue;
        }

        findings.push(Finding {
            rule_id: "weak_crypto".to_string(),
            severity: Severity::Warning,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: call.line,
            end_line: call.line,
            message: format!(
                "function {} uses weak cryptographic primitive {}",
                function.fingerprint.name, import_path
            ),
            evidence: vec![
                format!("import alias {receiver} resolves to {import_path}"),
                format!("weak crypto call: {receiver}.{}", call.name),
            ],
        });
    }

    findings
}

pub(super) fn pkg_secret_findings(file: &ParsedFile) -> Vec<Finding> {
    file.pkg_strings
        .iter()
        .filter(|literal| {
            is_secret_like_name(&literal.name) && looks_like_secret_value(&literal.value)
        })
        .map(|literal| Finding {
            rule_id: "hardcoded_secret".to_string(),
            severity: Severity::Warning,
            path: file.path.clone(),
            function_name: None,
            start_line: literal.line,
            end_line: literal.line,
            message: format!(
                "file declares a hardcoded secret-like literal in {}",
                literal.name
            ),
            evidence: vec![
                format!("secret-like identifier: {}", literal.name),
                format!("literal length: {}", literal.value.len()),
            ],
        })
        .collect()
}

pub(super) fn secret_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    function
        .local_strings
        .iter()
        .filter(|literal| {
            is_secret_like_name(&literal.name) && looks_like_secret_value(&literal.value)
        })
        .map(|literal| Finding {
            rule_id: "hardcoded_secret".to_string(),
            severity: Severity::Warning,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: literal.line,
            end_line: literal.line,
            message: format!(
                "function {} assigns a hardcoded secret-like literal",
                function.fingerprint.name
            ),
            evidence: vec![
                format!("secret-like identifier: {}", literal.name),
                format!("literal length: {}", literal.value.len()),
            ],
        })
        .collect()
}

pub(super) fn sql_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    let go = function.go_evidence();

    go.db_query_calls
        .iter()
        .filter(|query_call| query_call.query_uses_dynamic_construction)
        .map(|query_call| Finding {
            rule_id: "sql_string_concat".to_string(),
            severity: Severity::Warning,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: query_call.line,
            end_line: query_call.line,
            message: format!(
                "function {} builds a SQL query dynamically before execution",
                function.fingerprint.name
            ),
            evidence: vec![
                format!("query method: {}", query_call.method_name),
                format!(
                    "query expression: {}",
                    query_call
                        .query_argument_text
                        .as_deref()
                        .unwrap_or("<unknown>")
                ),
            ],
        })
        .collect()
}

fn is_secret_like_name(name: &str) -> bool {
    let tokens = identifier_tokens(name);
    if tokens.is_empty() {
        return false;
    }

    let joined = tokens.join("_");
    matches!(
        joined.as_str(),
        "password"
            | "passwd"
            | "pwd"
            | "secret"
            | "client_secret"
            | "api_key"
            | "apikey"
            | "private_key"
            | "access_token"
            | "refresh_token"
            | "auth_token"
    ) || tokens.contains(&"password".to_string())
        || tokens.contains(&"secret".to_string())
        || tokens.contains(&"apikey".to_string())
        || tokens.contains(&"private".to_string()) && tokens.contains(&"key".to_string())
        || tokens.contains(&"token".to_string())
            && tokens.iter().any(|token| {
                matches!(
                    token.as_str(),
                    "api" | "auth" | "access" | "refresh" | "client"
                )
            })
}

fn looks_like_secret_value(value: &str) -> bool {
    let trimmed = value.trim();
    let normalized = trimmed.to_ascii_lowercase();

    trimmed.len() >= 8
        && !matches!(
            normalized.as_str(),
            "placeholder" | "example" | "sample" | "changeme" | "your-api-key" | "your-secret"
        )
        && normalized != "bearer"
}
