use std::collections::HashSet;
use std::io::Write;

use anyhow::Result;
use log::warn;
use serde::{Serialize, Serializer};
use serde_json::json;

use ruff_db::diagnostic::{Diagnostic, SecondaryCode};
use ruff_source_file::{OneIndexed, SourceFile};
use ruff_text_size::{Ranged, TextRange};

use crate::VERSION;
use crate::fs::normalize_path;
use crate::message::{Emitter, EmitterContext};
use crate::registry::{Linter, RuleNamespace};

/// An emitter for producing SARIF 2.1.0-compliant JSON output.
///
/// Static Analysis Results Interchange Format (SARIF) is a standard format
/// for static analysis results. For full specification, see:
/// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
pub struct SarifEmitter;

impl Emitter for SarifEmitter {
    fn emit(
        &mut self,
        writer: &mut dyn Write,
        diagnostics: &[Diagnostic],
        _context: &EmitterContext,
    ) -> Result<()> {
        let results = diagnostics
            .iter()
            .map(SarifResult::from_message)
            .collect::<Result<Vec<_>>>()?;

        let unique_rules: HashSet<_> = results
            .iter()
            .filter_map(|result| result.rule_id.as_secondary_code())
            .collect();
        let mut rules: Vec<SarifRule> = unique_rules.into_iter().map(SarifRule::from).collect();
        rules.sort_by(|a, b| a.code.cmp(b.code));

        let output = json!({
            "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
            "version": "2.1.0",
            "runs": [{
                "tool": {
                    "driver": {
                        "name": "ruff",
                        "informationUri": "https://github.com/astral-sh/ruff",
                        "rules": rules,
                        "version": VERSION.to_string(),
                    }
                },
                "results": results,
            }],
        });
        serde_json::to_writer_pretty(writer, &output)?;
        Ok(())
    }
}

#[derive(Debug, Clone)]
struct SarifRule<'a> {
    name: &'a str,
    code: &'a SecondaryCode,
    linter: &'a str,
    summary: &'a str,
    explanation: Option<&'a str>,
    url: Option<String>,
}

impl<'a> From<&'a SecondaryCode> for SarifRule<'a> {
    fn from(code: &'a SecondaryCode) -> Self {
        // This is a manual re-implementation of Rule::from_code, but we also want the Linter. This
        // avoids calling Linter::parse_code twice.
        let (linter, suffix) = Linter::parse_code(code).unwrap();
        let rule = linter
            .all_rules()
            .find(|rule| rule.noqa_code().suffix() == suffix)
            .expect("Expected a valid noqa code corresponding to a rule");
        Self {
            name: rule.into(),
            code,
            linter: linter.name(),
            summary: rule.message_formats()[0],
            explanation: rule.explanation(),
            url: rule.url(),
        }
    }
}

impl Serialize for SarifRule<'_> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        json!({
            "id": self.code,
            "shortDescription": {
                "text": self.summary,
            },
            "fullDescription": {
                "text": self.explanation,
            },
            "help": {
                "text": self.summary,
            },
            "helpUri": self.url,
            "properties": {
                "id": self.code,
                "kind": self.linter,
                "name": self.name,
                "problem.severity": "error".to_string(),
            },
        })
        .serialize(serializer)
    }
}

#[derive(Debug)]
enum RuleCode<'a> {
    SecondaryCode(&'a SecondaryCode),
    LintId(&'a str),
}

impl RuleCode<'_> {
    fn as_secondary_code(&self) -> Option<&SecondaryCode> {
        match self {
            RuleCode::SecondaryCode(code) => Some(code),
            RuleCode::LintId(_) => None,
        }
    }

    fn as_str(&self) -> &str {
        match self {
            RuleCode::SecondaryCode(code) => code.as_str(),
            RuleCode::LintId(id) => id,
        }
    }
}

impl Serialize for RuleCode<'_> {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(self.as_str())
    }
}

impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
    fn from(code: &'a Diagnostic) -> Self {
        match code.secondary_code() {
            Some(diagnostic) => Self::SecondaryCode(diagnostic),
            None => Self::LintId(code.id().as_str()),
        }
    }
}

/// Represents a single result in a SARIF 2.1.0 report.
///
/// See the SARIF 2.1.0 specification for details:
/// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifResult<'a> {
    rule_id: RuleCode<'a>,
    level: String,
    message: SarifMessage,
    locations: Vec<SarifLocation>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    fixes: Vec<SarifFix>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifMessage {
    text: String,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifPhysicalLocation {
    artifact_location: SarifArtifactLocation,
    region: SarifRegion,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifLocation {
    physical_location: SarifPhysicalLocation,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifFix {
    description: RuleDescription,
    artifact_changes: Vec<SarifArtifactChange>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct RuleDescription {
    text: Option<String>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifArtifactChange {
    artifact_location: SarifArtifactLocation,
    replacements: Vec<SarifReplacement>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifArtifactLocation {
    uri: String,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifReplacement {
    deleted_region: SarifRegion,
    #[serde(skip_serializing_if = "Option::is_none")]
    inserted_content: Option<InsertedContent>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct InsertedContent {
    text: String,
}

#[derive(Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "camelCase")]
struct SarifRegion {
    start_line: OneIndexed,
    start_column: OneIndexed,
    end_line: OneIndexed,
    end_column: OneIndexed,
}

impl<'a> SarifResult<'a> {
    fn range_to_sarif_region(source_file: &SourceFile, range: TextRange) -> SarifRegion {
        let source_code = source_file.to_source_code();
        let start_location = source_code.line_column(range.start());
        let end_location = source_code.line_column(range.end());

        SarifRegion {
            start_line: start_location.line,
            start_column: start_location.column,
            end_line: end_location.line,
            end_column: end_location.column,
        }
    }

    fn fix(diagnostic: &'a Diagnostic, uri: &str) -> Option<SarifFix> {
        let fix = diagnostic.fix()?;

        let Some(source_file) = diagnostic.ruff_source_file() else {
            debug_assert!(
                false,
                "Omitting the fix for diagnostic with id `{}` because the source file is missing. This is a bug in Ruff, please report an issue.",
                diagnostic.id()
            );

            warn!(
                "Omitting the fix for diagnostic with id `{}` because the source file is missing. This is a bug in Ruff, please report an issue.",
                diagnostic.id()
            );
            return None;
        };

        let fix_description = diagnostic
            .first_help_text()
            .map(std::string::ToString::to_string);

        let replacements: Vec<SarifReplacement> = fix
            .edits()
            .iter()
            .map(|edit| {
                let range = edit.range();
                let deleted_region = Self::range_to_sarif_region(source_file, range);
                SarifReplacement {
                    deleted_region,
                    inserted_content: edit.content().map(|content| InsertedContent {
                        text: content.to_string(),
                    }),
                }
            })
            .collect();

        let artifact_changes = vec![SarifArtifactChange {
            artifact_location: SarifArtifactLocation {
                uri: uri.to_string(),
            },
            replacements,
        }];

        Some(SarifFix {
            description: RuleDescription {
                text: fix_description,
            },
            artifact_changes,
        })
    }

    #[allow(clippy::unnecessary_wraps)]
    fn uri(diagnostic: &Diagnostic) -> Result<String> {
        let path = normalize_path(&*diagnostic.expect_ruff_filename());
        #[cfg(not(target_arch = "wasm32"))]
        return url::Url::from_file_path(&path)
            .map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))
            .map(|u| u.to_string());
        #[cfg(target_arch = "wasm32")]
        return Ok(format!("file://{}", path.display()));
    }

    fn from_message(diagnostic: &'a Diagnostic) -> Result<Self> {
        let start_location = diagnostic.ruff_start_location().unwrap_or_default();
        let end_location = diagnostic.ruff_end_location().unwrap_or_default();
        let region = SarifRegion {
            start_line: start_location.line,
            start_column: start_location.column,
            end_line: end_location.line,
            end_column: end_location.column,
        };

        let uri = Self::uri(diagnostic)?;

        Ok(Self {
            rule_id: RuleCode::from(diagnostic),
            level: "error".to_string(),
            message: SarifMessage {
                text: diagnostic.concise_message().to_string(),
            },
            fixes: Self::fix(diagnostic, &uri).into_iter().collect(),
            locations: vec![SarifLocation {
                physical_location: SarifPhysicalLocation {
                    artifact_location: SarifArtifactLocation { uri },
                    region,
                },
            }],
        })
    }
}

#[cfg(test)]
mod tests {
    use crate::message::SarifEmitter;
    use crate::message::tests::{
        capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
    };

    fn get_output() -> String {
        let mut emitter = SarifEmitter {};
        capture_emitter_output(&mut emitter, &create_diagnostics())
    }

    #[test]
    fn valid_json() {
        let content = get_output();
        serde_json::from_str::<serde_json::Value>(&content).unwrap();
    }

    #[test]
    fn valid_syntax_error_json() {
        let mut emitter = SarifEmitter {};
        let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
        serde_json::from_str::<serde_json::Value>(&content).unwrap();
    }

    #[test]
    fn test_results() {
        let content = get_output();
        let value = serde_json::from_str::<serde_json::Value>(&content).unwrap();

        insta::assert_json_snapshot!(value, {
            ".runs[0].tool.driver.version" => "[VERSION]",
            ".runs[0].results[].locations[].physicalLocation.artifactLocation.uri" => "[URI]",
            ".runs[0].results[].fixes[].artifactChanges[].artifactLocation.uri" => "[URI]",
        });
    }
}
