#![feature(rustc_private)]
use std::{
    collections::HashMap,
    net::{IpAddr, Ipv4Addr},
    path::{Path, PathBuf},
    process::ExitCode,
    str::FromStr,
};

use clap::{Args, Parser, Subcommand};
use netutils::{netconfig::NetConfig, Error};
use runtime::abi::net::{
    self,
    netconfig::{InterfaceInfo, RouteInfo},
};
use serde::Deserialize;

#[derive(Debug, Parser)]
#[clap(author, version, about, long_about = None)]
pub struct Arguments {
    #[clap(subcommand)]
    section: Section,
}

#[derive(Debug, Subcommand)]
pub enum Section {
    #[clap(
        arg_required_else_help = true,
        about = "Apply settings from configuration file"
    )]
    Apply(#[clap(subcommand)] ApplyAction),
    #[clap(arg_required_else_help = true, about = "Route query and manipulation")]
    Route(#[clap(subcommand)] RouteAction),
    #[clap(
        arg_required_else_help = true,
        about = "Address query and manipulation"
    )]
    Addr(#[clap(subcommand)] AddrAction),
    #[clap(
        arg_required_else_help = true,
        about = "Link information query and manipulation"
    )]
    Link(#[clap(subcommand)] LinkAction),
}

#[derive(Debug, Args)]
pub struct ApplyAction {
    #[clap(help = "Config filename")]
    filename: PathBuf,
}

#[derive(Debug, Args)]
pub struct RouteAction {
    #[clap(subcommand)]
    command: RouteCommands,
}

#[derive(Debug, Args)]
pub struct AddrAction {
    #[clap(subcommand)]
    command: AddrCommands,
}

#[derive(Debug, Args)]
pub struct LinkAction {
    #[clap(subcommand)]
    command: LinkCommands,
}

#[derive(Debug, Deserialize)]
pub struct InterfaceConfiguration {
    pub address: Option<IpAddr>,
    pub routes: Option<Vec<RouteConfiguration>>,
}

#[derive(Debug, Deserialize)]
pub struct RouteConfiguration {
    pub network: Ipv4Addr,
    pub mask: u8,
    pub gateway: Ipv4Addr,
}

#[derive(Debug, Deserialize)]
pub struct Configuration {
    pub interfaces: HashMap<String, InterfaceConfiguration>,
}

#[derive(Debug, Subcommand)]
pub enum RouteCommands {
    #[clap(about = "List routes")]
    List {
        #[clap(help = "Filter routes by given network interface")]
        interface: Option<String>,
    },
    #[clap(about = "Add a route", arg_required_else_help = true)]
    Add {
        #[clap(help = "Network interface the route is associated with")]
        interface: String,
        #[clap(
            help = "Subnetwork the route is directed to",
            value_parser = SubnetAddr::from_str
        )]
        subnet: SubnetAddr,
        #[clap(help = "Gateway the route should be reached through")]
        gateway: Option<IpAddr>,
    },
}

#[derive(Debug, Subcommand)]
pub enum AddrCommands {
    #[clap(about = "List network interface IP addresses")]
    List,
    #[clap(about = "Get interface IP address")]
    Get {
        #[clap(help = "Interface to query")]
        interface: String,
    },
    #[clap(about = "Set interface's IP address")]
    Set {
        #[clap(help = "Interface to assign address to")]
        interface: String,
        #[clap(help = "IPv4/IPv6 address for the interface")]
        address: IpAddr,
    },
}

#[derive(Debug, Subcommand)]
pub enum LinkCommands {
    #[clap(about = "List network interfaces")]
    List,
    #[clap(about = "Describe a network interface")]
    Show {
        #[clap(help = "Interface to describe")]
        interface: String,
    },
    #[clap(about = "Get interface MAC address")]
    GetMac {
        #[clap(help = "Interface to describe")]
        interface: String,
    },
}

#[derive(Clone, Debug)]
pub struct SubnetAddr(net::SubnetAddr);

impl FromStr for SubnetAddr {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let inner = net::SubnetAddr::from_str(s).map_err(|_| "Invalid subnet format")?;
        Ok(Self(inner))
    }
}

pub fn run_route_action(nc: &mut NetConfig, action: RouteCommands) -> Result<(), Error> {
    match action {
        RouteCommands::List { interface: None } => {
            let list = nc.list_routes()?;
            for route in list {
                print_route_info(&route);
            }

            Ok(())
        }
        RouteCommands::List {
            interface: Some(interface),
        } => {
            let list = nc.describe_routes(&*interface)?;

            for route in list {
                print_route_info(&route);
            }

            Ok(())
        }
        RouteCommands::Add {
            interface,
            subnet,
            gateway,
        } => nc.add_route(&*interface, subnet.0, gateway.map(Into::into)),
    }
}

pub fn run_addr_action(nc: &mut NetConfig, action: AddrCommands) -> Result<(), Error> {
    match action {
        AddrCommands::List => {
            for binding in nc.list_interfaces()? {
                let info = match nc.describe_interface(binding.id) {
                    Ok(info) => info,
                    Err(error) => {
                        eprintln!("{}: {error}", binding.name);
                        continue;
                    }
                };

                if let Some(address) = info.address {
                    println!("{}: {address}", binding.name);
                }
            }

            Ok(())
        }
        AddrCommands::Get { interface } => {
            let info = nc.describe_interface(&*interface)?;

            if let Some(address) = info.address {
                println!("{}", address);
            } else {
                println!("<No address>");
            }

            Ok(())
        }
        AddrCommands::Set { interface, address } => nc.set_interface_address(&*interface, address),
    }
}

pub fn run_link_action(nc: &mut NetConfig, action: LinkCommands) -> Result<(), Error> {
    match action {
        LinkCommands::List => {
            let list = nc.list_interfaces()?;

            for binding in list {
                let info = match nc.describe_interface(binding.id) {
                    Ok(info) => info,
                    Err(error) => {
                        eprintln!("{}: {error}", binding.name);
                        continue;
                    }
                };

                print_interface_info(&info);
            }

            Ok(())
        }
        LinkCommands::Show { interface } => {
            let info = nc.describe_interface(&*interface)?;

            print_interface_info(&info);

            Ok(())
        }
        LinkCommands::GetMac { interface } => {
            let info = nc.describe_interface(&*interface)?;

            println!("{}", info.mac);

            Ok(())
        }
    }
}

pub fn run_apply_action(nc: &mut NetConfig, filename: &Path) -> Result<(), Error> {
    let config_str = std::fs::read_to_string(filename)?;
    let config: Configuration = toml::from_str(&config_str)?;

    for (if_name, if_config) in config.interfaces {
        let if_name = if_name.as_str();
        if let Some(address) = if_config.address {
            nc.set_interface_address(if_name, address)?;
        }

        // TODO drop old routes
        if let Some(routes) = if_config.routes {
            for route in routes {
                let subnet = net::SubnetAddr::V4(net::SubnetV4Addr::from_address_mask(
                    route.network,
                    route.mask,
                ));
                nc.add_route(if_name, subnet, Some(route.gateway.into()))?;
            }
        }
    }

    Ok(())
}

pub fn run_action(section: Section) -> Result<(), Error> {
    let mut nc = NetConfig::open()?;
    match section {
        Section::Route(RouteAction { command }) => run_route_action(&mut nc, command),
        Section::Addr(AddrAction { command }) => run_addr_action(&mut nc, command),
        Section::Link(LinkAction { command }) => run_link_action(&mut nc, command),
        Section::Apply(ApplyAction { filename }) => run_apply_action(&mut nc, filename.as_path()),
    }
}

fn print_interface_info(info: &InterfaceInfo) {
    println!("{}:", info.interface_name);
    println!("  Id: #{}", info.interface_id);
    println!("  Link: {}", info.link);
    if let Some(address) = info.address {
        println!("  Address: {}", address);
    } else {
        println!("  <No address>");
    }
    println!("  Hardware address: {}", info.mac);
}

fn print_route_info(info: &RouteInfo) {
    print!("{}", info.subnet);
    if let Some(gw) = info.gateway {
        print!(" via {}", gw);
    }
    println!(" dev {}", info.interface_name);
}

fn main() -> ExitCode {
    let args = Arguments::parse();

    match run_action(args.section) {
        Ok(_) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("Error: {}", error);
            ExitCode::FAILURE
        }
    }
}

