use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VrpRecord {
    pub run_id: String,
    pub asn: u32,
    pub prefix: String,
    pub max_length: u8,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IpPrefix {
    addr: IpAddr,
    prefix_length: u8,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VrpLookup {
    Ip(IpAddr),
    Prefix(IpPrefix),
}

impl VrpRecord {
    pub fn from_report(
        run_id: impl Into<String>,
        asn: u32,
        prefix: &str,
        max_length: u8,
    ) -> Result<Self, String> {
        let network = IpPrefix::parse(prefix)?;
        if max_length < network.prefix_length {
            return Err(format!(
                "VRP max_length {max_length} is shorter than prefix length {}",
                network.prefix_length
            ));
        }
        if max_length > network.max_prefix_length() {
            return Err(format!(
                "VRP max_length {max_length} exceeds address family limit {}",
                network.max_prefix_length()
            ));
        }
        Ok(Self {
            run_id: run_id.into(),
            asn,
            prefix: network.to_cidr(),
            max_length,
        })
    }

    pub fn prefix_data(&self) -> Result<IpPrefix, String> {
        IpPrefix::parse(&self.prefix)
    }

    pub fn index_key(&self) -> Result<String, String> {
        let prefix = self.prefix_data()?;
        Ok(format!(
            "vrp/{}/{}/{:03}/{}/{:010}/{:03}",
            self.run_id,
            prefix.family(),
            prefix.prefix_length,
            prefix.network_hex(),
            self.asn,
            self.max_length
        ))
    }
}

impl IpPrefix {
    pub fn parse(value: &str) -> Result<Self, String> {
        let (addr, raw_length) = value
            .rsplit_once('/')
            .ok_or_else(|| format!("invalid CIDR prefix: {value}"))?;
        let ip = addr
            .parse::<IpAddr>()
            .map_err(|_| format!("invalid IP address in prefix: {value}"))?;
        let prefix_length = raw_length
            .parse::<u8>()
            .map_err(|_| format!("invalid prefix length in: {value}"))?;
        Self::new(ip, prefix_length)
    }

    pub fn new(addr: IpAddr, prefix_length: u8) -> Result<Self, String> {
        if prefix_length > max_prefix_length(addr) {
            return Err(format!(
                "prefix length {prefix_length} exceeds address family limit {}",
                max_prefix_length(addr)
            ));
        }
        Ok(Self {
            addr: network_address(addr, prefix_length),
            prefix_length,
        })
    }

    pub fn prefix_length(&self) -> u8 {
        self.prefix_length
    }

    pub fn max_prefix_length(&self) -> u8 {
        max_prefix_length(self.addr)
    }

    pub fn family(&self) -> &'static str {
        match self.addr {
            IpAddr::V4(_) => "v4",
            IpAddr::V6(_) => "v6",
        }
    }

    pub fn network_hex(&self) -> String {
        match self.addr {
            IpAddr::V4(addr) => hex::encode(addr.octets()),
            IpAddr::V6(addr) => hex::encode(addr.octets()),
        }
    }

    pub fn to_cidr(&self) -> String {
        format!("{}/{}", self.addr, self.prefix_length)
    }

    pub fn contains_ip(&self, ip: IpAddr) -> bool {
        if std::mem::discriminant(&self.addr) != std::mem::discriminant(&ip) {
            return false;
        }
        network_address(ip, self.prefix_length) == self.addr
    }

    pub fn contains_prefix(&self, other: &Self) -> bool {
        self.family() == other.family()
            && self.prefix_length <= other.prefix_length
            && self.contains_ip(other.addr)
    }

    pub fn ancestor_prefixes(&self) -> Vec<Self> {
        (0..=self.prefix_length)
            .filter_map(|length| Self::new(self.addr, length).ok())
            .collect()
    }
}

impl VrpLookup {
    pub fn parse_ip(value: &str) -> Result<Self, String> {
        value
            .parse::<IpAddr>()
            .map(VrpLookup::Ip)
            .map_err(|_| format!("invalid IP address: {value}"))
    }

    pub fn parse_prefix(value: &str) -> Result<Self, String> {
        IpPrefix::parse(value).map(VrpLookup::Prefix)
    }

    pub fn candidate_prefixes(&self) -> Vec<IpPrefix> {
        match self {
            VrpLookup::Ip(ip) => (0..=max_prefix_length(*ip))
                .filter_map(|length| IpPrefix::new(*ip, length).ok())
                .collect(),
            VrpLookup::Prefix(prefix) => prefix.ancestor_prefixes(),
        }
    }

    pub fn matches(&self, record: &VrpRecord) -> Result<bool, String> {
        let record_prefix = record.prefix_data()?;
        Ok(match self {
            VrpLookup::Ip(ip) => record_prefix.contains_ip(*ip),
            VrpLookup::Prefix(prefix) => {
                record_prefix.contains_prefix(prefix) && record.max_length >= prefix.prefix_length
            }
        })
    }
}

pub fn vrp_key_prefix(run_id: &str, prefix: &IpPrefix) -> String {
    format!(
        "vrp/{}/{}/{:03}/{}/",
        run_id,
        prefix.family(),
        prefix.prefix_length,
        prefix.network_hex()
    )
}

pub fn parse_cursor(cursor: Option<&str>) -> Result<usize, String> {
    let Some(cursor) = cursor else {
        return Ok(0);
    };
    let offset = cursor
        .strip_prefix("vrp:")
        .ok_or_else(|| format!("invalid VRP cursor: {cursor}"))?
        .parse::<usize>()
        .map_err(|_| format!("invalid VRP cursor: {cursor}"))?;
    Ok(offset)
}

pub fn next_cursor(offset: usize, limit: usize, total: usize) -> Option<String> {
    let next = offset.saturating_add(limit);
    (next < total).then(|| format!("vrp:{next}"))
}

fn max_prefix_length(addr: IpAddr) -> u8 {
    match addr {
        IpAddr::V4(_) => 32,
        IpAddr::V6(_) => 128,
    }
}

fn network_address(addr: IpAddr, prefix_length: u8) -> IpAddr {
    match addr {
        IpAddr::V4(addr) => {
            let value = u32::from(addr);
            let mask = if prefix_length == 0 {
                0
            } else {
                u32::MAX << (32 - prefix_length)
            };
            IpAddr::V4(Ipv4Addr::from(value & mask))
        }
        IpAddr::V6(addr) => {
            let mut octets = addr.octets();
            let full_bytes = usize::from(prefix_length / 8);
            let remaining_bits = prefix_length % 8;
            if full_bytes < octets.len() {
                if remaining_bits > 0 {
                    octets[full_bytes] &= 0xff << (8 - remaining_bits);
                }
                let first_zero = full_bytes + usize::from(remaining_bits > 0);
                for byte in &mut octets[first_zero..] {
                    *byte = 0;
                }
            }
            IpAddr::V6(Ipv6Addr::from(octets))
        }
    }
}

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

    #[test]
    fn normalizes_ipv4_and_ipv6_networks() {
        assert_eq!(
            IpPrefix::parse("192.0.2.129/24").unwrap().to_cidr(),
            "192.0.2.0/24"
        );
        assert_eq!(
            IpPrefix::parse("2001:db8:1::1234/48").unwrap().to_cidr(),
            "2001:db8:1::/48"
        );
    }

    #[test]
    fn prefix_lookup_uses_covering_and_max_length_semantics() {
        let lookup = VrpLookup::parse_prefix("192.0.2.0/24").unwrap();
        let covering = VrpRecord::from_report("run_1", 64496, "192.0.0.0/16", 24).unwrap();
        let too_short = VrpRecord::from_report("run_1", 64497, "192.0.0.0/16", 23).unwrap();
        let intersecting = VrpRecord::from_report("run_1", 64498, "192.0.2.128/25", 25).unwrap();
        assert!(lookup.matches(&covering).unwrap());
        assert!(!lookup.matches(&too_short).unwrap());
        assert!(!lookup.matches(&intersecting).unwrap());
    }

    #[test]
    fn cursor_is_scoped_and_bounded() {
        assert_eq!(parse_cursor(None).unwrap(), 0);
        assert_eq!(parse_cursor(Some("vrp:12")).unwrap(), 12);
        assert!(parse_cursor(Some("12")).is_err());
        assert_eq!(next_cursor(0, 10, 12).as_deref(), Some("vrp:10"));
        assert_eq!(next_cursor(10, 10, 12), None);
    }
}
