use crate::config::OutputFormat;
use crate::model::{Diagnostic, FixHint, 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 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)
        })
    }
}

/// レポートを 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 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 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());
            rules.insert(diagnostic.rule_id.clone());
            results.push(build_sarif_result(diagnostic));
        }
    }

    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": rules.iter().map(build_sarif_rule).collect::<Vec<_>>()
                    }
                },
                "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_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_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 sarif_rule_description(rule_id: &RuleId) -> String {
    match rule_id {
        RuleId::ParseError => "Python 構文エラーを報告します。".to_owned(),
        RuleId::MutableGlobalState => "可変グローバル状態の参照や更新を検出します。".to_owned(),
        RuleId::AssignmentOveruse => "代入の多い関数を検出します。".to_owned(),
        RuleId::DirectIo => "直接 I/O 呼び出しを検出します。".to_owned(),
        RuleId::NonDeterministicUse => "非決定的な呼び出しを検出します。".to_owned(),
        RuleId::ExternalStateSideEffect => "外部状態への副作用を検出します。".to_owned(),
        RuleId::MutableDefaultArgument => "ミュータブルなデフォルト引数を検出します。".to_owned(),
        RuleId::DirectEffectfulCall => "副作用の強い直接呼び出しを検出します。".to_owned(),
        RuleId::GlobalNonlocalUsage => "`global` / `nonlocal` を検出します。".to_owned(),
        RuleId::HiddenMutation => "引数やローカル状態の hidden mutation を検出します。".to_owned(),
        RuleId::LoopSideEffect => "ループ内の副作用を検出します。".to_owned(),
        RuleId::ExceptionControlFlow => "例外ベースの制御フローを検出します。".to_owned(),
        RuleId::ClassMethodSideEffect => {
            "クラスメソッド内での状態変更や副作用を検出します。".to_owned()
        }
        RuleId::ReferentialTransparencyBreak => {
            "参照透過性を壊す暗黙依存や動的評価を検出します。".to_owned()
        }
        RuleId::DeprecatedApiUse => "非推奨 API の使用を検出します。".to_owned(),
        RuleId::Custom(id) => format!("カスタムルール `{id}` に一致する使用箇所を検出します。"),
    }
}

fn sarif_rule_help(rule_id: &RuleId) -> String {
    match rule_id {
        RuleId::ParseError => "構文エラーを修正してから再実行してください。".to_owned(),
        RuleId::MutableGlobalState => "状態を引数と戻り値で明示的に受け渡してください。".to_owned(),
        RuleId::AssignmentOveruse => "中間状態を減らし、小さな関数へ分割してください。".to_owned(),
        RuleId::DirectIo => "I/O は関数境界へ分離してください。".to_owned(),
        RuleId::NonDeterministicUse => "乱数や時刻は依存として注入してください。".to_owned(),
        RuleId::ExternalStateSideEffect => {
            "外部状態の変更ではなく新しい値を返してください。".to_owned()
        }
        RuleId::MutableDefaultArgument => {
            "None をデフォルトにして関数内で生成してください。".to_owned()
        }
        RuleId::DirectEffectfulCall => {
            "副作用呼び出しを抽象化して外から注入してください。".to_owned()
        }
        RuleId::GlobalNonlocalUsage => {
            "共有状態を避け、明示的な値受け渡しへ変換してください。".to_owned()
        }
        RuleId::HiddenMutation => {
            "破壊的変更を避け、新しいコレクションを返してください。".to_owned()
        }
        RuleId::LoopSideEffect => "ループ内副作用を外へ移して集約してください。".to_owned(),
        RuleId::ExceptionControlFlow => {
            "例外ではなく明示的な分岐や結果型で表現してください。".to_owned()
        }
        RuleId::ClassMethodSideEffect => {
            "メソッド内部で状態を直接更新せず、新しい値を返して外側で反映してください。".to_owned()
        }
        RuleId::ReferentialTransparencyBreak => {
            "暗黙依存を引数または明示的な依存注入へ置き換えてください。".to_owned()
        }
        RuleId::DeprecatedApiUse => "推奨される代替 API へ置き換えてください。".to_owned(),
        RuleId::Custom(_) => "設定ファイルで定義した指摘内容に従って修正してください。".to_owned(),
    }
}
