//! Markdown output format for documentation and PR comments.
//!
//! Produces a full markdown document with summary table,
//! detailed conflict sections, and action items.

use super::{extract_leaf_concept, object_value_display, verdict_label, ReportFormatter};
use crate::types::convergence::ConvergenceSeverity;
use crate::types::{ScanResult, Verdict};
use crate::verify::AuditVerdict;

/// Markdown report formatter.
pub struct MarkdownReport;

impl ReportFormatter for MarkdownReport {
    fn format(&self, result: &ScanResult) -> String {
        let mut out = String::new();

        // Title
        out.push_str(&format!("# Aphoria Scan: {}\n\n", result.project));

        // Summary
        out.push_str(&format!(
            "**{}** files scanned | **{}** observations",
            result.files_scanned, result.claims_extracted,
        ));

        if let Some(ref verify) = result.verify {
            out.push_str(&format!(
                " | **{}** claims ({} pass, {} conflict, {} missing)",
                verify.summary.total_claims,
                verify.summary.pass,
                verify.summary.conflict,
                verify.summary.missing,
            ));
        }

        if !result.conflicts.is_empty() {
            out.push_str(&format!(" | **{}** authority conflicts", result.conflicts.len(),));
        }
        if result.has_drifts() {
            out.push_str(&format!(" | **{}** drifts", result.drift_count()));
        }
        out.push('\n');

        if result.strict {
            out.push_str("\n**Mode:** strict (BLOCK >= 0.50, FLAG >= 0.30)\n");
        }
        out.push('\n');

        // Claim verification section
        if let Some(ref verify) = result.verify {
            let claim_results: Vec<_> =
                verify.results.iter().filter(|r| r.claim.is_some()).collect();

            if !claim_results.is_empty() {
                out.push_str("## Claim Verification\n\n");
                out.push_str("| Verdict | Claim | Invariant | Explanation |\n");
                out.push_str("|---------|-------|-----------|-------------|\n");

                for vr in &claim_results {
                    if let Some(ref claim) = vr.claim {
                        let verdict_str = match vr.verdict {
                            AuditVerdict::Pass => "PASS",
                            AuditVerdict::Conflict => "CONFLICT",
                            AuditVerdict::Missing => "MISSING",
                            AuditVerdict::Unclaimed => "UNCLAIMED",
                        };
                        out.push_str(&format!(
                            "| {} | `{}` | {} | {} |\n",
                            verdict_str, claim.id, claim.invariant, vr.explanation,
                        ));
                    }
                }
                out.push('\n');
            }
        }

        if result.conflicts.is_empty() && result.drifts.is_empty() {
            if result.verify.is_none() {
                out.push_str("No claims found. Run `aphoria claims create` to author claims.\n");
            }

            // Still show observations if requested
            if let Some(claims) = &result.claims {
                out.push_str("\n## Extracted Observations\n\n");

                if claims.is_empty() {
                    out.push_str("No observations extracted.\n\n");
                } else {
                    out.push_str("| Concept | Value | File | Line | Confidence |\n");
                    out.push_str("|---------|-------|------|------|------------|\n");

                    for claim in claims {
                        out.push_str(&format!(
                            "| `{}` | `{}` | `{}` | {} | {:.0}% |\n",
                            extract_leaf_concept(&claim.concept_path),
                            object_value_display(&claim.value),
                            claim.file,
                            claim.line,
                            claim.confidence * 100.0,
                        ));
                    }
                    out.push('\n');
                }
            }

            // Show convergence suggestions even when there are no authority conflicts
            append_convergence_section(&mut out, result);

            return out;
        }

        // Verdict badges
        let blocks = result.count_by_verdict(Verdict::Block);
        let flags = result.count_by_verdict(Verdict::Flag);
        let drifts = result.drift_count();
        if blocks > 0 {
            out.push_str(&format!("**{blocks} BLOCK** "));
        }
        if flags > 0 {
            out.push_str(&format!("**{flags} FLAG** "));
        }
        if drifts > 0 {
            out.push_str(&format!("**{drifts} DRIFT** "));
        }
        out.push('\n');
        out.push('\n');

        // Summary table
        out.push_str("| Verdict | Concept | Citation | File | Score |\n");
        out.push_str("|---------|---------|----------|------|-------|\n");

        for conflict in &result.conflicts {
            let concept = extract_leaf_concept(&conflict.claim.concept_path);

            // Get RFC/OWASP citation
            let citation = conflict
                .conflicts
                .first()
                .and_then(|s| s.rfc_citation.as_ref())
                .map(|c| c.as_str())
                .unwrap_or("-");

            out.push_str(&format!(
                "| {} | `{}` | {} | `{}:{}` | {:.2} |\n",
                verdict_label(conflict.verdict),
                concept,
                citation,
                conflict.claim.file,
                conflict.claim.line,
                conflict.conflict_score,
            ));
        }
        out.push('\n');

        // Detailed sections for BLOCK and FLAG
        let actionable: Vec<_> = result
            .conflicts
            .iter()
            .filter(|c| c.verdict == Verdict::Block || c.verdict == Verdict::Flag)
            .collect();

        if !actionable.is_empty() {
            out.push_str("## Details\n\n");

            for conflict in actionable {
                out.push_str(&format!(
                    "### {} `{}`\n\n",
                    verdict_label(conflict.verdict),
                    conflict.claim.concept_path
                ));

                out.push_str(&format!(
                    "- **Your code:** {} (`{}:{}`)\n",
                    conflict.claim.description, conflict.claim.file, conflict.claim.line
                ));

                for source in &conflict.conflicts {
                    let citation = source
                        .rfc_citation
                        .as_ref()
                        .map(|c| format!(" [{}]", c))
                        .unwrap_or_default();
                    out.push_str(&format!(
                        "- **{:?}** (Tier {}){}: `{}`\n",
                        source.source_class,
                        source.source_class.tier(),
                        citation,
                        object_value_display(&source.value),
                    ));
                    // Show policy source if this came from a Trust Pack
                    if let Some(policy) = &source.policy_source {
                        let signer = policy.signer_name.as_deref().unwrap_or(&policy.issuer_hex);
                        out.push_str(&format!(
                            "  - *Source: {} v{} ({})*\n",
                            policy.pack_name, policy.pack_version, signer
                        ));
                        if let Some(contact) = &policy.contact {
                            out.push_str(&format!("  - *Contact: {}*\n", contact));
                        }
                    }
                }

                out.push_str(&format!("- **Score:** {:.2}\n", conflict.conflict_score));

                // Show tier breakdown in debug mode
                if let Some(breakdown) = &conflict.tier_breakdown {
                    let parts: Vec<String> = breakdown
                        .iter()
                        .map(|tb| format!("{:?} (Tier {})", tb.source_class, tb.tier))
                        .collect();
                    out.push_str(&format!("- **Authority:** {}\n", parts.join(" > ")));
                }

                if let Some(ack) = &conflict.acknowledged {
                    if ack.expired {
                        // Expired acknowledgment
                        out.push_str(&format!(
                            "- **Previous acknowledgment expired** {}\n",
                            ack.expires_at.as_deref().unwrap_or("(unknown date)")
                        ));
                        out.push_str(&format!(
                            "- **Prior ack** by {} on {}: \"{}\"\n",
                            ack.by, ack.timestamp, ack.reason
                        ));
                        if conflict.verdict == Verdict::Block {
                            out.push_str(
                                "- **Action:** Fix or run `aphoria ack <path> --reason \"...\"`\n",
                            );
                        } else {
                            out.push_str("- **Action:** Review recommended\n");
                        }
                    } else {
                        // Active acknowledgment
                        out.push_str(&format!(
                            "- **Acknowledged** by {} on {}: \"{}\"\n",
                            ack.by, ack.timestamp, ack.reason
                        ));
                        if let Some(exp) = &ack.expires_at {
                            out.push_str(&format!("- **Expires:** {}\n", exp));
                        }
                    }
                } else if conflict.verdict == Verdict::Block {
                    out.push_str(
                        "- **Action:** Fix or run `aphoria ack <path> --reason \"...\"`\n",
                    );
                } else {
                    out.push_str("- **Action:** Review recommended\n");
                }

                out.push('\n');
            }
        }

        // Drift section
        if !result.drifts.is_empty() {
            out.push_str("## Drift Detected\n\n");
            out.push_str("| Concept | Current | Prior | File |\n");
            out.push_str("|---------|---------|-------|------|\n");

            for drift in &result.drifts {
                out.push_str(&format!(
                    "| `{}` | `{}` | `{}` | `{}:{}` |\n",
                    extract_leaf_concept(&drift.claim.concept_path),
                    object_value_display(&drift.claim.value),
                    object_value_display(&drift.prior.value),
                    drift.claim.file,
                    drift.claim.line,
                ));
            }
            out.push('\n');

            out.push_str("**Action:** Review these changes to ensure they were intentional.\n\n");
        }

        // Deprecated patterns section
        if !result.deprecated_usages.is_empty() {
            out.push_str("## Deprecated Patterns\n\n");
            out.push_str("> ⚠️ **Migration Required:** The following patterns are deprecated and should be updated.\n\n");

            out.push_str("| Pattern | File | Severity | Sunset |\n");
            out.push_str("|---------|------|----------|--------|\n");

            for usage in &result.deprecated_usages {
                let sunset = usage
                    .days_until_sunset
                    .map(|d| if d < 0 { format!("OVERDUE ({}d)", -d) } else { format!("{}d", d) })
                    .unwrap_or_else(|| "-".to_string());

                out.push_str(&format!(
                    "| `{}` | `{}:{}` | {} | {} |\n",
                    usage.pattern_name,
                    usage.file_path,
                    usage.line,
                    usage.severity(),
                    sunset,
                ));
            }
            out.push('\n');

            // Details for each deprecated pattern
            out.push_str("### Migration Details\n\n");
            for usage in &result.deprecated_usages {
                out.push_str(&format!("#### `{}`\n\n", usage.pattern_name));
                out.push_str(&format!("- **Reason:** {}\n", usage.reason));
                out.push_str(&format!("- **Location:** `{}:{}`\n", usage.file_path, usage.line));

                if let Some(ref replacement) = usage.superseded_by {
                    out.push_str(&format!("- **Replacement:** `{}`\n", replacement));
                }

                if let Some(ref guide) = usage.migration_guide {
                    out.push_str(&format!("- **Migration Guide:** {}\n", guide));
                }

                out.push('\n');
            }
        }

        // Convergence Suggestions section
        append_convergence_section(&mut out, result);

        // Extracted Observations section
        if let Some(claims) = &result.claims {
            out.push_str("## Extracted Observations\n\n");

            if claims.is_empty() {
                out.push_str("No observations extracted.\n\n");
            } else {
                out.push_str("| Concept | Value | File | Line | Confidence |\n");
                out.push_str("|---------|-------|------|------|------------|\n");

                for claim in claims {
                    out.push_str(&format!(
                        "| `{}` | `{}` | `{}` | {} | {:.0}% |\n",
                        extract_leaf_concept(&claim.concept_path),
                        object_value_display(&claim.value),
                        claim.file,
                        claim.line,
                        claim.confidence * 100.0,
                    ));
                }
                out.push('\n');
            }
        }

        out
    }
}

/// Append a `## Convergence Suggestions` section to `out`.
///
/// No-ops when `result.convergence_suggestions` is empty, so callers do not
/// need to guard the call.
fn append_convergence_section(out: &mut String, result: &ScanResult) {
    if result.convergence_suggestions.is_empty() {
        return;
    }

    out.push_str("## Convergence Suggestions\n\n");
    out.push_str("| Severity | Pattern | Local | Org | Tier | File |\n");
    out.push_str("|----------|---------|-------|-----|------|------|\n");

    for suggestion in &result.convergence_suggestions {
        let severity_label = suggestion.severity.display_name();
        // Combine concept_path and predicate into a single pattern column
        let pattern = format!("{}.{}", suggestion.concept_path, suggestion.predicate);

        out.push_str(&format!(
            "| {} | `{}` | `{}` | `{}` | {} | `{}:{}` |\n",
            severity_label,
            pattern,
            suggestion.local_value,
            suggestion.org_value,
            suggestion.org_tier_name,
            suggestion.file,
            suggestion.line,
        ));
    }
    out.push('\n');

    // Detail blocks for Authoritative suggestions (highest priority)
    let authoritative: Vec<_> = result
        .convergence_suggestions
        .iter()
        .filter(|s| s.severity == ConvergenceSeverity::Authoritative)
        .collect();

    if !authoritative.is_empty() {
        out.push_str("### Authoritative Suggestions\n\n");
        for suggestion in authoritative {
            out.push_str(&format!(
                "#### `{}.{}`\n\n",
                suggestion.concept_path, suggestion.predicate,
            ));
            out.push_str(&format!(
                "- **Local:** `{}` | **Org:** `{}` ({} tier)\n",
                suggestion.local_value, suggestion.org_value, suggestion.org_tier_name,
            ));
            out.push_str(&format!("- **Location:** `{}:{}`\n", suggestion.file, suggestion.line,));
            let claim_word = if suggestion.matching_claims_count == 1 { "claim" } else { "claims" };
            out.push_str(&format!(
                "- **Org coverage:** {} matching {}\n",
                suggestion.matching_claims_count, claim_word,
            ));

            if let Some(ref dc) = suggestion.driving_claim {
                out.push_str(&format!("- **Invariant:** {}\n", dc.invariant));
                out.push_str(&format!("- **Consequence:** {}\n", dc.consequence));
                if !dc.evidence.is_empty() {
                    out.push_str(&format!("- **Evidence:** {}\n", dc.evidence.join(", ")));
                }
            }

            out.push('\n');
        }
    }
}

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

    #[test]
    fn test_markdown_with_conflicts() {
        let formatter = MarkdownReport;
        let result = ScanResult {
            project: "testproject".to_string(),
            scan_id: "scan-md".to_string(),
            files_scanned: 20,
            claims_extracted: 4,
            conflicts: vec![ConflictResult {
                claim: Observation {
                    concept_path: "code://rust/test/cors/allow_origin".to_string(),
                    predicate: "config_value".to_string(),
                    value: ObjectValue::Text("*".to_string()),
                    file: "src/server.rs".to_string(),
                    line: 55,
                    matched_text: "allow_origin(\"*\")".to_string(),
                    confidence: 1.0,
                    description: "CORS wildcard allow-origin".to_string(),
                },
                conflicts: vec![ConflictingSource {
                    path: "owasp://cors/allow_origin".to_string(),
                    source_class: SourceClass::Clinical,
                    value: ObjectValue::Text("explicit_list".to_string()),
                    confidence: 1.0,
                    rfc_citation: Some("OWASP A05:2021".to_string()),
                    policy_source: None,
                }],
                conflict_score: 0.77,
                verdict: Verdict::Block,
                acknowledged: None,
                trace: None,
                tier_breakdown: None,
                tier_verdict: None,
                primary_tier: None,
            }],
            drifts: vec![],
            format: "markdown".to_string(),
            debug: false,
            strict: false,
            observations_recorded: 0,
            timing: None,
            claims: None,
            observations: vec![],
            deprecated_usages: vec![],
            verify: None,
            convergence_suggestions: vec![],
        };

        let output = formatter.format(&result);

        assert!(output.contains("# Aphoria Scan: testproject"));
        assert!(output.contains("| BLOCK |"));
        assert!(output.contains("## Details"));
        assert!(output.contains("CORS wildcard"));
        assert!(output.contains("`aphoria ack"));
    }

    #[test]
    fn test_markdown_empty() {
        let formatter = MarkdownReport;
        let result = ScanResult::stub(&std::path::PathBuf::from("empty"), "markdown");
        let output = formatter.format(&result);

        assert!(output.contains("No claims found"));
    }
}
