#![warn(missing_docs)]
// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
// SPDX-License-Identifier: GPL-2.0-or-later
//! Display a range of IP addresses.
//!
//! The `prips` tool takes either a start and end address or a CIDR
//! subnet/prefixlen specification and displays the IPv4 addresses
//! contained within the specified range.

use std::env;

use clap::Parser as _;
use clap::error::{Error as ClapError, ErrorKind as ClapErrorKind, Result as ClapResult};
use clap_derive::Parser;
use eyre::{Result, WrapErr as _, bail, eyre};

mod defs;
mod parse;
mod prips;

use defs::{AddrExclude, AddrFormat, Config};

const VERSION: &str = env!("CARGO_PKG_VERSION");

#[derive(Debug, Parser)]
/// The top-level command-line arguments.
#[clap(version)]
struct Cli {
    /// Print the range in CIDR notation".
    #[clap(short)]
    cidr: bool,

    /// Set the delimiter to the character with the specified ASCII code,
    #[clap(short, default_value("10"))]
    delim: u8,

    /// Exclude a specific pattern of IP addresses.
    #[clap(short, value_parser(_clap_parse_exclude))]
    #[expect(clippy::absolute_paths, reason = "something seems off with clap")]
    exclude: Option<std::vec::Vec<AddrExclude>>,

    /// Set the address display format (hex, dec, or dot),
    #[clap(short, default_value("dot"), allow_hyphen_values(true))]
    format: AddrFormat,

    /// Display information about the supported features.
    #[clap(long)]
    features: bool,

    /// Increment by the specified number of addresses instead of one.
    #[clap(short('i'), default_value("1"))]
    step: usize,

    /// The CIDR range or the address to print from.
    first: Option<String>,

    /// The address to print to, if not a CIDR range.
    second: Option<String>,
}

/// The program operation mode: list addresses or do something else.
#[derive(Debug)]
enum Mode {
    /// Display usage or version information, already done.
    Handled,

    /// List the addresses in the specified range.
    List(Config),

    /// Represent a range in CIDR form.
    Cidrize(Config),
}

/// Use [`parse::parse_exclude`] to parse the exclude pattern command-line argument.
fn _clap_parse_exclude(value: &str) -> ClapResult<Vec<AddrExclude>> {
    // Maybe there is a way to pass the error string as context, but I have overlooked it.
    #[expect(clippy::map_err_ignore, reason = "we really do not care")]
    parse::parse_exclude(value).map_err(|_| ClapError::new(ClapErrorKind::ValueValidation))
}

#[expect(clippy::print_stdout, reason = "list features, options, etc")]
fn parse_args() -> Result<Mode> {
    let args = match Cli::try_parse() {
        Ok(args) => args,
        Err(err)
            if matches!(
                err.kind(),
                ClapErrorKind::DisplayHelp | ClapErrorKind::DisplayVersion
            ) =>
        {
            err.print()
                .context("Could not display the usage or version message")?;
            return Ok(Mode::Handled);
        }
        Err(err) if err.kind() == ClapErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => {
            err.print()
                .context("Could not display the usage or version message")?;
            bail!("Invalid or missing command-line options");
        }
        Err(err) => return Err(err).context("Could not parse the command-line options"),
    };

    if args.features {
        println!("Features: prips={VERSION} prips-impl-rust={VERSION} exclude=1.0");
    }
    if args.features {
        return Ok(Mode::Handled);
    }

    let range = match (args.first, args.second) {
        (None, _) => bail!("No addresses specified"),
        (Some(first), None) => parse::parse_cidr(&first)?,
        (Some(first), Some(second)) => parse::parse_range(&first, &second)?,
    };

    let delim = char::from_u32(u32::from(args.delim))
        .ok_or_else(|| eyre!("Invalid delimiter character code"))?;

    let exclude = args.exclude.map_or_else(Vec::new, |excls| {
        excls
            .into_iter()
            .filter(|excl| excl.mask() != 0_u32)
            .collect::<Vec<_>>()
    });

    let cfg = Config {
        delim,
        format: args.format,
        exclude: (!exclude.is_empty()).then_some(exclude),
        range,
        step: args.step,
    };
    if args.cidr {
        Ok(Mode::Cidrize(cfg))
    } else {
        Ok(Mode::List(cfg))
    }
}

#[expect(clippy::print_stdout, reason = "this is the whole point")]
fn print_range(cfg: &Config) {
    for addr in prips::output_range(cfg) {
        print!("{addr}{delim}", delim = cfg.delim);
    }
}

#[expect(clippy::print_stdout, reason = "this is the whole point")]
fn print_cidr(cfg: &Config) -> Result<()> {
    let cidr = prips::cidrize(cfg).context("Could not compute a CIDR range")?;
    println!("{cidr}");
    Ok(())
}

fn main() -> Result<()> {
    let mode = parse_args().context("Could not parse the command-line arguments")?;
    match mode {
        Mode::Handled => (),
        Mode::List(cfg) => print_range(&cfg),
        Mode::Cidrize(cfg) => print_cidr(&cfg)?,
    }
    Ok(())
}
