//! Rendering of check results: the verbose summary, --show output, and --json.

use std::io::{self, Write};

use serde::Serialize;

use crate::checker::{CheckReport, Issue, MissingRange};
use crate::cli::ShowKind;

pub fn format_missing_range(r: &MissingRange) -> String {
    if r.first == r.last {
        r.first.to_string()
    } else {
        format!("{}..{}", r.first, r.last)
    }
}

pub fn format_issue(issue: &Issue) -> String {
    match issue {
        Issue::Duplicate { row, value } => format!("row {row}: duplicate value {value}"),
        Issue::Gap {
            row,
            expected,
            actual,
            missing,
        } => format!(
            "row {row}: gap: expected {expected}, got {actual} (missing {})",
            format_missing_range(missing)
        ),
        Issue::OrderViolation {
            row,
            expected,
            actual,
        } => format!("row {row}: order violation: expected {expected}, got {actual}"),
    }
}

fn opt_value(v: Option<i128>) -> String {
    v.map_or_else(|| "-".to_string(), |v| v.to_string())
}

pub fn write_verbose(report: &CheckReport, w: &mut impl Write) -> io::Result<()> {
    writeln!(w, "rows: {}", report.rows)?;
    writeln!(w, "start: {}", opt_value(report.start))?;
    writeln!(w, "end: {}", opt_value(report.end))?;
    let missing: Vec<String> = report
        .missing_ranges()
        .iter()
        .map(format_missing_range)
        .collect();
    writeln!(w, "missing: {}", join_or_none(&missing))?;
    let dups: Vec<String> = report
        .duplicate_values()
        .iter()
        .map(i128::to_string)
        .collect();
    writeln!(w, "duplicates: {}", join_or_none(&dups))?;
    for issue in &report.issues {
        writeln!(w, "issue: {}", format_issue(issue))?;
    }
    writeln!(
        w,
        "result: {}",
        if report.is_ok() {
            "OK".to_string()
        } else {
            format!("NG ({} issue(s))", report.issues.len())
        }
    )
}

fn join_or_none(items: &[String]) -> String {
    if items.is_empty() {
        "(none)".to_string()
    } else {
        items.join(", ")
    }
}

/// Schema of the --json output. The default is a bounded, counts-only summary;
/// the missing / duplicates / issues lists are included only when `detailed`
/// (--verbose combined with --json) is set.
#[derive(Serialize)]
struct JsonReport<'a> {
    ok: bool,
    rows: u64,
    start: Option<i128>,
    end: Option<i128>,
    issue_count: IssueCount,
    missing_count: u128,
    #[serde(skip_serializing_if = "Option::is_none")]
    missing: Option<Vec<MissingRange>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    duplicates: Option<Vec<i128>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    issues: Option<&'a [Issue]>,
}

#[derive(Serialize)]
struct IssueCount {
    total: usize,
    gap: usize,
    duplicate: usize,
    order_violation: usize,
}

impl IssueCount {
    fn tally(issues: &[Issue]) -> Self {
        let count = |pred: fn(&Issue) -> bool| issues.iter().filter(|i| pred(i)).count();
        Self {
            total: issues.len(),
            gap: count(|i| matches!(i, Issue::Gap { .. })),
            duplicate: count(|i| matches!(i, Issue::Duplicate { .. })),
            order_violation: count(|i| matches!(i, Issue::OrderViolation { .. })),
        }
    }
}

pub fn write_json(report: &CheckReport, detailed: bool, w: &mut impl Write) -> io::Result<()> {
    let json = JsonReport {
        ok: report.is_ok(),
        rows: report.rows,
        start: report.start,
        end: report.end,
        issue_count: IssueCount::tally(&report.issues),
        missing_count: report.missing_total(),
        missing: detailed.then(|| report.missing_ranges()),
        duplicates: detailed.then(|| report.duplicate_values()),
        issues: detailed.then_some(&report.issues),
    };
    serde_json::to_writer(&mut *w, &json)?;
    writeln!(w)
}

/// Prints only the information selected via --show: bare values when a single
/// kind is requested, "kind: value" lines when multiple kinds are requested.
pub fn write_show(report: &CheckReport, kinds: &[ShowKind], w: &mut impl Write) -> io::Result<()> {
    let labeled = kinds.len() > 1;
    for kind in kinds {
        let prefix = |name: &str| {
            if labeled {
                format!("{name}: ")
            } else {
                String::new()
            }
        };
        match kind {
            ShowKind::Start => writeln!(w, "{}{}", prefix("start"), opt_value(report.start))?,
            ShowKind::End => writeln!(w, "{}{}", prefix("end"), opt_value(report.end))?,
            ShowKind::Missing => {
                for r in report.missing_ranges() {
                    writeln!(w, "{}{}", prefix("missing"), format_missing_range(&r))?;
                }
            }
            ShowKind::Duplicates => {
                for v in report.duplicate_values() {
                    writeln!(w, "{}{}", prefix("duplicates"), v)?;
                }
            }
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn missing_range_formats_single_and_span() {
        assert_eq!(
            format_missing_range(&MissingRange {
                first: 3,
                last: 3,
                count: 1,
            }),
            "3"
        );
        assert_eq!(
            format_missing_range(&MissingRange {
                first: 3,
                last: 7,
                count: 5,
            }),
            "3..7"
        );
    }

    #[test]
    fn verbose_reports_ok_summary() {
        let report = CheckReport {
            rows: 3,
            start: Some(0),
            end: Some(2),
            issues: vec![],
        };
        let mut buf = Vec::new();
        write_verbose(&report, &mut buf).unwrap();
        let text = String::from_utf8(buf).unwrap();
        assert!(text.contains("rows: 3"));
        assert!(text.contains("start: 0"));
        assert!(text.contains("end: 2"));
        assert!(text.contains("result: OK"));
    }

    fn report_with_one_gap() -> CheckReport {
        CheckReport {
            rows: 3,
            start: Some(0),
            end: Some(4),
            issues: vec![Issue::Gap {
                row: 3,
                expected: 2,
                actual: 4,
                missing: MissingRange {
                    first: 2,
                    last: 3,
                    count: 2,
                },
            }],
        }
    }

    #[test]
    fn json_summary_has_counts_but_no_lists() {
        let mut buf = Vec::new();
        write_json(&report_with_one_gap(), false, &mut buf).unwrap();
        let v: serde_json::Value = serde_json::from_slice(&buf).unwrap();
        assert_eq!(
            v,
            serde_json::json!({
                "ok": false,
                "rows": 3,
                "start": 0,
                "end": 4,
                "issue_count": {"total": 1, "gap": 1, "duplicate": 0, "order_violation": 0},
                "missing_count": 2,
            })
        );
    }

    #[test]
    fn json_detailed_includes_lists() {
        let mut buf = Vec::new();
        write_json(&report_with_one_gap(), true, &mut buf).unwrap();
        let v: serde_json::Value = serde_json::from_slice(&buf).unwrap();
        assert_eq!(
            v["missing"],
            serde_json::json!([{"first": 2, "last": 3, "count": 2}])
        );
        assert_eq!(v["duplicates"], serde_json::json!([]));
        assert_eq!(
            v["issues"],
            serde_json::json!([{
                "type": "gap",
                "row": 3,
                "expected": 2,
                "actual": 4,
                "missing": {"first": 2, "last": 3, "count": 2},
            }])
        );
    }

    #[test]
    fn json_uses_null_for_absent_start_end() {
        let report = CheckReport::default();
        let mut buf = Vec::new();
        write_json(&report, false, &mut buf).unwrap();
        let v: serde_json::Value = serde_json::from_slice(&buf).unwrap();
        assert_eq!(v["ok"], true);
        assert_eq!(v["start"], serde_json::Value::Null);
        assert_eq!(v["end"], serde_json::Value::Null);
    }

    #[test]
    fn show_single_kind_prints_bare_values() {
        let report = CheckReport {
            rows: 2,
            start: Some(10),
            end: Some(11),
            issues: vec![],
        };
        let mut buf = Vec::new();
        write_show(&report, &[ShowKind::Start], &mut buf).unwrap();
        assert_eq!(String::from_utf8(buf).unwrap(), "10\n");
    }

    #[test]
    fn show_multiple_kinds_labels_lines() {
        let report = CheckReport {
            rows: 2,
            start: Some(10),
            end: Some(11),
            issues: vec![],
        };
        let mut buf = Vec::new();
        write_show(&report, &[ShowKind::Start, ShowKind::End], &mut buf).unwrap();
        assert_eq!(String::from_utf8(buf).unwrap(), "start: 10\nend: 11\n");
    }
}
