use crate::config::OutputFormat;
use crate::model::{
    Diagnostic, ExternalPurityAssessment, ExternalPurityCategory, FixHint, FunctionAssessment,
    ImplementationStyleAssessment, ImplementationStyleCategory, RuleId, Severity,
};
use serde::Serialize;
use serde_json::json;
use std::collections::BTreeSet;

#[derive(Debug, Clone, Serialize)]
pub struct FileReport {
    pub path: String,
    pub checked_functions: usize,
    pub ignored_functions: usize,
    pub function_assessments: Vec<FunctionAssessment>,
    pub diagnostics: Vec<Diagnostic>,
}

#[derive(Debug, Clone, Serialize, Default)]
pub struct Summary {
    pub files: usize,
    pub checked_functions: usize,
    pub ignored_functions: usize,
    pub diagnostics: usize,
}

#[derive(Debug, Clone, Serialize, Default)]
pub struct Report {
    pub summary: Summary,
    pub files: Vec<FileReport>,
}

impl Report {
    #[must_use]
    pub fn should_fail(&self, fail_on: Severity) -> bool {
        self.files.iter().any(|file| {
            file.diagnostics
                .iter()
                .any(|diagnostic| diagnostic.severity >= fail_on)
                || file.function_assessments.iter().any(|assessment| {
                    let purity_fails =
                        external_purity_is_finding(assessment.external_purity.as_ref())
                            && assessment
                                .external_purity
                                .as_ref()
                                .is_some_and(|purity| purity.severity >= fail_on);
                    let style_fails =
                        implementation_style_is_finding(assessment.implementation_style.as_ref())
                            && assessment
                                .implementation_style
                                .as_ref()
                                .is_some_and(|style| style.severity >= fail_on);
                    purity_fails || style_fails
                })
        })
    }
}

/// レポートを text または JSON へ整形します。
///
/// # Errors
///
/// JSON 生成時にシリアライズへ失敗した場合に返します。
pub fn render_report(report: &Report, format: OutputFormat) -> Result<String, serde_json::Error> {
    match format {
        OutputFormat::Json => serde_json::to_string_pretty(report),
        OutputFormat::Sarif => serde_json::to_string_pretty(&build_sarif_report(report)),
        OutputFormat::Text => Ok(render_text(report)),
    }
}

fn render_text(report: &Report) -> String {
    let mut lines = Vec::new();
    lines.push("fp-checker report".to_owned());
    lines.push(format!(
        "files={} checked_functions={} ignored_functions={} diagnostics={}",
        report.summary.files,
        report.summary.checked_functions,
        report.summary.ignored_functions,
        report.summary.diagnostics
    ));

    for file in &report.files {
        lines.push(String::new());
        lines.push(format!(
            "{} (checked={}, ignored={}, diagnostics={})",
            file.path,
            file.checked_functions,
            file.ignored_functions,
            file.diagnostics.len()
        ));
        for assessment in &file.function_assessments {
            let purity = assessment.external_purity.as_ref().map_or_else(
                || "disabled".to_owned(),
                |value| format!("{}[{}]", value.primary, value.severity),
            );
            let style = assessment.implementation_style.as_ref().map_or_else(
                || "disabled".to_owned(),
                |value| format!("{}[{}]", value.primary, value.severity),
            );
            lines.push(format!(
                "  function {} purity={} style={}",
                assessment.qualified_name, purity, style,
            ));
        }
        for diagnostic in &file.diagnostics {
            lines.push(format!(
                "  [{}] {}:{}:{} {} ({})",
                diagnostic.severity,
                diagnostic.path,
                diagnostic.location.start.line,
                diagnostic.location.start.column,
                diagnostic.message,
                diagnostic.rule_id
            ));
            if let Some(function) = &diagnostic.function {
                lines.push(format!("    function: {function}"));
            }
            if let Some(hint) = &diagnostic.hint {
                lines.push(format!("    hint: {hint}"));
            }
        }
    }

    lines.join("\n")
}

fn build_sarif_report(report: &Report) -> serde_json::Value {
    let mut artifacts = BTreeSet::new();
    let mut diagnostic_rules = BTreeSet::new();
    let mut assessment_rules = BTreeSet::new();
    let mut results = Vec::new();

    for file in &report.files {
        artifacts.insert(file.path.clone());
        for diagnostic in &file.diagnostics {
            artifacts.insert(diagnostic.path.clone());
            diagnostic_rules.insert(diagnostic.rule_id.clone());
            results.push(build_sarif_result(diagnostic));
        }
        for assessment in &file.function_assessments {
            artifacts.insert(assessment.location.path.clone());
            if let Some(result) = build_external_purity_sarif_result(assessment) {
                assessment_rules.insert(external_purity_rule_id(
                    assessment
                        .external_purity
                        .as_ref()
                        .expect("external purity exists")
                        .primary,
                ));
                results.push(result);
            }
            if let Some(result) = build_implementation_style_sarif_result(assessment) {
                assessment_rules.insert(implementation_style_rule_id(
                    assessment
                        .implementation_style
                        .as_ref()
                        .expect("implementation style exists")
                        .primary,
                ));
                results.push(result);
            }
        }
    }

    let mut sarif_rules = diagnostic_rules
        .iter()
        .map(build_sarif_rule)
        .collect::<Vec<_>>();
    sarif_rules.extend(
        assessment_rules
            .iter()
            .map(|rule_id| build_assessment_sarif_rule(rule_id)),
    );

    json!({
        "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
        "version": "2.1.0",
        "runs": [
            {
                "tool": {
                    "driver": {
                        "name": "fp-checker",
                        "informationUri": "https://github.com/sotanengel/python-fp-checker",
                        "rules": sarif_rules
                    }
                },
                "artifacts": artifacts.into_iter().map(|uri| {
                    json!({
                        "location": {
                            "uri": uri
                        }
                    })
                }).collect::<Vec<_>>(),
                "results": results,
                "properties": {
                    "summary": {
                        "files": report.summary.files,
                        "checked_functions": report.summary.checked_functions,
                        "ignored_functions": report.summary.ignored_functions,
                        "diagnostics": report.summary.diagnostics
                    }
                }
            }
        ]
    })
}

fn build_external_purity_sarif_result(
    assessment: &FunctionAssessment,
) -> Option<serde_json::Value> {
    let purity = assessment.external_purity.as_ref()?;
    if !external_purity_is_finding(Some(purity)) {
        return None;
    }

    Some(json!({
        "ruleId": external_purity_rule_id(purity.primary),
        "level": sarif_level(purity.severity),
        "message": {
            "text": format!("Function `{}` is classified as `{}`.", assessment.qualified_name, purity.primary)
        },
        "locations": [build_assessment_location(assessment)],
        "properties": {
            "kind": "function_assessment",
            "axis": "external_purity",
            "function": assessment.qualified_name,
            "category": purity.primary.to_string(),
            "categories": purity.categories.iter().map(ToString::to_string).collect::<Vec<_>>(),
            "reasons": purity.reasons
        }
    }))
}

fn build_implementation_style_sarif_result(
    assessment: &FunctionAssessment,
) -> Option<serde_json::Value> {
    let style = assessment.implementation_style.as_ref()?;
    if !implementation_style_is_finding(Some(style)) {
        return None;
    }

    Some(json!({
        "ruleId": implementation_style_rule_id(style.primary),
        "level": sarif_level(style.severity),
        "message": {
            "text": format!("Function `{}` is classified as `{}`.", assessment.qualified_name, style.primary)
        },
        "locations": [build_assessment_location(assessment)],
        "properties": {
            "kind": "function_assessment",
            "axis": "implementation_style",
            "function": assessment.qualified_name,
            "category": style.primary.to_string(),
            "categories": style.categories.iter().map(ToString::to_string).collect::<Vec<_>>(),
            "metrics": style.metrics
        }
    }))
}

fn build_assessment_location(assessment: &FunctionAssessment) -> serde_json::Value {
    json!({
        "physicalLocation": {
            "artifactLocation": {
                "uri": assessment.location.path
            },
            "region": {
                "startLine": assessment.location.start.line,
                "startColumn": assessment.location.start.column,
                "endLine": assessment.location.end.as_ref().map_or(assessment.location.start.line, |end| end.line),
                "endColumn": assessment.location.end.as_ref().map_or(assessment.location.start.column, |end| end.column)
            }
        }
    })
}

fn build_sarif_rule(rule_id: &RuleId) -> serde_json::Value {
    json!({
        "id": rule_id.as_str(),
        "name": rule_id.as_str(),
        "shortDescription": {
            "text": sarif_rule_description(rule_id)
        },
        "help": {
            "text": sarif_rule_help(rule_id)
        }
    })
}

fn build_assessment_sarif_rule(rule_id: &str) -> serde_json::Value {
    json!({
        "id": rule_id,
        "name": rule_id,
        "shortDescription": {
            "text": assessment_sarif_rule_description(rule_id)
        },
        "help": {
            "text": assessment_sarif_rule_help(rule_id)
        }
    })
}

fn build_sarif_result(diagnostic: &Diagnostic) -> serde_json::Value {
    json!({
        "ruleId": diagnostic.rule_id.as_str(),
        "level": sarif_level(diagnostic.severity),
        "message": {
            "text": diagnostic.message
        },
        "locations": [
            {
                "physicalLocation": {
                    "artifactLocation": {
                        "uri": diagnostic.path
                    },
                    "region": {
                        "startLine": diagnostic.location.start.line,
                        "startColumn": diagnostic.location.start.column,
                        "endLine": diagnostic.location.end.as_ref().map_or(diagnostic.location.start.line, |end| end.line),
                        "endColumn": diagnostic.location.end.as_ref().map_or(diagnostic.location.start.column, |end| end.column)
                    }
                }
            }
        ],
        "properties": build_sarif_properties(diagnostic)
    })
}

fn build_sarif_properties(diagnostic: &Diagnostic) -> serde_json::Value {
    let fix_hint = diagnostic.fix_hint.as_ref().map(build_sarif_fix_hint);
    json!({
        "function": diagnostic.function,
        "hint": diagnostic.hint,
        "fix_hint": fix_hint
    })
}

fn build_sarif_fix_hint(fix_hint: &FixHint) -> serde_json::Value {
    json!({
        "kind": fix_hint.kind,
        "summary": fix_hint.summary,
        "suggested_action": fix_hint.suggested_action
    })
}

fn sarif_level(severity: Severity) -> &'static str {
    match severity {
        Severity::Error => "error",
        Severity::Warning => "warning",
        Severity::Info => "note",
    }
}

fn external_purity_is_finding(assessment: Option<&ExternalPurityAssessment>) -> bool {
    assessment.is_some_and(|purity| purity.primary != ExternalPurityCategory::Pure)
}

fn implementation_style_is_finding(assessment: Option<&ImplementationStyleAssessment>) -> bool {
    assessment.is_some_and(|style| style.primary != ImplementationStyleCategory::NoReassignment)
}

fn external_purity_rule_id(category: ExternalPurityCategory) -> String {
    format!("external_purity_{category}")
}

fn implementation_style_rule_id(category: ImplementationStyleCategory) -> String {
    format!("implementation_style_{category}")
}

fn assessment_sarif_rule_description(rule_id: &str) -> String {
    if let Some(category) = rule_id.strip_prefix("external_purity_") {
        format!("Function-level external purity assessment for `{category}`.")
    } else if let Some(category) = rule_id.strip_prefix("implementation_style_") {
        format!("Function-level implementation-style assessment for `{category}`.")
    } else {
        "Function-level assessment.".to_owned()
    }
}

fn assessment_sarif_rule_help(rule_id: &str) -> String {
    if let Some(category) = rule_id.strip_prefix("external_purity_") {
        match category {
            "impure_reads_external_state" => {
                "Pass external state as explicit arguments instead of reading it implicitly."
                    .to_owned()
            }
            "impure_writes_external_state" => {
                "Return updated values instead of mutating externally visible state.".to_owned()
            }
            "impure_direct_io" => "Move I/O to the boundary layer.".to_owned(),
            "impure_nondeterministic" => {
                "Inject time, randomness, environment, and process state as dependencies."
                    .to_owned()
            }
            "impure_unknown_call" => {
                "Annotate the callee or add it to the allowlist/denylist.".to_owned()
            }
            "impure_exceptional_control_dependency" => {
                "Replace exception-driven behavior with explicit control flow.".to_owned()
            }
            _ => "Review the purity classification.".to_owned(),
        }
    } else if let Some(category) = rule_id.strip_prefix("implementation_style_") {
        match category {
            "has_reassignment" | "reassignment_heavy" => {
                "Reduce repeated assignment and prefer value-oriented flow.".to_owned()
            }
            "has_mutation" | "mutation_heavy" => {
                "Prefer returning new values over mutating objects in place.".to_owned()
            }
            "imperative_style_high" => {
                "Split the function and reduce mutation, branching, and intermediate state."
                    .to_owned()
            }
            _ => "Review the implementation-style classification.".to_owned(),
        }
    } else {
        "Review the function-level assessment.".to_owned()
    }
}

fn sarif_rule_description(rule_id: &RuleId) -> String {
    match rule_id {
        RuleId::ParseError => "Reports Python syntax errors.".to_owned(),
        RuleId::MutableGlobalState => "Detects reads and writes of mutable global state.".to_owned(),
        RuleId::AssignmentOveruse => "Detects functions with too many assignments.".to_owned(),
        RuleId::DirectIo => "Detects direct I/O calls.".to_owned(),
        RuleId::NonDeterministicUse => "Detects non-deterministic calls.".to_owned(),
        RuleId::ExternalStateSideEffect => "Detects side effects on external state.".to_owned(),
        RuleId::MutableDefaultArgument => "Detects mutable default arguments.".to_owned(),
        RuleId::DirectEffectfulCall => "Detects direct calls with strong side effects.".to_owned(),
        RuleId::GlobalNonlocalUsage => "Detects `global` and `nonlocal` usage.".to_owned(),
        RuleId::HiddenMutation => "Detects hidden mutation of parameters and local state.".to_owned(),
        RuleId::LoopSideEffect => "Detects side effects inside loops.".to_owned(),
        RuleId::ExceptionControlFlow => "Detects exception-based control flow.".to_owned(),
        RuleId::ClassMethodSideEffect => {
            "Detects state changes and side effects inside class methods.".to_owned()
        }
        RuleId::ReferentialTransparencyBreak => {
            "Detects implicit dependencies and dynamic evaluation that break referential transparency.".to_owned()
        }
        RuleId::DeprecatedApiUse => "Detects deprecated API usage.".to_owned(),
        RuleId::Custom(id) => format!("Detects usages that match custom rule `{id}`."),
    }
}

fn sarif_rule_help(rule_id: &RuleId) -> String {
    match rule_id {
        RuleId::ParseError => "Fix the syntax error and run the checker again.".to_owned(),
        RuleId::MutableGlobalState => {
            "Pass state explicitly through arguments and return values.".to_owned()
        }
        RuleId::AssignmentOveruse => {
            "Reduce intermediate state and split the logic into smaller functions.".to_owned()
        }
        RuleId::DirectIo => "Move I/O to the function boundary.".to_owned(),
        RuleId::NonDeterministicUse => {
            "Inject time, randomness, and similar values as dependencies.".to_owned()
        }
        RuleId::ExternalStateSideEffect => "Return a new value instead of mutating external state.".to_owned(),
        RuleId::MutableDefaultArgument => {
            "Use `None` as the default and create the value inside the function.".to_owned()
        }
        RuleId::DirectEffectfulCall => {
            "Abstract the effectful call and inject it from the outside.".to_owned()
        }
        RuleId::GlobalNonlocalUsage => {
            "Avoid shared state and convert it into explicit value passing.".to_owned()
        }
        RuleId::HiddenMutation => {
            "Avoid in-place mutation and return a new collection.".to_owned()
        }
        RuleId::LoopSideEffect => "Move loop side effects outside the loop and aggregate them.".to_owned(),
        RuleId::ExceptionControlFlow => {
            "Use explicit branching or result values instead of exceptions.".to_owned()
        }
        RuleId::ClassMethodSideEffect => {
            "Do not update object state directly inside the method; return a new value and apply it outside.".to_owned()
        }
        RuleId::ReferentialTransparencyBreak => {
            "Replace implicit dependencies with arguments or explicit dependency injection.".to_owned()
        }
        RuleId::DeprecatedApiUse => "Replace it with a recommended alternative API.".to_owned(),
        RuleId::Custom(_) => "Follow the guidance defined in the custom rule configuration.".to_owned(),
    }
}
