//! SARIF output format for CI integration.
//!
//! SARIF (Static Analysis Results Interchange Format) v2.1.0 is supported by:
//! - GitHub Code Scanning
//! - GitLab SAST
//! - Azure DevOps
//!
//! Reference: <https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html>

use super::{object_value_display, verdict_label, ReportFormatter};
use crate::types::{ScanResult, Verdict};

/// SARIF report formatter for CI integration.
pub struct SarifReport;

impl ReportFormatter for SarifReport {
    fn format(&self, result: &ScanResult) -> String {
        // Build SARIF rules from unique conflict types
        let mut rules = Vec::new();
        let mut rule_indices: std::collections::HashMap<String, usize> =
            std::collections::HashMap::new();

        for conflict in &result.conflicts {
            let rule_id = format!("aphoria/{}", extract_rule_id(&conflict.claim.concept_path));
            if !rule_indices.contains_key(&rule_id) {
                let idx = rules.len();
                rule_indices.insert(rule_id.clone(), idx);

                let level = match conflict.verdict {
                    Verdict::Block => "error",
                    Verdict::Flag | Verdict::Drift => "warning",
                    Verdict::Pass | Verdict::Ack => "note",
                };

                // Generate help URI based on RFC citation if available
                let help_uri = conflict
                    .conflicts
                    .first()
                    .and_then(|s| s.rfc_citation.as_ref())
                    .map(|citation| {
                        if citation.starts_with("RFC ") {
                            let rfc_num = citation.strip_prefix("RFC ").unwrap_or("");
                            format!("https://www.rfc-editor.org/rfc/rfc{}", rfc_num)
                        } else if citation.starts_with("OWASP") {
                            "https://owasp.org/www-project-top-ten/".to_string()
                        } else {
                            format!(
                                "https://github.com/orchard9/aphoria/rules/{}",
                                extract_rule_id(&conflict.claim.concept_path)
                            )
                        }
                    })
                    .unwrap_or_else(|| {
                        format!(
                            "https://github.com/orchard9/aphoria/rules/{}",
                            extract_rule_id(&conflict.claim.concept_path)
                        )
                    });

                rules.push(serde_json::json!({
                    "id": rule_id,
                    "shortDescription": {
                        "text": conflict.claim.description,
                    },
                    "defaultConfiguration": {
                        "level": level,
                    },
                    "helpUri": help_uri,
                }));
            }
        }

        // Build SARIF results
        let results: Vec<serde_json::Value> = result
            .conflicts
            .iter()
            .map(|conflict| {
                let rule_id = format!("aphoria/{}", extract_rule_id(&conflict.claim.concept_path));
                let rule_index = rule_indices.get(&rule_id).copied().unwrap_or(0);

                let level = match conflict.verdict {
                    Verdict::Block => "error",
                    Verdict::Flag | Verdict::Drift => "warning",
                    Verdict::Pass | Verdict::Ack => "note",
                };

                // Build message with authoritative source details
                let source_details: Vec<String> = conflict
                    .conflicts
                    .iter()
                    .map(|s| {
                        let mut detail = format!(
                            "{:?} (Tier {}): {}",
                            s.source_class,
                            s.source_class.tier(),
                            object_value_display(&s.value)
                        );
                        // Include policy source info if available
                        if let Some(ps) = &s.policy_source {
                            let signer = ps.signer_name.as_deref().unwrap_or(&ps.issuer_hex);
                            detail.push_str(&format!(
                                " [Source: {} v{} ({})",
                                ps.pack_name, ps.pack_version, signer
                            ));
                            if let Some(contact) = &ps.contact {
                                detail.push_str(&format!(", Contact: {}", contact));
                            }
                            detail.push(']');
                        }
                        detail
                    })
                    .collect();

                let message = format!(
                    "{}\nYour code: {} = {}\nAuthoritative: {}",
                    conflict.claim.description,
                    conflict.claim.predicate,
                    object_value_display(&conflict.claim.value),
                    source_details.join("; ")
                );

                serde_json::json!({
                    "ruleId": rule_id,
                    "ruleIndex": rule_index,
                    "level": level,
                    "message": {
                        "text": message,
                    },
                    "locations": [{
                        "physicalLocation": {
                            "artifactLocation": {
                                "uri": conflict.claim.file,
                                "uriBaseId": "%SRCROOT%",
                            },
                            "region": {
                                "startLine": conflict.claim.line,
                            }
                        }
                    }],
                    "properties": {
                        "conflict_score": conflict.conflict_score,
                        "verdict": verdict_label(conflict.verdict),
                    }
                })
            })
            .collect();

        // Add drift rules and results
        for drift in &result.drifts {
            let rule_id = format!("aphoria/drift/{}", extract_rule_id(&drift.claim.concept_path));
            if !rule_indices.contains_key(&rule_id) {
                let idx = rules.len();
                rule_indices.insert(rule_id.clone(), idx);

                rules.push(serde_json::json!({
                    "id": rule_id,
                    "shortDescription": {
                        "text": format!("Value drift detected for {}", drift.claim.concept_path),
                    },
                    "defaultConfiguration": {
                        "level": "warning",
                    },
                    "helpUri": "https://github.com/orchard9/aphoria/docs/drift",
                }));
            }
        }

        // Add drift results
        let drift_results: Vec<serde_json::Value> = result
            .drifts
            .iter()
            .map(|drift| {
                let rule_id = format!("aphoria/drift/{}", extract_rule_id(&drift.claim.concept_path));
                let rule_index = rule_indices.get(&rule_id).copied().unwrap_or(0);

                let message = format!(
                    "Value changed from prior observation.\nCurrent: {}\nPrior: {} (recorded at {}:{})",
                    object_value_display(&drift.claim.value),
                    object_value_display(&drift.prior.value),
                    drift.prior.file,
                    drift.prior.line
                );

                serde_json::json!({
                    "ruleId": rule_id,
                    "ruleIndex": rule_index,
                    "level": "warning",
                    "message": {
                        "text": message,
                    },
                    "locations": [{
                        "physicalLocation": {
                            "artifactLocation": {
                                "uri": drift.claim.file,
                                "uriBaseId": "%SRCROOT%",
                            },
                            "region": {
                                "startLine": drift.claim.line,
                            }
                        }
                    }],
                    "properties": {
                        "verdict": verdict_label(drift.verdict),
                        "prior_value": object_value_display(&drift.prior.value),
                        "prior_timestamp": drift.prior.timestamp,
                    }
                })
            })
            .collect();

        // Add deprecated usage rules and results
        for usage in &result.deprecated_usages {
            let rule_id = format!("aphoria/deprecated/{}", usage.pattern_name);
            if !rule_indices.contains_key(&rule_id) {
                let idx = rules.len();
                rule_indices.insert(rule_id.clone(), idx);

                let level = match usage.severity() {
                    "OVERDUE" => "error",
                    "URGENT" => "warning",
                    _ => "note",
                };

                rules.push(serde_json::json!({
                    "id": rule_id,
                    "shortDescription": {
                        "text": format!("Deprecated pattern: {}", usage.pattern_name),
                    },
                    "fullDescription": {
                        "text": usage.reason.clone(),
                    },
                    "defaultConfiguration": {
                        "level": level,
                    },
                    "helpUri": usage.migration_guide.clone().unwrap_or_else(|| {
                        "https://github.com/orchard9/aphoria/docs/deprecation".to_string()
                    }),
                }));
            }
        }

        // Add deprecated usage results
        let deprecated_results: Vec<serde_json::Value> = result
            .deprecated_usages
            .iter()
            .map(|usage| {
                let rule_id = format!("aphoria/deprecated/{}", usage.pattern_name);
                let rule_index = rule_indices.get(&rule_id).copied().unwrap_or(0);

                let level = match usage.severity() {
                    "OVERDUE" => "error",
                    "URGENT" => "warning",
                    _ => "note",
                };

                let mut message = format!(
                    "Deprecated pattern '{}' detected.\nReason: {}",
                    usage.pattern_name, usage.reason
                );

                if let Some(ref replacement) = usage.superseded_by {
                    message.push_str(&format!("\nReplace with: {}", replacement));
                }

                if let Some(days) = usage.days_until_sunset {
                    if days < 0 {
                        message.push_str(&format!("\nSunset: OVERDUE by {} days", -days));
                    } else {
                        message.push_str(&format!("\nSunset: {} days remaining", days));
                    }
                }

                serde_json::json!({
                    "ruleId": rule_id,
                    "ruleIndex": rule_index,
                    "level": level,
                    "message": {
                        "text": message,
                    },
                    "locations": [{
                        "physicalLocation": {
                            "artifactLocation": {
                                "uri": usage.file_path,
                                "uriBaseId": "%SRCROOT%",
                            },
                            "region": {
                                "startLine": usage.line,
                            }
                        }
                    }],
                    "properties": {
                        "pattern_id": usage.pattern_id.to_string(),
                        "severity": usage.severity(),
                        "days_until_sunset": usage.days_until_sunset,
                    }
                })
            })
            .collect();

        // Add claims if present (as informational-level results)
        let claims_results: Vec<serde_json::Value> = if let Some(claims) = &result.claims {
            // Add a single rule for all claims
            if !claims.is_empty() && !rule_indices.contains_key("aphoria/claim") {
                let idx = rules.len();
                rule_indices.insert("aphoria/claim".to_string(), idx);
                rules.push(serde_json::json!({
                    "id": "aphoria/claim",
                    "shortDescription": {
                        "text": "Extracted claim (no conflict detected)",
                    },
                    "defaultConfiguration": {
                        "level": "note",
                    },
                    "helpUri": "https://github.com/orchard9/aphoria/docs/claims",
                }));
            }

            claims
                .iter()
                .map(|claim| {
                    let rule_index = rule_indices.get("aphoria/claim").copied().unwrap_or(0);
                    let message = format!(
                        "{}\n{} = {}",
                        claim.description,
                        claim.predicate,
                        object_value_display(&claim.value)
                    );

                    serde_json::json!({
                        "ruleId": "aphoria/claim",
                        "ruleIndex": rule_index,
                        "level": "note",
                        "message": {
                            "text": message,
                        },
                        "locations": [{
                            "physicalLocation": {
                                "artifactLocation": {
                                    "uri": claim.file,
                                    "uriBaseId": "%SRCROOT%",
                                },
                                "region": {
                                    "startLine": claim.line,
                                }
                            }
                        }],
                        "properties": {
                            "concept_path": claim.concept_path,
                            "confidence": claim.confidence,
                        }
                    })
                })
                .collect()
        } else {
            Vec::new()
        };

        // Combine all results
        let mut all_results = results;
        all_results.extend(drift_results);
        all_results.extend(deprecated_results);
        all_results.extend(claims_results);

        let sarif = serde_json::json!({
            "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
            "version": "2.1.0",
            "runs": [{
                "tool": {
                    "driver": {
                        "name": "aphoria",
                        "version": env!("CARGO_PKG_VERSION"),
                        "informationUri": "https://github.com/orchard9/aphoria",
                        "rules": rules,
                    }
                },
                "results": all_results,
                "invocations": [{
                    "executionSuccessful": true,
                    "properties": {
                        "scan_id": result.scan_id,
                        "files_scanned": result.files_scanned,
                        "claims_extracted": result.claims_extracted,
                        "drifts_detected": result.drift_count(),
                        "deprecated_usages": result.deprecated_usage_count(),
                    }
                }]
            }]
        });

        serde_json::to_string_pretty(&sarif).unwrap_or_else(|_| sarif.to_string())
    }
}

/// Extract a rule ID from a concept path.
///
/// e.g., `code://rust/myapp/tls/cert_verification` -> `tls/cert_verification`
fn extract_rule_id(concept_path: &str) -> String {
    // Strip the scheme and project prefix, keep the meaningful tail
    if let Some(after_scheme) = concept_path.split("://").nth(1) {
        // Skip language and project segments (first two after scheme)
        let segments: Vec<&str> = after_scheme.split('/').collect();
        if segments.len() > 2 {
            segments[2..].join("/")
        } else {
            after_scheme.to_string()
        }
    } else {
        concept_path.to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{ConflictResult, ConflictingSource, ExtractedClaim};
    use stemedb_core::types::{ObjectValue, SourceClass};

    #[test]
    fn test_sarif_structure() {
        let formatter = SarifReport;
        let result = ScanResult {
            project: "testproject".to_string(),
            scan_id: "scan-789".to_string(),
            files_scanned: 42,
            claims_extracted: 5,
            conflicts: vec![ConflictResult {
                claim: ExtractedClaim {
                    concept_path: "code://rust/testproject/tls/cert_verification".to_string(),
                    predicate: "enabled".to_string(),
                    value: ObjectValue::Boolean(false),
                    file: "src/client.rs".to_string(),
                    line: 23,
                    matched_text: "danger_accept_invalid_certs(true)".to_string(),
                    confidence: 1.0,
                    description: "TLS certificate verification disabled".to_string(),
                },
                conflicts: vec![ConflictingSource {
                    path: "rfc://5246/tls/cert_verification".to_string(),
                    source_class: SourceClass::Regulatory,
                    value: ObjectValue::Boolean(true),
                    confidence: 1.0,
                    rfc_citation: Some("RFC 5246".to_string()),
                    policy_source: None,
                }],
                conflict_score: 0.92,
                verdict: Verdict::Block,
                acknowledged: None,
                trace: None,
            }],
            drifts: vec![],
            format: "sarif".to_string(),
            debug: false,
            observations_recorded: 0,
            timing: None,
            claims: None,
            deprecated_usages: vec![],
        };

        let output = formatter.format(&result);
        let parsed: serde_json::Value = serde_json::from_str(&output).expect("valid json");

        // SARIF version
        assert_eq!(parsed["version"], "2.1.0");

        // Tool info
        assert_eq!(parsed["runs"][0]["tool"]["driver"]["name"], "aphoria");

        // Rules
        let rules = parsed["runs"][0]["tool"]["driver"]["rules"].as_array().expect("rules array");
        assert_eq!(rules.len(), 1);
        assert_eq!(rules[0]["id"], "aphoria/tls/cert_verification");

        // Results
        let results = parsed["runs"][0]["results"].as_array().expect("results array");
        assert_eq!(results.len(), 1);
        assert_eq!(results[0]["level"], "error");
        assert_eq!(
            results[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
            "src/client.rs"
        );
        assert_eq!(results[0]["locations"][0]["physicalLocation"]["region"]["startLine"], 23);
    }

    #[test]
    fn test_sarif_empty() {
        let formatter = SarifReport;
        let result = ScanResult::stub(&std::path::PathBuf::from("."), "sarif");
        let output = formatter.format(&result);
        let parsed: serde_json::Value = serde_json::from_str(&output).expect("valid json");

        assert_eq!(parsed["version"], "2.1.0");
        assert_eq!(parsed["runs"][0]["results"].as_array().map(|a| a.len()), Some(0));
    }

    #[test]
    fn test_extract_rule_id() {
        assert_eq!(
            extract_rule_id("code://rust/myapp/tls/cert_verification"),
            "tls/cert_verification"
        );
        assert_eq!(
            extract_rule_id("code://go/myapp/jwt/audience_validation"),
            "jwt/audience_validation"
        );
        assert_eq!(extract_rule_id("simple"), "simple");
    }
}
