use std::fmt::Write;
use std::net::{Ipv4Addr, Ipv6Addr};

use serde_json::{Value, json};

use rpki::data_model::resources::ip_resources::{IPAddress, IPAddressPrefix};
use rpki::rtr::cache::SerialResult;
use rpki::rtr::payload::{Payload, RouteOrigin};
use rpki::rtr::pdu::{CacheResponse, EndOfDataV1, IPv4Prefix, IPv6Prefix};

pub struct RtrDebugDumper {
    entries: Vec<Value>,
}

impl RtrDebugDumper {
    pub fn new() -> Self {
        Self {
            entries: Vec::new(),
        }
    }

    pub fn push<T: serde::Serialize>(&mut self, pdu: u8, body: &T) {
        self.entries.push(json!({
            "pdu": pdu,
            "pdu_name": pdu_type_name(pdu),
            "body": body
        }));
    }

    pub fn push_value(&mut self, pdu: u8, body: Value) {
        self.entries.push(json!({
            "pdu": pdu,
            "pdu_name": pdu_type_name(pdu),
            "body": body
        }));
    }

    pub fn print_pretty(&self, test_name: &str) {
        println!(
            "\n===== RTR Debug Dump: {} =====\n{}\n",
            test_name,
            serde_json::to_string_pretty(&self.entries).unwrap()
        );
    }
}

pub fn pdu_type_name(pdu: u8) -> &'static str {
    match pdu {
        0 => "Serial Notify",
        1 => "Serial Query",
        2 => "Reset Query",
        3 => "Cache Response",
        4 => "IPv4 Prefix",
        6 => "IPv6 Prefix",
        7 => "End of Data",
        8 => "Cache Reset",
        9 => "Router Key",
        10 => "Error Report",
        11 => "ASPA",
        255 => "Reserved",
        _ => "Unknown",
    }
}

pub fn dump_cache_response(resp: &CacheResponse) -> Value {
    json!({
        "header": {
            "version": resp.version(),
            "pdu": resp.pdu(),
            "session_id": resp.session_id(),
            "length": 8
        }
    })
}

pub fn dump_ipv4_prefix(p: &IPv4Prefix) -> Value {
    json!({
        "header": {
            "version": p.version(),
            "pdu": p.pdu(),
            "session_id": 0,
            "length": 20
        },
        "flags": {
            "raw": if p.flag().is_announce() { 1 } else { 0 },
            "announce": p.flag().is_announce()
        },
        "prefix": p.prefix().to_string(),
        "prefix_len": p.prefix_len(),
        "max_len": p.max_len(),
        "asn": p.asn().into_u32()
    })
}

pub fn dump_ipv6_prefix(p: &IPv6Prefix) -> Value {
    json!({
        "header": {
            "version": p.version(),
            "pdu": p.pdu(),
            "session_id": 0,
            "length": 32
        },
        "flags": {
            "raw": if p.flag().is_announce() { 1 } else { 0 },
            "announce": p.flag().is_announce()
        },
        "prefix": p.prefix().to_string(),
        "prefix_len": p.prefix_len(),
        "max_len": p.max_len(),
        "asn": p.asn().into_u32()
    })
}

pub fn dump_eod_v1(eod: &EndOfDataV1) -> Value {
    let timing = eod.timing();
    json!({
        "header": {
            "version": eod.version(),
            "pdu": eod.pdu(),
            "session_id": eod.session_id(),
            "length": 24
        },
        "serial_number": eod.serial_number(),
        "refresh_interval": timing.refresh,
        "retry_interval": timing.retry,
        "expire_interval": timing.expire
    })
}

pub fn dump_cache_reset(version: u8, pdu: u8) -> Value {
    json!({
        "header": {
            "version": version,
            "pdu": pdu,
            "session_id": 0,
            "length": 8
        }
    })
}

pub fn v4_prefix(a: u8, b: u8, c: u8, d: u8, prefix_len: u8) -> IPAddressPrefix {
    IPAddressPrefix {
        address: IPAddress::from_ipv4(Ipv4Addr::new(a, b, c, d)),
        prefix_length: prefix_len,
    }
}

pub fn v6_prefix(addr: Ipv6Addr, prefix_len: u8) -> IPAddressPrefix {
    IPAddressPrefix {
        address: IPAddress::from_ipv6(addr),
        prefix_length: prefix_len,
    }
}

pub fn v4_origin(a: u8, b: u8, c: u8, d: u8, prefix_len: u8, max_len: u8, asn: u32) -> RouteOrigin {
    let prefix = v4_prefix(a, b, c, d, prefix_len);
    RouteOrigin::new(prefix, max_len, asn.into())
}

pub fn v6_origin(addr: Ipv6Addr, prefix_len: u8, max_len: u8, asn: u32) -> RouteOrigin {
    let prefix = v6_prefix(addr, prefix_len);
    RouteOrigin::new(prefix, max_len, asn.into())
}

pub fn as_route_origin(payload: &Payload) -> &RouteOrigin {
    match payload {
        Payload::RouteOrigin(ro) => ro,
        _ => panic!("expected RouteOrigin payload"),
    }
}

pub fn as_v4_route_origin(payload: &Payload) -> &RouteOrigin {
    let ro = as_route_origin(payload);
    assert!(ro.prefix().address.is_ipv4(), "expected IPv4 RouteOrigin");
    ro
}

pub fn as_v6_route_origin(payload: &Payload) -> &RouteOrigin {
    let ro = as_route_origin(payload);
    assert!(ro.prefix().address.is_ipv6(), "expected IPv6 RouteOrigin");
    ro
}

pub fn route_origin_to_string(ro: &RouteOrigin) -> String {
    let prefix = ro.prefix();
    let addr = match prefix.address {
        IPAddress::V4(v4) => v4.to_string(),
        IPAddress::V6(v6) => v6.to_string(),
    };

    format!(
        "{}/{}-{} AS{}",
        addr,
        prefix.prefix_length,
        ro.max_length(),
        ro.asn().into_u32()
    )
}

pub fn payload_to_string(payload: &Payload) -> String {
    match payload {
        Payload::RouteOrigin(ro) => format!("RouteOrigin({})", route_origin_to_string(ro)),
        Payload::RouterKey(_) => "RouterKey(...)".to_string(),
        Payload::Aspa(_) => "Aspa(...)".to_string(),
    }
}

pub fn payloads_to_pretty_lines(payloads: &[Payload]) -> String {
    let mut out = String::new();
    for (idx, payload) in payloads.iter().enumerate() {
        let _ = writeln!(&mut out, "  [{}] {}", idx, payload_to_string(payload));
    }
    out
}

pub fn print_payloads(label: &str, payloads: &[Payload]) {
    println!(
        "\n===== {} =====\n{}",
        label,
        payloads_to_pretty_lines(payloads)
    );
}

pub fn serial_result_to_string(result: &SerialResult) -> String {
    match result {
        SerialResult::UpToDate => "UpToDate".to_string(),
        SerialResult::ResetRequired => "ResetRequired".to_string(),
        SerialResult::Delta(delta) => {
            format!("Delta serial={}", delta.serial())
        }
    }
}

pub fn print_serial_result(label: &str, result: &SerialResult) {
    println!(
        "\n===== {} =====\n{}\n",
        label,
        serial_result_to_string(result)
    );
}

pub fn bytes_to_hex(bytes: &[u8]) -> String {
    let mut out = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        let _ = write!(&mut out, "{:02x}", b);
    }
    out
}

pub fn print_snapshot_hashes(label: &str, snapshot: &rpki::rtr::cache::Snapshot) {
    println!(
        "\n===== {} =====\norigins_hash={}\nrouter_keys_hash={}\naspas_hash={}\nsnapshot_hash={}\n",
        label,
        bytes_to_hex(&snapshot.origins_hash()),
        bytes_to_hex(&snapshot.router_keys_hash()),
        bytes_to_hex(&snapshot.aspas_hash()),
        bytes_to_hex(&snapshot.snapshot_hash()),
    );
}

pub fn test_report(name: &str, purpose: &str, input: &str, output: &str) {
    println!(
        "\n==================== TEST REPORT ====================\n测试名称: {}\n测试目的: {}\n\n【输入】\n{}\n【输出】\n{}\n====================================================\n",
        name, purpose, input, output
    );
}

pub fn payloads_to_string(payloads: &[Payload]) -> String {
    let mut out = String::new();
    for (idx, payload) in payloads.iter().enumerate() {
        let _ = writeln!(&mut out, "  [{}] {}", idx, payload_to_string(payload));
    }
    if out.is_empty() {
        out.push_str("  <empty>\n");
    }
    out
}

pub fn snapshot_hashes_to_string(snapshot: &rpki::rtr::cache::Snapshot) -> String {
    format!(
        "  origins_hash: {}\n  router_keys_hash: {}\n  aspas_hash: {}\n  snapshot_hash: {}\n",
        bytes_to_hex(&snapshot.origins_hash()),
        bytes_to_hex(&snapshot.router_keys_hash()),
        bytes_to_hex(&snapshot.aspas_hash()),
        bytes_to_hex(&snapshot.snapshot_hash()),
    )
}

pub fn serial_result_detail_to_string(result: &rpki::rtr::cache::SerialResult) -> String {
    match result {
        rpki::rtr::cache::SerialResult::UpToDate => "  result: UpToDate\n".to_string(),
        rpki::rtr::cache::SerialResult::ResetRequired => "  result: ResetRequired\n".to_string(),
        rpki::rtr::cache::SerialResult::Delta(delta) => {
            let mut out = String::new();
            let _ = writeln!(&mut out, "  result: Delta");
            let _ = writeln!(&mut out, "  delta.serial: {}", delta.serial());
            let _ = writeln!(&mut out, "  delta.announced:");
            out.push_str(&indent_block(&payloads_to_string(delta.announced()), 4));
            let _ = writeln!(&mut out, "  delta.withdrawn:");
            out.push_str(&indent_block(&payloads_to_string(delta.withdrawn()), 4));
            out
        }
    }
}

pub fn indent_block(text: &str, spaces: usize) -> String {
    let pad = " ".repeat(spaces);
    let mut out = String::new();
    for line in text.lines() {
        let _ = writeln!(&mut out, "{}{}", pad, line);
    }
    out
}
