mod data_access;
mod gin;
mod hot_path;

pub(super) const DATA_ACCESS_BINDING_LOCATION: &str = data_access::BINDING_LOCATION;
pub(super) const DATA_ACCESS_CLIENTS_BINDING_LOCATION: &str = data_access::CLIENTS_BINDING_LOCATION;
pub(super) const DATA_ACCESS_GORM_BINDING_LOCATION: &str = data_access::GORM_BINDING_LOCATION;
pub(super) const DATA_ACCESS_SQL_BINDING_LOCATION: &str = data_access::SQL_BINDING_LOCATION;
pub(super) const GIN_BINDING_LOCATION: &str = gin::BINDING_LOCATION;
pub(super) const HOT_PATH_BINDING_LOCATION: &str = hot_path::BINDING_LOCATION;

use std::collections::{BTreeMap, BTreeSet};

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

use self::data_access::data_access_performance_findings;
use self::gin::gin_request_performance_findings;
use self::hot_path::core_hot_path_findings as run_hot_path_findings;

#[derive(Debug, Clone)]
pub(super) struct BodyLine {
    pub(super) line: usize,
    pub(super) text: String,
    pub(super) in_loop: bool,
}

pub(super) const LARGE_MULTIPART_FORM_BYTES: u64 = 32 * 1024 * 1024;

pub(crate) fn go_framework_patterns_file_findings(file: &ParsedFile) -> Vec<Finding> {
    let mut findings = Vec::new();

    for function in &file.functions {
        findings.extend(go_framework_patterns_function_findings(file, function));
    }

    findings
}

fn go_framework_patterns_function_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    if file.is_test_file || function.is_test_function {
        return Vec::new();
    }

    let mut findings = core_hot_path_findings(file, function);
    findings.extend(data_access_performance_findings(file, function));
    findings.extend(gin_request_performance_findings(file, function));
    findings
}

fn core_hot_path_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    run_hot_path_findings(file, function)
}

pub(crate) fn is_request_path_function(file: &ParsedFile, function: &ParsedFunction) -> bool {
    is_gin_handler(file, function)
        || is_http_handler(file, function)
        || is_echo_handler(file, function)
        || is_fiber_handler(file, function)
}

pub(crate) fn is_likely_non_request_workload(file: &ParsedFile, function: &ParsedFunction) -> bool {
    let path_str = file.path.to_string_lossy();
    let name_lower = function.fingerprint.name.to_ascii_lowercase();

    let non_request_path_hints = [
        "cmd/",
        "migrate",
        "migration",
        "seed",
        "cli",
        "cron",
        "job",
        "worker",
        "batch",
        "offline",
        "script",
        "tool",
        "setup",
        "init",
        "bootstrap",
    ];

    let non_request_name_hints = [
        "migrate",
        "seed",
        "setup",
        "init",
        "bootstrap",
        "main",
        "run",
        "execute",
        "process",
        "cron",
        "worker",
        "job",
        "batch",
        "export",
        "import",
    ];

    non_request_path_hints
        .iter()
        .any(|hint| path_str.contains(hint))
        || non_request_name_hints
            .iter()
            .any(|hint| name_lower.contains(hint))
}

pub(crate) fn is_gin_handler(file: &ParsedFile, function: &ParsedFunction) -> bool {
    import_aliases_for(file, "github.com/gin-gonic/gin")
        .into_iter()
        .any(|alias| {
            function
                .signature_text
                .contains(&format!("*{alias}.Context"))
        })
}

pub(crate) fn is_http_handler(file: &ParsedFile, function: &ParsedFunction) -> bool {
    import_aliases_for(file, "net/http")
        .into_iter()
        .any(|alias| {
            function
                .signature_text
                .contains(&format!("{alias}.ResponseWriter"))
                && function
                    .signature_text
                    .contains(&format!("*{alias}.Request"))
        })
}

pub(crate) fn is_echo_handler(file: &ParsedFile, function: &ParsedFunction) -> bool {
    import_aliases_for(file, "github.com/labstack/echo/v4")
        .into_iter()
        .any(|alias| {
            function
                .signature_text
                .contains(&format!("{alias}.Context"))
        })
}

pub(crate) fn is_fiber_handler(file: &ParsedFile, function: &ParsedFunction) -> bool {
    import_aliases_for(file, "github.com/gofiber/fiber/v2")
        .into_iter()
        .any(|alias| {
            function.signature_text.contains(&format!("*{alias}.Ctx"))
                || function.signature_text.contains(&format!("{alias}.Ctx"))
        })
}

pub(crate) fn has_sql_like_import(file: &ParsedFile) -> bool {
    [
        "database/sql",
        "github.com/jmoiron/sqlx",
        "github.com/jackc/pgx/v5",
        "github.com/jackc/pgx/v5/pgxpool",
        "github.com/jackc/pgx/v4",
        "github.com/jackc/pgx/v4/pgxpool",
    ]
    .iter()
    .any(|path| has_import_path(file, path))
}

pub(crate) fn has_import_path(file: &ParsedFile, path: &str) -> bool {
    file.imports.iter().any(|import| import.path == path)
}

pub(crate) fn binding_matches(
    lines: &[BodyLine],
    patterns: &[&str],
) -> Vec<(String, usize, String)> {
    let mut matches = Vec::new();

    for body_line in lines {
        if let Some((name, target)) = binding_for_patterns(&body_line.text, patterns) {
            matches.push((name, body_line.line, target));
        }
    }

    matches
}

pub(crate) fn binding_for_patterns(text: &str, patterns: &[&str]) -> Option<(String, String)> {
    let (left, right) = split_assignment(text)?;
    let target = patterns
        .iter()
        .find(|pattern| right.contains(**pattern))?
        .to_string();
    let binding = left
        .trim()
        .trim_start_matches("var ")
        .split(',')
        .next()?
        .split_whitespace()
        .next()?
        .trim();
    is_identifier_name(binding).then(|| (binding.to_string(), target))
}

pub(crate) fn split_assignment(text: &str) -> Option<(&str, &str)> {
    if let Some((left, right)) = text.split_once(":=") {
        return Some((left, right));
    }

    if text.contains("==") || text.contains("!=") || text.contains("<=") || text.contains(">=") {
        return None;
    }

    text.split_once(" = ")
        .or_else(|| text.split_once('='))
        .filter(|(left, _)| !left.trim_start().starts_with("if "))
}

pub(crate) fn body_lines(function: &ParsedFunction) -> Vec<BodyLine> {
    let mut brace_depth = 0usize;
    let mut loop_exit_depths = Vec::new();
    let mut lines = Vec::new();

    for (offset, raw_line) in function.body_text.lines().enumerate() {
        let line_no = function.body_start_line + offset;
        let stripped = strip_line_comment(raw_line).trim().to_string();
        let closing_braces = stripped
            .chars()
            .filter(|character| *character == '}')
            .count();
        for _ in 0..closing_braces {
            brace_depth = brace_depth.saturating_sub(1);
            while loop_exit_depths
                .last()
                .is_some_and(|exit_depth| *exit_depth > brace_depth)
            {
                loop_exit_depths.pop();
            }
        }

        let starts_loop = contains_keyword(&stripped, "for");
        let in_loop = !loop_exit_depths.is_empty() || starts_loop;
        let opening_braces = stripped
            .chars()
            .filter(|character| *character == '{')
            .count();
        if starts_loop {
            loop_exit_depths.push(brace_depth + opening_braces.max(1));
        }

        brace_depth += opening_braces;
        lines.push(BodyLine {
            line: line_no,
            text: stripped,
            in_loop,
        });
    }

    lines
}

pub(crate) fn import_aliases_for(file: &ParsedFile, import_path: &str) -> Vec<String> {
    file.imports
        .iter()
        .filter(|import| import.path == import_path)
        .map(|import| import.alias.clone())
        .collect()
}

pub(crate) fn json_aliases(file: &ParsedFile) -> Vec<String> {
    import_aliases_for(file, "encoding/json")
}

pub(crate) fn first_line_with_any(function: &ParsedFunction, markers: &[&str]) -> Option<usize> {
    function
        .body_text
        .lines()
        .enumerate()
        .find(|(_, line)| markers.iter().any(|marker| line.contains(marker)))
        .map(|(offset, _)| function.body_start_line + offset)
}

pub(crate) fn has_prior_loop_line(function: &ParsedFunction, line_no: usize) -> bool {
    function
        .body_text
        .lines()
        .enumerate()
        .take_while(|(offset, _)| function.body_start_line + *offset < line_no)
        .any(|(_, line)| contains_keyword(strip_line_comment(line), "for"))
}

pub(crate) fn is_identifier_name(text: &str) -> bool {
    !text.is_empty()
        && text
            .chars()
            .all(|character| character == '_' || character.is_ascii_alphanumeric())
        && text
            .chars()
            .next()
            .is_some_and(|character| character == '_' || character.is_ascii_alphabetic())
}

pub(crate) fn join_lines(lines: &[usize]) -> String {
    lines
        .iter()
        .map(usize::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}

fn contains_keyword(line: &str, keyword: &str) -> bool {
    let bytes = line.as_bytes();
    let keyword_bytes = keyword.as_bytes();

    if keyword_bytes.is_empty() || bytes.len() < keyword_bytes.len() {
        return false;
    }

    for start in 0..=bytes.len() - keyword_bytes.len() {
        if &bytes[start..start + keyword_bytes.len()] != keyword_bytes {
            continue;
        }

        let left_ok =
            start == 0 || (!bytes[start - 1].is_ascii_alphanumeric() && bytes[start - 1] != b'_');
        let right_index = start + keyword_bytes.len();
        let right_ok = right_index == bytes.len()
            || (!bytes[right_index].is_ascii_alphanumeric() && bytes[right_index] != b'_');

        if left_ok && right_ok {
            return true;
        }
    }

    false
}

pub(crate) fn strip_line_comment(line: &str) -> &str {
    line.split("//").next().unwrap_or("")
}

pub(crate) fn repeated_parse_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    let mut findings = Vec::new();

    for (parser_family, rule_id, payload_label) in [
        (
            "json_unmarshal",
            "json_unmarshal_same_payload_multiple_times",
            "JSON",
        ),
        (
            "xml_unmarshal",
            "xml_unmarshal_same_payload_multiple_times",
            "XML",
        ),
        (
            "yaml_unmarshal",
            "yaml_unmarshal_same_payload_multiple_times",
            "YAML",
        ),
        (
            "proto_unmarshal",
            "proto_unmarshal_same_payload_multiple_times",
            "protobuf",
        ),
    ] {
        findings.extend(repeated_parse_family_findings(
            file,
            function,
            parser_family,
            rule_id,
            payload_label,
        ));
    }

    findings
}

fn repeated_parse_family_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
    parser_family: &str,
    rule_id: &str,
    payload_label: &str,
) -> Vec<Finding> {
    let go = function.go_evidence();
    let mut groups = BTreeMap::<String, Vec<_>>::new();

    for call in go
        .parse_input_calls
        .iter()
        .filter(|call| call.parser_family == parser_family && call.input_binding.is_some())
    {
        groups
            .entry(call.input_binding.clone().unwrap_or_default())
            .or_default()
            .push(call);
    }

    let mut findings = Vec::new();
    for (input_binding, calls) in groups {
        if calls.len() < 2 {
            continue;
        }

        let distinct_targets = calls
            .iter()
            .filter_map(|call| call.target_text.clone())
            .collect::<BTreeSet<_>>();
        if distinct_targets.len() < 2 {
            continue;
        }

        let repeated_lines = calls.iter().map(|call| call.line).collect::<Vec<_>>();
        let anchor_line = repeated_lines[1];
        findings.push(Finding {
            rule_id: rule_id.to_string(),
            severity: Severity::Info,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: anchor_line,
            end_line: anchor_line,
            message: format!(
                "function {} unmarshals the same {} payload binding multiple times",
                function.fingerprint.name, payload_label
            ),
            evidence: vec![
                format!(
                    "input binding {input_binding} was unmarshaled at lines {}",
                    join_lines(&repeated_lines)
                ),
                format!("normalized input text: {}", calls[0].input_text),
                format!(
                    "distinct targets observed: {}",
                    distinct_targets.into_iter().collect::<Vec<_>>().join(", ")
                ),
            ],
        });
    }

    findings
}
