use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::convert::TryInto;

use crate::error::CliError;
use nmstate::{
    BaseInterface, BondConfig, BondInterface, BondMode, Interface,
    InterfaceState, InterfaceType, Interfaces, LldpNeighborTlv, NetworkState,
    VlanConfig, VlanInterface,
};

const APP_NAME: &str = "nmstatectl-autoconf";
const EXIT_FAILURE: i32 = 1;
const BOND_PREFIX: &str = "bond";

pub(crate) fn autoconf(argv: &[String]) {
    let matches = clap::Command::new(APP_NAME)
        .version(clap::crate_version!())
        .author("Gris Ge <fge@redhat.com>")
        .about("Network Auto-Configure using LLDP information")
        .arg(
            clap::Arg::new("DRY_RUN")
                .long("dry-run")
                .short('d')
                .takes_value(false)
                .help(
                    "Generate the network state that is going to be \
                    applied and print it out without applying any change.",
                ),
        )
        .arg(
            clap::Arg::new("ONLY")
                .index(1)
                .help("Use only the specified NICs (comma-separated)"),
        )
        .get_matches_from(argv);
    let mut log_builder = env_logger::Builder::new();

    log_builder.filter(Some("autoconf"), log::LevelFilter::Info);
    log_builder.filter(Some("nmstate"), log::LevelFilter::Info);
    log_builder.filter(Some("nm_dbus"), log::LevelFilter::Info);
    log_builder.init();

    let mut cur_state = NetworkState::new();
    if let Err(e) = cur_state.retrieve() {
        print_error_and_exit(e.into(), EXIT_FAILURE);
    }
    if let Err(e) = filter_net_state(&mut cur_state, matches.value_of("ONLY")) {
        print_error_and_exit(e, EXIT_FAILURE);
    }

    let vlan_to_iface = get_lldp_vlans(&cur_state);

    let desire_state = gen_desire_state(&vlan_to_iface);

    if matches.is_present("DRY_RUN") {
        print_result_and_exit(
            serde_yaml::to_string(&desire_state).map_err(|e| e.into()),
            EXIT_FAILURE,
        );
    } else {
        eprintln!("This is a experimental function!");
        if let Err(e) = desire_state.apply() {
            print_error_and_exit(e.into(), EXIT_FAILURE);
        } else {
            print_result_and_exit(
                serde_yaml::to_string(&desire_state).map_err(|e| e.into()),
                EXIT_FAILURE,
            );
        }
    }
}

fn filter_net_state(
    net_state: &mut NetworkState,
    filters: Option<&str>,
) -> Result<(), CliError> {
    if let Some(filters) = filters {
        let mut new_ifaces = Interfaces::new();
        for filter in filters.split(',') {
            if let Some(iface) = net_state
                .interfaces
                .get_iface(filter, InterfaceType::Unknown)
            {
                new_ifaces.push(iface.clone());
            } else {
                return Err(CliError {
                    msg: format!("Interface {} not found", filter),
                });
            }
        }
        net_state.interfaces = new_ifaces;
    }
    Ok(())
}

// Use T instead of String where T has Serialize
fn print_result_and_exit(result: Result<String, CliError>, errno: i32) {
    match result {
        Ok(s) => print_string_and_exit(s),
        Err(e) => print_error_and_exit(e, errno),
    }
}

fn print_error_and_exit(e: CliError, errno: i32) {
    eprintln!("{}", e);
    std::process::exit(errno);
}

fn print_string_and_exit(s: String) {
    println!("{}", s);
    std::process::exit(0);
}

// Return HashMap:
//  key:  (vlan_id, vlan_name)
//  value: Vec<interface_name>
fn get_lldp_vlans(net_state: &NetworkState) -> HashMap<(u32, &str), Vec<&str>> {
    let mut ret: HashMap<(u32, &str), Vec<&str>> = HashMap::new();
    for iface in net_state.interfaces.to_vec() {
        if let Some(lldp_neighbors) = iface
            .base_iface()
            .lldp
            .as_ref()
            .map(|l| l.neighbors.as_slice())
        {
            if lldp_neighbors.is_empty() {
                continue;
            }
            for lldp_tlvs in lldp_neighbors {
                for lldp_tlv in lldp_tlvs {
                    if let LldpNeighborTlv::Ieee8021Vlans(lldp_vlans) = lldp_tlv
                    {
                        for lldp_vlan in &lldp_vlans.0 {
                            match ret
                                .entry((lldp_vlan.vid, lldp_vlan.name.as_str()))
                            {
                                Entry::Occupied(o) => {
                                    o.into_mut().push(iface.name());
                                }
                                Entry::Vacant(v) => {
                                    v.insert(vec![iface.name()]);
                                }
                            };
                        }
                    }
                }
            }
        }
    }
    ret
}

fn gen_desire_state(
    lldp_vlan_info: &HashMap<(u32, &str), Vec<&str>>,
) -> NetworkState {
    let mut ret = NetworkState::new();
    for ((vid, name), ifaces) in lldp_vlan_info.iter() {
        if ifaces.len() > 1 {
            let bond_iface_name = format!("{}{}", BOND_PREFIX, vid);
            ret.append_interface_data(gen_bond_iface(&bond_iface_name, ifaces));
            ret.append_interface_data(gen_vlan_iface(
                name,
                *vid,
                &bond_iface_name,
            ));
        } else if let Some(iface) = ifaces.first() {
            ret.append_interface_data(gen_vlan_iface(name, *vid, iface));
        }
    }
    ret
}

fn gen_bond_iface(bond_name: &str, ifaces: &[&str]) -> Interface {
    let mut base_iface = BaseInterface::new();
    base_iface.name = bond_name.to_string();
    base_iface.iface_type = InterfaceType::Bond;
    base_iface.state = InterfaceState::Up;
    let mut bond_conf = BondConfig::new();
    bond_conf.mode = Some(BondMode::RoundRobin);
    bond_conf.port = Some(ifaces.iter().map(|i| i.to_string()).collect());
    let mut bond_iface = BondInterface::new();
    bond_iface.base = base_iface;
    bond_iface.bond = Some(bond_conf);
    Interface::Bond(bond_iface)
}

fn gen_vlan_iface(vlan_name: &str, id: u32, parent: &str) -> Interface {
    let mut base_iface = BaseInterface::new();
    base_iface.name = vlan_name.to_string();
    base_iface.iface_type = InterfaceType::Vlan;
    base_iface.state = InterfaceState::Up;
    let mut vlan_conf = VlanConfig::default();
    vlan_conf.base_iface = parent.to_string();
    vlan_conf.id = id.try_into().unwrap();
    let mut vlan_iface = VlanInterface::new();
    vlan_iface.base = base_iface;
    vlan_iface.vlan = Some(vlan_conf);
    Interface::Vlan(vlan_iface)
}
