// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
// SPDX-License-Identifier: GPL-2.0-or-later
//! Parse an IP address or a CIDR specification.

use eyre::eyre;

use crate::defs::{AddrExclude, AddrRange, Error};

#[cfg(feature = "nom7")]
mod p_impl {
    use nom7::{
        Err as NErr, IResult,
        bytes::complete::tag,
        character::complete::{none_of, u8 as p_u8, u32 as p_u32},
        combinator::{all_consuming, opt},
        error::Error as NError,
        multi::separated_list0,
        sequence::{preceded, separated_pair, tuple},
    };

    use crate::defs::Error;

    use super::AddrExcludeSpec;

    /// Make a `nom` error suitable for using as an `eyre` error.
    fn clone_err_input(err: NErr<NError<&str>>) -> NErr<NError<String>> {
        err.map_input(ToOwned::to_owned)
    }

    fn p_excl_single(input: &str) -> IResult<&str, Vec<u8>> {
        separated_list0(none_of("0123456789."), p_u8)(input)
    }

    /// Parse an exclusion pattern.
    fn p_exclude(input: &str) -> IResult<&str, AddrExcludeSpec> {
        let (r_input, (first, second_opt, third_opt, fourth_opt)) = tuple((
            p_excl_single,
            opt(preceded(tag("."), p_excl_single)),
            opt(preceded(tag("."), p_excl_single)),
            opt(preceded(tag("."), p_excl_single)),
        ))(input)?;

        Ok((r_input, (first, second_opt, third_opt, fourth_opt)))
    }

    /// Parse an exclusion pattern into an [`AddrExcludeSpec`] tuple.
    pub fn parse_exclude_spec(pattern: &str) -> Result<AddrExcludeSpec, Error> {
        match all_consuming(p_exclude)(pattern).map_err(clone_err_input) {
            Ok((_, excl)) => Ok(excl),
            Err(err) => Err(Error::ParseExclude(pattern.to_owned(), err.into())),
        }
    }

    /// Parse an IPv4 address into a [`u32`].
    fn p_ipv4(input: &str) -> IResult<&str, u32> {
        let (r_input, (first, second, third, fourth)) = tuple((
            p_u8,
            preceded(tag("."), p_u8),
            preceded(tag("."), p_u8),
            preceded(tag("."), p_u8),
        ))(input)?;
        Ok((r_input, super::u32_from_bytes(first, second, third, fourth)))
    }

    /// Parse an addr/prefixlen CIDR specification.
    fn p_cidr(input: &str) -> IResult<&str, (u32, u32)> {
        separated_pair(p_ipv4, tag("/"), p_u32)(input)
    }

    /// Parse an IPv4 address into an unsigned 32-bit number.
    pub fn parse_addr(saddr: &str) -> Result<u32, Error> {
        match all_consuming(p_ipv4)(saddr) {
            Ok((_, res)) => Ok(res),
            Err(err) => Err(Error::ParseAddress(
                saddr.to_owned(),
                clone_err_input(err).into(),
            )),
        }
    }

    /// Parse an address/prefixlen specification without any validation.
    pub fn parse_cidr_base(spec: &str) -> Result<(u32, u32), Error> {
        match all_consuming(p_cidr)(spec) {
            Ok((_, res)) => Ok(res),
            Err(err) => Err(Error::ParseCidr(
                spec.to_owned(),
                clone_err_input(err).into(),
            )),
        }
    }
}

#[cfg(all(feature = "nom8", not(feature = "nom7")))]
mod p_impl {
    use nom8::{
        Err as NErr, IResult, Parser as _,
        bytes::complete::tag,
        character::complete::{none_of, u8 as p_u8, u32 as p_u32},
        combinator::{all_consuming, opt},
        error::Error as NError,
        multi::separated_list0,
        sequence::{preceded, separated_pair},
    };

    use crate::defs::Error;

    use super::AddrExcludeSpec;

    /// Make a `nom` error suitable for using as an `eyre` error.
    fn clone_err_input(err: NErr<NError<&str>>) -> NErr<NError<String>> {
        err.map_input(ToOwned::to_owned)
    }

    fn p_excl_single(input: &str) -> IResult<&str, Vec<u8>> {
        separated_list0(none_of("0123456789."), p_u8).parse(input)
    }

    /// Parse an exclusion pattern.
    fn p_exclude(input: &str) -> IResult<&str, AddrExcludeSpec> {
        let (r_input, (first, second_opt, third_opt, fourth_opt)) = (
            p_excl_single,
            opt(preceded(tag("."), p_excl_single)),
            opt(preceded(tag("."), p_excl_single)),
            opt(preceded(tag("."), p_excl_single)),
        )
            .parse(input)?;

        Ok((r_input, (first, second_opt, third_opt, fourth_opt)))
    }

    /// Parse an exclusion pattern into an [`AddrExcludeSpec`] tuple.
    pub fn parse_exclude_spec(pattern: &str) -> Result<AddrExcludeSpec, Error> {
        match all_consuming(p_exclude)
            .parse(pattern)
            .map_err(clone_err_input)
        {
            Ok((_, excl)) => Ok(excl),
            Err(err) => Err(Error::ParseExclude(pattern.to_owned(), err.into())),
        }
    }

    /// Parse an IPv4 address into a [`u32`].
    fn p_ipv4(input: &str) -> IResult<&str, u32> {
        let (r_input, (first, second, third, fourth)) = (
            p_u8,
            preceded(tag("."), p_u8),
            preceded(tag("."), p_u8),
            preceded(tag("."), p_u8),
        )
            .parse(input)?;
        Ok((r_input, super::u32_from_bytes(first, second, third, fourth)))
    }

    /// Parse an addr/prefixlen CIDR specification.
    fn p_cidr(input: &str) -> IResult<&str, (u32, u32)> {
        separated_pair(p_ipv4, tag("/"), p_u32).parse(input)
    }

    /// Parse an IPv4 address into an unsigned 32-bit number.
    pub fn parse_addr(saddr: &str) -> Result<u32, Error> {
        match all_consuming(p_ipv4).parse(saddr) {
            Ok((_, res)) => Ok(res),
            Err(err) => Err(Error::ParseAddress(
                saddr.to_owned(),
                clone_err_input(err).into(),
            )),
        }
    }

    /// Parse an address/prefixlen specification without any validation.
    pub fn parse_cidr_base(spec: &str) -> Result<(u32, u32), Error> {
        match all_consuming(p_cidr).parse(spec) {
            Ok((_, res)) => Ok(res),
            Err(err) => Err(Error::ParseCidr(
                spec.to_owned(),
                clone_err_input(err).into(),
            )),
        }
    }
}

#[cfg(not(any(feature = "nom7", feature = "nom8")))]
mod p_impl {
    use eyre::eyre;
    use winnow::{
        Parser as _, Result as ParseResult,
        ascii::dec_uint,
        combinator::{eof, opt, preceded, separated, separated_pair, terminated},
        error::ContextError,
        token::none_of,
    };

    use crate::defs::Error;

    use super::AddrExcludeSpec;

    /// Parse an IPv4 address into a [`u32`].
    fn p_ipv4(input: &mut &str) -> ParseResult<u32> {
        let (first, second, third, fourth) = (
            terminated(dec_uint::<_, u8, ContextError>, '.'),
            terminated(dec_uint::<_, u8, ContextError>, '.'),
            terminated(dec_uint::<_, u8, ContextError>, '.'),
            dec_uint::<_, u8, ContextError>,
        )
            .parse_next(input)?;
        Ok(super::u32_from_bytes(first, second, third, fourth))
    }

    /// Parse an IPv4 address into an unsigned 32-bit number.
    pub fn parse_addr(saddr: &str) -> Result<u32, Error> {
        let mut input = saddr;
        terminated(p_ipv4, eof)
            .parse_next(&mut input)
            .map_err(|err| Error::ParseAddress(saddr.to_owned(), eyre!("{err}")))
    }

    /// Parse an address/prefixlen specification without any validation.
    pub fn parse_cidr_base(spec: &str) -> Result<(u32, u32), Error> {
        let mut input = spec;
        separated_pair(p_ipv4, '/', dec_uint::<_, u32, ContextError>)
            .parse_next(&mut input)
            .map_err(|err| Error::ParseCidr(spec.to_owned(), eyre!("{err}")))
    }

    /// Parse an exclusion pattern into an [`AddrExcludeSpec`] tuple.
    fn p_exclude(input: &mut &str) -> ParseResult<Vec<u8>> {
        separated(
            0..,
            dec_uint::<_, u8, ContextError>,
            none_of::<_, _, ContextError>(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.']),
        )
        .parse_next(input)
    }

    /// Parse an exclusion pattern into a series of [`AddrExclude`] objects.
    pub fn parse_exclude_spec(pattern: &str) -> Result<AddrExcludeSpec, Error> {
        let mut input = pattern;
        let (first, second_opt, third_opt, fourth_opt) = terminated(
            (
                p_exclude,
                opt(preceded(".", p_exclude)),
                opt(preceded(".", p_exclude)),
                opt(preceded(".", p_exclude)),
            ),
            eof,
        )
        .parse_next(&mut input)
        .map_err(|err| Error::ParseExclude(pattern.to_owned(), eyre!("{err}")))?;
        Ok((first, second_opt, third_opt, fourth_opt))
    }
}

/// The raw exclude-ranges specification as supplied by the user.
type AddrExcludeSpec = (Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>, Option<Vec<u8>>);

/// Parse an IPv4 address into an unsigned 32-bit number.
pub fn parse_addr(saddr: &str) -> Result<u32, Error> {
    p_impl::parse_addr(saddr)
}

/// Parse an address/prefixlen specification without any validation.
fn parse_cidr_base(spec: &str) -> Result<(u32, u32), Error> {
    p_impl::parse_cidr_base(spec)
}

/// Parse an exclusion pattern into a series of [`AddrExclude`] objects.
fn parse_exclude_spec(pattern: &str) -> Result<AddrExcludeSpec, Error> {
    p_impl::parse_exclude_spec(pattern)
}

/// Build a `u32` value from four bytes.
fn u32_from_bytes(first: u8, second: u8, third: u8, fourth: u8) -> u32 {
    #[expect(clippy::little_endian_bytes, reason = "simplest way to build")]
    u32::from_le_bytes((fourth, third, second, first).into())
}

/// Parse an exclusion pattern into a series of [`AddrExclude`] objects.
pub fn parse_exclude(pattern: &str) -> Result<Vec<AddrExclude>, Error> {
    let (first, second_opt, third_opt, fourth_opt) = parse_exclude_spec(pattern)?;
    let excl = {
        let excl_first = first
            .into_iter()
            .map(|excl| AddrExclude::new(0xFF_00_00_00_u32, u32_from_bytes(excl, 0, 0, 0)));
        let excl_second = second_opt
            .map_or_else(|| vec![].into_iter(), Vec::into_iter)
            .map(|excl| AddrExclude::new(0x00_FF_00_00, u32_from_bytes(0, excl, 0, 0)));
        let excl_third = third_opt
            .map_or_else(|| vec![].into_iter(), Vec::into_iter)
            .map(|excl| AddrExclude::new(0x00_00_FF_00, u32_from_bytes(0, 0, excl, 0)));
        let excl_fourth = fourth_opt
            .map_or_else(|| vec![].into_iter(), Vec::into_iter)
            .map(|excl| AddrExclude::new(0x00_00_00_FF, u32_from_bytes(0, 0, 0, excl)));

        excl_first
            .chain(excl_second)
            .chain(excl_third)
            .chain(excl_fourth)
            .collect::<Vec<_>>()
    };
    Ok(excl)
}

/// Parse an address/prefixlen specification into an address range.
pub fn parse_cidr(spec: &str) -> Result<AddrRange, Error> {
    let (start, prefix_len) = parse_cidr_base(spec)?;

    let err_internal = || {
        Error::Internal(eyre!(
            "Unexpected arithmetic operation error for {start}/{prefix_len}"
        ))
    };
    match prefix_len {
        0 => {
            if start == 0 {
                Ok(AddrRange {
                    start,
                    end: u32::MAX,
                })
            } else {
                Err(Error::CidrBadStart)
            }
        }
        value if value < 32 => {
            let offset = 1_u32
                .checked_shl(32_u32.checked_sub(value).ok_or_else(err_internal)?)
                .ok_or_else(err_internal)?;
            let mask = offset.checked_sub(1).ok_or_else(err_internal)?;
            if (start & mask) == 0 {
                Ok(AddrRange {
                    start,
                    end: start.checked_add(mask).ok_or_else(err_internal)?,
                })
            } else {
                Err(Error::CidrBadStart)
            }
        }
        32 => Ok(AddrRange { start, end: start }),
        _ => Err(Error::CidrPrefixTooLarge),
    }
}

/// Parse two IPv4 addresses into a range.
pub fn parse_range(first: &str, second: &str) -> Result<AddrRange, Error> {
    let start = parse_addr(first)?;
    let end = parse_addr(second)?;
    if start <= end {
        Ok(AddrRange { start, end })
    } else {
        Err(Error::StartBeforeEnd)
    }
}

#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "unit tests")]
mod tests {
    use eyre::{Result, bail};
    use rstest::rstest;

    use crate::defs::{AddrExclude, AddrRange};

    #[rstest]
    #[case("...", vec![])]
    #[case("...1", vec![AddrExclude::new(0x00_00_00_FF, 0x00_00_00_01 )])]
    #[case("..1.2,3", vec![
        AddrExclude::new(0x00_00_FF_00, 0x00_00_01_00),
        AddrExclude::new(0x00_00_00_FF, 0x00_00_00_02),
        AddrExclude::new(0x00_00_00_FF, 0x00_00_00_03),
    ])]
    fn parse_exclude_ok(#[case] value: &str, #[case] expected: Vec<AddrExclude>) {
        assert_eq!(super::parse_exclude(value).unwrap(), expected);
    }

    #[rstest]
    #[case("0.0.0.0", 0_u32)]
    #[case("0.0.0.1", 1_u32)]
    #[case("0.0.0.127", 127_u32)]
    #[case("0.0.0.255", 255_u32)]
    #[case("0.0.1.0", 256_u32)]
    #[case("1.2.3.4", 0x01_02_03_04_u32)]
    #[case("127.0.13.13", 0x7F_00_0D_0D_u32)]
    #[case("254.2.3.4", 0xFE_02_03_04_u32)]
    #[case("255.255.255.0", 0xFF_FF_FF_00_u32)]
    #[case("255.255.255.255", 0xFF_FF_FF_FF_u32)]
    fn parse_addr_ok(#[case] value: &str, #[case] expected: u32) {
        assert_eq!(super::parse_addr(value).unwrap(), expected);
    }

    #[rstest]
    #[case("")]
    #[case("1")]
    #[case("1.2")]
    #[case("1.2.3")]
    #[case("1.2.3.4.5")]
    #[case("a")]
    #[case("1.2.3.b")]
    #[case("0.0.0.256")]
    #[case("1.3.2.-4")]
    #[case("0.0.256.0")]
    #[case("0.256.0.0")]
    #[case("256.0.0.0")]
    #[case("1..2.3.4")]
    #[case(".2.3.4")]
    #[case("1..3.4")]
    #[case("1.2..4")]
    #[case("1.2.3.")]
    fn parse_addr_fail(#[case] value: &str) -> Result<()> {
        match super::parse_addr(value) {
            Ok(res) => bail!("Unexpected parse_addr() success for {value:?}: {res:?}"),
            Err(_) => Ok(()),
        }
    }

    #[rstest]
    #[case("0.0.0.0/0", AddrRange { start: 0_u32, end: 0xFF_FF_FF_FF_u32 })]
    #[case("0.0.0.0/1", AddrRange { start: 0_u32, end: 0x7F_FF_FF_FF_u32 })]
    #[case("127.0.13.0/24", AddrRange { start: 0x7F_00_0D_00_u32, end: 0x7F_00_0D_FF_u32 })]
    #[case("172.12.3.8/29", AddrRange { start: 0xAC_0C_03_08_u32, end: 0xAC_0C_03_0F_u32 })]
    fn parse_cidr_ok(#[case] value: &str, #[case] expected: AddrRange) {
        assert_eq!(super::parse_cidr(value).unwrap(), expected);
    }

    #[rstest]
    #[case("0.0.0.0", "255.255.255.255", AddrRange { start: 0_u32, end: 0xFF_FF_FF_FF_u32 })]
    #[case("1.0.1.0", "2.3.4.5", AddrRange { start: 0x01_00_01_00_u32, end: 0x02_03_04_05_u32 })]
    #[case("127.0.0.1", "127.0.0.2", AddrRange { start: 0x7F_00_00_01_u32, end: 0x7F_00_00_02_u32 })]
    #[case("127.0.0.1", "127.0.0.1", AddrRange { start: 0x7F_00_00_01_u32, end: 0x7F_00_00_01_u32 })]
    fn parse_range_ok(#[case] first: &str, #[case] second: &str, #[case] expected: AddrRange) {
        assert_eq!(super::parse_range(first, second).unwrap(), expected);
    }
}
