use std::cmp::Ordering;
use std::fmt;

use crate::data_model::resources::ip_resources::IPAddress;
use crate::rtr::payload::{Aspa, Payload, RouteOrigin, RouterKey, Ski};

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct OrderingViolation {
    index: usize,
    left: String,
    right: String,
}

impl OrderingViolation {
    fn new(index: usize, left: &Payload, right: &Payload) -> Self {
        Self {
            index,
            left: payload_brief(left),
            right: payload_brief(right),
        }
    }

    fn new_update(index: usize, left: (bool, &Payload), right: (bool, &Payload)) -> Self {
        Self {
            index,
            left: payload_update_brief(left.0, left.1),
            right: payload_update_brief(right.0, right.1),
        }
    }
}

impl fmt::Display for OrderingViolation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "RTR payload ordering violation at positions {} and {}: {} should not appear before {}",
            self.index,
            self.index + 1,
            self.left,
            self.right
        )
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum RouterKeyKey {
    Key { ski: Ski, spki: Vec<u8>, asn: u32 },
}

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum ChangeKey {
    RouteOrigin(RouteOriginKey),
    RouterKey(RouterKeyKey),
    AspaCustomer(u32),
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
enum PayloadPduType {
    Ipv4Prefix = 4,
    Ipv6Prefix = 6,
    RouterKey = 9,
    Aspa = 11,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum RouteOriginKey {
    V4 {
        addr: u32,
        plen: u8,
        mlen: u8,
        asn: u32,
    },
    V6 {
        addr: u128,
        plen: u8,
        mlen: u8,
        asn: u32,
    },
}

pub(crate) fn change_key(payload: &Payload) -> ChangeKey {
    match payload {
        Payload::RouteOrigin(ro) => ChangeKey::RouteOrigin(route_origin_key(ro)),
        Payload::RouterKey(rk) => ChangeKey::RouterKey(router_key_key(rk)),
        Payload::Aspa(aspa) => ChangeKey::AspaCustomer(aspa.customer_asn().into_u32()),
    }
}

pub(crate) fn compare_payload_update_for_rtr(
    a_payload: &Payload,
    a_announce: bool,
    b_payload: &Payload,
    b_announce: bool,
) -> Ordering {
    let type_a = payload_pdu_type(a_payload);
    let type_b = payload_pdu_type(b_payload);

    match type_a.cmp(&type_b) {
        Ordering::Equal => {}
        other => return other,
    }

    match b_announce.cmp(&a_announce) {
        Ordering::Equal => {}
        other => return other,
    }

    match (a_payload, b_payload) {
        (Payload::RouteOrigin(a), Payload::RouteOrigin(b)) => {
            compare_route_origin_for_rtr(a, b, a_announce)
        }
        (Payload::RouterKey(a), Payload::RouterKey(b)) => compare_router_key_for_rtr(a, b),
        (Payload::Aspa(a), Payload::Aspa(b)) => compare_aspa_for_rtr(a, b),
        _ => Ordering::Equal,
    }
}

pub(crate) fn sort_payloads_for_rtr(payloads: &mut [Payload], announce: bool) {
    payloads.sort_by(|a, b| compare_payload_for_rtr(a, b, announce));
}

pub fn validate_payloads_for_rtr(
    payloads: &[Payload],
    announce: bool,
) -> Result<(), OrderingViolation> {
    for (index, pair) in payloads.windows(2).enumerate() {
        if compare_payload_for_rtr(&pair[0], &pair[1], announce) == Ordering::Greater {
            return Err(OrderingViolation::new(index, &pair[0], &pair[1]));
        }
    }

    Ok(())
}

pub fn validate_payload_updates_for_rtr(
    updates: &[(bool, Payload)],
) -> Result<(), OrderingViolation> {
    for (index, pair) in updates.windows(2).enumerate() {
        if compare_payload_update_for_rtr(&pair[0].1, pair[0].0, &pair[1].1, pair[1].0)
            == Ordering::Greater
        {
            return Err(OrderingViolation::new_update(
                index,
                (pair[0].0, &pair[0].1),
                (pair[1].0, &pair[1].1),
            ));
        }
    }

    Ok(())
}

pub fn validate_payload_update_refs_for_rtr(
    updates: &[(bool, &Payload)],
) -> Result<(), OrderingViolation> {
    for (index, pair) in updates.windows(2).enumerate() {
        if compare_payload_update_for_rtr(pair[0].1, pair[0].0, pair[1].1, pair[1].0)
            == Ordering::Greater
        {
            return Err(OrderingViolation::new_update(
                index,
                (pair[0].0, pair[0].1),
                (pair[1].0, pair[1].1),
            ));
        }
    }

    Ok(())
}

fn router_key_key(rk: &RouterKey) -> RouterKeyKey {
    RouterKeyKey::Key {
        ski: rk.ski(),
        spki: rk.spki().as_ref().to_vec(),
        asn: rk.asn().into_u32(),
    }
}

fn compare_payload_for_rtr(a: &Payload, b: &Payload, announce: bool) -> Ordering {
    let type_a = payload_pdu_type(a);
    let type_b = payload_pdu_type(b);

    match type_a.cmp(&type_b) {
        Ordering::Equal => {}
        other => return other,
    }

    match (a, b) {
        (Payload::RouteOrigin(a), Payload::RouteOrigin(b)) => {
            compare_route_origin_for_rtr(a, b, announce)
        }
        (Payload::RouterKey(a), Payload::RouterKey(b)) => compare_router_key_for_rtr(a, b),
        (Payload::Aspa(a), Payload::Aspa(b)) => compare_aspa_for_rtr(a, b),
        _ => Ordering::Equal,
    }
}

fn payload_pdu_type(payload: &Payload) -> PayloadPduType {
    match payload {
        Payload::RouteOrigin(ro) => {
            if route_origin_is_ipv4(ro) {
                PayloadPduType::Ipv4Prefix
            } else {
                PayloadPduType::Ipv6Prefix
            }
        }
        Payload::RouterKey(_) => PayloadPduType::RouterKey,
        Payload::Aspa(_) => PayloadPduType::Aspa,
    }
}

fn route_origin_is_ipv4(ro: &RouteOrigin) -> bool {
    ro.prefix().address.is_ipv4()
}

fn route_origin_key(ro: &RouteOrigin) -> RouteOriginKey {
    let prefix = ro.prefix();
    let plen = prefix.prefix_length;
    let mlen = ro.max_length();
    let asn = ro.asn().into_u32();

    match prefix.address {
        IPAddress::V4(addr) => RouteOriginKey::V4 {
            addr: u32::from(addr),
            plen,
            mlen,
            asn,
        },
        IPAddress::V6(addr) => RouteOriginKey::V6 {
            addr: u128::from(addr),
            plen,
            mlen,
            asn,
        },
    }
}

fn compare_route_origin_for_rtr(a: &RouteOrigin, b: &RouteOrigin, announce: bool) -> Ordering {
    match (route_origin_key(a), route_origin_key(b)) {
        (
            RouteOriginKey::V4 {
                addr: addr_a,
                plen: plen_a,
                mlen: mlen_a,
                asn: asn_a,
            },
            RouteOriginKey::V4 {
                addr: addr_b,
                plen: plen_b,
                mlen: mlen_b,
                asn: asn_b,
            },
        ) => {
            if announce {
                addr_b
                    .cmp(&addr_a)
                    .then_with(|| mlen_b.cmp(&mlen_a))
                    .then_with(|| plen_b.cmp(&plen_a))
                    .then_with(|| asn_b.cmp(&asn_a))
            } else {
                addr_a
                    .cmp(&addr_b)
                    .then_with(|| mlen_a.cmp(&mlen_b))
                    .then_with(|| plen_a.cmp(&plen_b))
                    .then_with(|| asn_a.cmp(&asn_b))
            }
        }
        (
            RouteOriginKey::V6 {
                addr: addr_a,
                plen: plen_a,
                mlen: mlen_a,
                asn: asn_a,
            },
            RouteOriginKey::V6 {
                addr: addr_b,
                plen: plen_b,
                mlen: mlen_b,
                asn: asn_b,
            },
        ) => {
            if announce {
                addr_b
                    .cmp(&addr_a)
                    .then_with(|| mlen_b.cmp(&mlen_a))
                    .then_with(|| plen_b.cmp(&plen_a))
                    .then_with(|| asn_b.cmp(&asn_a))
            } else {
                addr_a
                    .cmp(&addr_b)
                    .then_with(|| mlen_a.cmp(&mlen_b))
                    .then_with(|| plen_a.cmp(&plen_b))
                    .then_with(|| asn_a.cmp(&asn_b))
            }
        }
        _ => Ordering::Equal,
    }
}

fn compare_router_key_for_rtr(a: &RouterKey, b: &RouterKey) -> Ordering {
    a.ski()
        .cmp(&b.ski())
        .then_with(|| a.spki().len().cmp(&b.spki().len()))
        .then_with(|| a.spki().cmp(b.spki()))
        .then_with(|| a.asn().into_u32().cmp(&b.asn().into_u32()))
}

fn compare_aspa_for_rtr(a: &Aspa, b: &Aspa) -> Ordering {
    a.customer_asn()
        .into_u32()
        .cmp(&b.customer_asn().into_u32())
}

fn payload_brief(payload: &Payload) -> String {
    match payload {
        Payload::RouteOrigin(origin) => format!(
            "{} prefix {:?}/{} max={} asn={}",
            if route_origin_is_ipv4(origin) {
                "IPv4"
            } else {
                "IPv6"
            },
            origin.prefix().address,
            origin.prefix().prefix_length,
            origin.max_length(),
            origin.asn().into_u32()
        ),
        Payload::RouterKey(key) => format!(
            "RouterKey ski={:02x?} asn={}",
            key.ski().as_ref(),
            key.asn().into_u32()
        ),
        Payload::Aspa(aspa) => format!("ASPA customer_asn={}", aspa.customer_asn().into_u32()),
    }
}

fn payload_update_brief(announce: bool, payload: &Payload) -> String {
    format!(
        "{} {}",
        if announce { "announce" } else { "withdraw" },
        payload_brief(payload)
    )
}
