use std::collections::BTreeSet;
use std::path::Path;

use serde_json::{Value, json};

use super::args::Args;
use super::io::{format_time, path_string};
use super::model::{
    ChurnRecord, ChurnSummaryRecord, IntraRpChurn, SandwichAnalysis, SandwichGroupStats,
    SandwichHeatmapRow, SandwichRecord, SequenceMeta,
};

pub(super) fn build_output_from_meta(
    args: &Args,
    left: &[SequenceMeta],
    right: &[SequenceMeta],
    sandwich: &SandwichAnalysis,
    churn: &IntraRpChurn,
) -> Value {
    json!({
        "schemaVersion": 3,
        "generatedBy": "sequence_triage_ccr_cir",
        "inputContract": "sequence-jsonl-cir-ccr-sandwich-only",
        "parameters": {
            "leftSequence": path_string(&args.left_sequence),
            "rightSequence": path_string(&args.right_sequence),
            "alignWindowRuns": args.align_window_runs,
            "alignWindowSecs": args.align_window_secs,
            "sampleLimit": args.sample_limit,
            "warmupSamples": args.warmup_samples,
            "cooldownSamples": args.cooldown_samples,
            "timelineSampleLimit": if args.timeline_sample_limit == 0 { args.sample_limit } else { args.timeline_sample_limit },
            "executionMode": "streaming-window",
            "sequenceSemantics": {
                "timeSource": "cir.validation_time",
                "sourceSampleOrder": "each side is sorted by CIR validation_time, with seq/runId used only as stable tie breakers",
                "sourceWindow": "only adjacent samples from the same side after CIR-time sorting form a source interval",
                "peerWindow": "all peer samples with source_start.time < peer.time < source_end.time are checked independently",
            },
        },
        "left": sequence_meta_summary(left),
        "right": sequence_meta_summary(right),
        "sandwich": {
            "strictTimeWindow": true,
            "method": "Streaming mode: first build sample timing metadata, then process each sandwich triple by loading only source_start, peer, and source_end CIR/CCR artifacts.",
            "totals": {
                "occurrences": sandwich.total_occurrences,
                "uniqueKeys": sandwich.unique_keys.len(),
            },
            "bySetType": sandwich.by_set_type,
            "classificationCounts": sandwich_classifications_to_json(sandwich),
            "heatmap": sandwich_heatmap_to_json(sandwich),
            "uriPrefixGroups": sandwich_uri_prefix_groups_to_json(sandwich),
            "asnGroups": sandwich_asn_groups_to_json(sandwich),
            "interpretation": {
                "missingStableObject": "The source side proves a URI/hash is stable across an interval that contains the peer sample, but the peer sample has no such URI.",
                "hashMismatchStableObject": "The source side proves a URI/hash is stable across an interval that contains the peer sample, but the peer sample has the same URI with a different hash.",
                "missingStableReject": "The source side consistently rejects the URI across an interval that contains the peer sample, but the peer sample does not reject it.",
                "missingStableOutput": "The source side consistently outputs a VRP/VAP key across an interval that contains the peer sample, but the peer sample does not output it."
            }
        },
        "intraRpChurn": {
            "method": "Streaming mode: compare adjacent samples by loading only the two CIR or CCR artifacts needed for the current adjacent pair.",
            "left": churn_records_to_json(&churn.left),
            "right": churn_records_to_json(&churn.right),
            "summary": churn_summary_to_json(&churn.summary),
        },
        "interpretation": {
            "sandwichOnlyMeaning": "This schema intentionally exposes only sandwich anomalies for cross-RP triage. Older raw persistent, adjusted, edge, and rollover classifications are not emitted.",
            "intraRpChurnMeaning": "Adjacent-run churn measures how much each RP's own observed object/output/reject sets changed during the sequence. It is a baseline for judging whether cross-RP anomalies exceed natural sequence movement.",
            "limits": [
                "Sequence triage only reads sequence JSONL plus referenced CCR/CIR files.",
                "Sample time is always read from CIR validation_time; sequence JSONL validationTime is accepted only as optional metadata.",
                "It does not read report.json, logs, repo-bytes DB, cache, mirror, or raw objects for root cause proof.",
                "Sandwich anomalies indicate stable-interval contradictions, not final root cause. Manual evidence is still required for network, repository, or implementation attribution."
            ]
        }
    })
}

fn sequence_meta_summary(samples: &[SequenceMeta]) -> Value {
    json!({
        "sampleCount": samples.len(),
        "rpIds": samples.iter().map(|sample| sample.raw.rp_id.clone()).collect::<BTreeSet<_>>(),
        "firstSeq": samples.first().map(|sample| sample.raw.seq),
        "lastSeq": samples.last().map(|sample| sample.raw.seq),
        "firstValidationTime": samples.first().map(|sample| format_time(sample.validation_time)),
        "lastValidationTime": samples.last().map(|sample| format_time(sample.validation_time)),
        "samples": samples.iter().map(|sample| json!({
            "seq": sample.raw.seq,
            "runId": sample.raw.run_id,
            "syncMode": sample.raw.sync_mode,
            "startTime": sample.raw.start_time,
            "finishTime": sample.raw.finish_time,
            "validationTime": format_time(sample.validation_time),
            "status": sample.raw.status,
            "ccrPath": path_string(&sample.ccr_path),
            "cirPath": path_string(&sample.cir_path),
            "ccrSha256": sample.raw.ccr_sha256,
            "cirSha256": sample.raw.cir_sha256,
            "wallMs": sample.raw.wall_ms,
            "maxRssKb": sample.raw.max_rss_kb,
            "vrps": sample.raw.vrps,
            "vaps": sample.raw.vaps,
            "publicationPoints": sample.raw.publication_points,
            "objectCount": sample.cir_object_count,
            "objectHashCount": sample.cir_object_count,
            "rejectCount": sample.cir_reject_count,
            "trustAnchorCount": sample.cir_trust_anchor_count,
        })).collect::<Vec<_>>(),
    })
}

fn sandwich_classifications_to_json(sandwich: &SandwichAnalysis) -> Vec<Value> {
    sandwich
        .stats
        .iter()
        .map(|(class, stats)| {
            json!({
                "classification": class,
                "occurrences": stats.total,
                "uniqueKeys": stats.unique_keys.len(),
                "samples": stats.samples.iter().map(sandwich_sample_to_json).collect::<Vec<_>>(),
            })
        })
        .collect()
}

fn sandwich_heatmap_to_json(sandwich: &SandwichAnalysis) -> Vec<Value> {
    sandwich
        .heatmap
        .values()
        .map(sandwich_heatmap_row_to_json)
        .collect()
}

fn sandwich_uri_prefix_groups_to_json(sandwich: &SandwichAnalysis) -> Vec<Value> {
    let mut groups = sandwich
        .uri_prefix_groups
        .values()
        .collect::<Vec<&SandwichGroupStats>>();
    groups.sort_by(|left, right| {
        right
            .total
            .cmp(&left.total)
            .then_with(|| left.set_type.cmp(right.set_type))
            .then_with(|| left.group_value.cmp(&right.group_value))
    });
    groups
        .into_iter()
        .map(sandwich_uri_prefix_group_to_json)
        .collect()
}

fn sandwich_asn_groups_to_json(sandwich: &SandwichAnalysis) -> Vec<Value> {
    let mut groups = sandwich
        .asn_groups
        .values()
        .collect::<Vec<&SandwichGroupStats>>();
    groups.sort_by(|left, right| {
        right
            .total
            .cmp(&left.total)
            .then_with(|| left.set_type.cmp(right.set_type))
            .then_with(|| left.group_value.cmp(&right.group_value))
    });
    groups.into_iter().map(sandwich_asn_group_to_json).collect()
}

fn sandwich_uri_prefix_group_to_json(group: &SandwichGroupStats) -> Value {
    json!({
        "uriPrefix": group.group_value,
        "setType": group.set_type,
        "occurrences": group.total,
        "uniqueKeys": group.unique_keys.len(),
        "classificationCounts": group.class_counts,
        "sampleKeys": group.sample_keys,
    })
}

fn sandwich_asn_group_to_json(group: &SandwichGroupStats) -> Value {
    json!({
        "asn": group.group_value,
        "setType": group.set_type,
        "occurrences": group.total,
        "uniqueKeys": group.unique_keys.len(),
        "classificationCounts": group.class_counts,
        "sampleKeys": group.sample_keys,
    })
}

fn sandwich_heatmap_row_to_json(row: &SandwichHeatmapRow) -> Value {
    json!({
        "window": row.window,
        "sourceSide": row.source_side.as_str(),
        "sourceStartSeq": row.source_start_seq,
        "sourceStartRunId": row.source_start_run_id,
        "peerSeq": row.peer_seq,
        "peerRunId": row.peer_run_id,
        "sourceEndSeq": row.source_end_seq,
        "sourceEndRunId": row.source_end_run_id,
        "sourceStartTime": row.source_start_time,
        "peerTime": row.peer_time,
        "sourceEndTime": row.source_end_time,
        "total": row.total,
        "counts": row.class_counts,
    })
}

fn sandwich_sample_to_json(sample: &SandwichRecord) -> Value {
    json!({
        "classification": sample.classification,
        "setType": sample.set_type,
        "key": sample.key,
        "sourceSide": sample.source_side.as_str(),
        "sourceStartSeq": sample.source_start_seq,
        "sourceStartRunId": sample.source_start_run_id,
        "sourceEndSeq": sample.source_end_seq,
        "sourceEndRunId": sample.source_end_run_id,
        "peerSeq": sample.peer_seq,
        "peerRunId": sample.peer_run_id,
        "sourceValue": sample.source_value,
        "peerValue": sample.peer_value,
        "sourceStartTime": sample.source_start_time,
        "peerTime": sample.peer_time,
        "sourceEndTime": sample.source_end_time,
        "note": sample.note,
    })
}

fn churn_records_to_json(records: &[ChurnRecord]) -> Vec<Value> {
    records
        .iter()
        .map(|record| {
            json!({
                "side": record.side.as_str(),
                "rpId": record.rp_id,
                "fromSeq": record.from_seq,
                "toSeq": record.to_seq,
                "fromRunId": record.from_run_id,
                "toRunId": record.to_run_id,
                "setType": record.set_type,
                "fromCount": record.from_count,
                "toCount": record.to_count,
                "addedCount": record.added_count,
                "removedCount": record.removed_count,
                "changedAbs": record.changed_abs,
                "unionCount": record.union_count,
                "changedRatioFrom": record.changed_ratio_from,
                "changedRatioUnion": record.changed_ratio_union,
            })
        })
        .collect()
}

fn churn_summary_to_json(summary: &[ChurnSummaryRecord]) -> Vec<Value> {
    summary
        .iter()
        .map(|record| {
            json!({
                "side": record.side.as_str(),
                "setType": record.set_type,
                "maxChangedAbs": record.max_changed_abs,
                "avgChangedAbs": record.avg_changed_abs,
                "maxChangedRatioFrom": record.max_changed_ratio_from,
                "avgChangedRatioFrom": record.avg_changed_ratio_from,
                "maxChangedRatioUnion": record.max_changed_ratio_union,
                "avgChangedRatioUnion": record.avg_changed_ratio_union,
            })
        })
        .collect()
}

pub(super) fn write_json(path: &Path, value: &Value) -> Result<(), String> {
    std::fs::write(
        path,
        serde_json::to_string_pretty(value).map_err(|e| e.to_string())? + "\n",
    )
    .map_err(|e| format!("write JSON failed: {}: {e}", path.display()))
}

pub(super) fn write_markdown(path: &Path, output: &Value) -> Result<(), String> {
    let mut lines = vec![
        "# CCR/CIR Sequence Triage Summary".to_string(),
        "".to_string(),
        format!("- `schemaVersion`: `{}`", output["schemaVersion"].as_u64().unwrap_or(0)),
        format!("- `generatedBy`: `{}`", output["generatedBy"].as_str().unwrap_or("")),
        format!("- `inputContract`: `{}`", output["inputContract"].as_str().unwrap_or("")),
        format!("- `leftSamples`: `{}`", output["left"]["sampleCount"].as_u64().unwrap_or(0)),
        format!("- `rightSamples`: `{}`", output["right"]["sampleCount"].as_u64().unwrap_or(0)),
        format!("- `alignWindowSecs`: `{}`", output["parameters"]["alignWindowSecs"].as_i64().unwrap_or(0)),
        "".to_string(),
        "## Sandwich Anomaly Check".to_string(),
        "".to_string(),
        format!("- `occurrences`: `{}`", output["sandwich"]["totals"]["occurrences"].as_u64().unwrap_or(0)),
        format!("- `uniqueKeys`: `{}`", output["sandwich"]["totals"]["uniqueKeys"].as_u64().unwrap_or(0)),
        "- Rule: source side has two adjacent stable samples, and peer sample timestamp is strictly between them.".to_string(),
        "".to_string(),
        "| Sandwich Classification | Occurrences | Unique Keys |".to_string(),
        "|---|---:|---:|".to_string(),
    ];
    if let Some(classes) = output["sandwich"]["classificationCounts"].as_array() {
        for item in classes {
            lines.push(format!(
                "| `{}` | {} | {} |",
                item["classification"].as_str().unwrap_or(""),
                item["occurrences"].as_u64().unwrap_or(0),
                item["uniqueKeys"].as_u64().unwrap_or(0)
            ));
        }
    }
    lines.extend([
        "".to_string(),
        "## Sandwich Heatmap".to_string(),
        "".to_string(),
        "| Window | Missing Object | Hash Mismatch Object | Missing Output | Missing Reject | Total |".to_string(),
        "|---|---:|---:|---:|---:|---:|".to_string(),
    ]);
    if let Some(rows) = output["sandwich"]["heatmap"].as_array() {
        for row in rows {
            let counts = &row["counts"];
            lines.push(format!(
                "| `{}` | {} | {} | {} | {} | {} |",
                row["window"].as_str().unwrap_or(""),
                counts["PEER_MISSING_STABLE_OBJECT"].as_u64().unwrap_or(0),
                counts["PEER_HASH_MISMATCH_STABLE_OBJECT"]
                    .as_u64()
                    .unwrap_or(0),
                counts["PEER_MISSING_STABLE_OUTPUT"].as_u64().unwrap_or(0),
                counts["PEER_MISSING_STABLE_REJECT"].as_u64().unwrap_or(0),
                row["total"].as_u64().unwrap_or(0)
            ));
        }
    }
    lines.extend([
        "".to_string(),
        "## Object / Reject URI Prefix Groups".to_string(),
        "".to_string(),
        "| Set Type | URI Prefix | Occurrences | Unique Keys | Classes | Sample Keys |".to_string(),
        "|---|---|---:|---:|---|---|".to_string(),
    ]);
    if let Some(groups) = output["sandwich"]["uriPrefixGroups"].as_array() {
        for group in groups {
            lines.push(format!(
                "| `{}` | `{}` | {} | {} | {} | {} |",
                md_escape(group["setType"].as_str().unwrap_or("")),
                md_escape(group["uriPrefix"].as_str().unwrap_or("")),
                group["occurrences"].as_u64().unwrap_or(0),
                group["uniqueKeys"].as_u64().unwrap_or(0),
                md_escape(&compact_counts(&group["classificationCounts"])),
                md_escape(&compact_sample_keys(group))
            ));
        }
    }
    lines.extend([
        "".to_string(),
        "## Output ASN Groups".to_string(),
        "".to_string(),
        "| Set Type | ASN | Occurrences | Unique Keys | Classes | Sample Keys |".to_string(),
        "|---|---|---:|---:|---|---|".to_string(),
    ]);
    if let Some(groups) = output["sandwich"]["asnGroups"].as_array() {
        for group in groups {
            lines.push(format!(
                "| `{}` | `{}` | {} | {} | {} | {} |",
                md_escape(group["setType"].as_str().unwrap_or("")),
                md_escape(group["asn"].as_str().unwrap_or("")),
                group["occurrences"].as_u64().unwrap_or(0),
                group["uniqueKeys"].as_u64().unwrap_or(0),
                md_escape(&compact_counts(&group["classificationCounts"])),
                md_escape(&compact_sample_keys(group))
            ));
        }
    }
    lines.extend([
        "".to_string(),
        "## Intra-RP Adjacent Churn Summary".to_string(),
        "".to_string(),
        "| Side | Set Type | Max Changed | Avg Changed | Max Ratio From | Max Ratio Union |"
            .to_string(),
        "|---|---|---:|---:|---:|---:|".to_string(),
    ]);
    if let Some(summary) = output["intraRpChurn"]["summary"].as_array() {
        for item in summary {
            lines.push(format!(
                "| `{}` | `{}` | {} | {:.2} | {:.6} | {:.6} |",
                item["side"].as_str().unwrap_or(""),
                item["setType"].as_str().unwrap_or(""),
                item["maxChangedAbs"].as_u64().unwrap_or(0),
                item["avgChangedAbs"].as_f64().unwrap_or(0.0),
                item["maxChangedRatioFrom"].as_f64().unwrap_or(0.0),
                item["maxChangedRatioUnion"].as_f64().unwrap_or(0.0)
            ));
        }
    }
    lines.extend([
        "".to_string(),
        "## Intra-RP Adjacent Churn Details".to_string(),
        "".to_string(),
        "| Side | From -> To | Set Type | From | To | Added | Removed | Changed | Ratio From | Ratio Union |".to_string(),
        "|---|---|---|---:|---:|---:|---:|---:|---:|---:|".to_string(),
    ]);
    for side_key in ["left", "right"] {
        if let Some(records) = output["intraRpChurn"][side_key].as_array() {
            for item in records {
                lines.push(format!(
                    "| `{}` | `{}` -> `{}` | `{}` | {} | {} | {} | {} | {} | {:.6} | {:.6} |",
                    item["side"].as_str().unwrap_or(""),
                    item["fromRunId"].as_str().unwrap_or(""),
                    item["toRunId"].as_str().unwrap_or(""),
                    item["setType"].as_str().unwrap_or(""),
                    item["fromCount"].as_u64().unwrap_or(0),
                    item["toCount"].as_u64().unwrap_or(0),
                    item["addedCount"].as_u64().unwrap_or(0),
                    item["removedCount"].as_u64().unwrap_or(0),
                    item["changedAbs"].as_u64().unwrap_or(0),
                    item["changedRatioFrom"].as_f64().unwrap_or(0.0),
                    item["changedRatioUnion"].as_f64().unwrap_or(0.0)
                ));
            }
        }
    }
    lines.extend([
        "".to_string(),
        "## Interpretation".to_string(),
        "".to_string(),
        "- `PEER_MISSING_STABLE_OBJECT` / `PEER_HASH_MISMATCH_STABLE_OBJECT` 表示输入对象在一个 RP 的稳定夹层内，与另一个 RP 的中间采样点不一致。".to_string(),
        "- `PEER_MISSING_STABLE_OUTPUT` 表示输出 VRP/VAP 在稳定夹层内缺失，是更接近最终产物差异的信号。".to_string(),
        "- `PEER_MISSING_STABLE_REJECT` 表示拒绝列表在稳定夹层内缺失，用于发现验证失败捕获差异。".to_string(),
        "- `intraRpChurn` 是同一个 RP 自身相邻 run 的自然变化基线，不代表跨 RP 异常。".to_string(),
    ]);
    std::fs::write(path, lines.join("\n") + "\n")
        .map_err(|e| format!("write markdown failed: {}: {e}", path.display()))
}

fn compact_counts(value: &Value) -> String {
    value
        .as_object()
        .map(|counts| {
            let mut items = counts
                .iter()
                .map(|(key, count)| format!("{}={}", key, count.as_u64().unwrap_or(0)))
                .collect::<Vec<_>>();
            items.sort();
            items.join(", ")
        })
        .unwrap_or_default()
}

fn compact_sample_keys(group: &Value) -> String {
    group["sampleKeys"]
        .as_array()
        .map(|items| {
            items
                .iter()
                .take(5)
                .filter_map(|item| item.as_str())
                .collect::<Vec<_>>()
                .join("; ")
        })
        .unwrap_or_default()
}

fn md_escape(value: &str) -> String {
    value.replace('|', "\\|").replace('\n', " ")
}
