use std::{collections::HashSet, process::exit};

use anyhow::{Context, Error, Result};
use clap::{Parser, ValueEnum};
use interface::{InterfaceCreationError, InterfaceInfo, MacAddress};
use nix::{
  ifaddrs::{InterfaceAddress, getifaddrs},
  net::if_::if_indextoname,
};

mod interface;

fn main() -> Result<(), Error> {
  let args = Args::parse();

  let interfaces = getifaddrs().with_context(|| "Unable to get address information from system")?;

  let mut bad_user_ifs = false;
  let ifnames = args.ifname.or_else(|| {
    args.ifindex.and_then(|ifindices| {
      let result = collect_ifnames(&ifindices);
      bad_user_ifs |= result.had_errors;
      (!result.names.is_empty()).then_some(result.names)
    })
  });

  let interfaces_to_report: Vec<InterfaceAddress> = interfaces
    // Filter the list by the interfaces the user specified, if any
    .filter(|interface| ifnames.as_ref().is_none_or(|names| names.contains(&interface.interface_name)))
    // Filter the list down to entries which are a packet interface
    .filter(|interface| interface.address.is_some_and(|addr| addr.as_link_addr().is_some()))
    .collect();
  let report_names: HashSet<&str> = interfaces_to_report.iter().map(|addr| addr.interface_name.as_str()).collect();

  // Make sure to report it to the user if not all the ifnames they want to collect are present
  if let Some(names) = &ifnames {
    let missing_names: Vec<&String> = names.iter().filter(|name| !report_names.contains(name.as_str())).collect();
    if !missing_names.is_empty() {
      eprintln!("Failed to find interfaces: {missing_names:?}");
      bad_user_ifs = true;
    }
  }

  // Actually gather data on interfaces
  let interface_results: Vec<Result<InterfaceInfo, InterfaceCreationError>> =
    interfaces_to_report.iter().map(TryInto::<InterfaceInfo>::try_into).collect();
  let mut ifdata_failed = false;
  for result in &interface_results {
    if let Err(e) = result {
      eprintln!("Failed to get interface data for {}: {:?}", e.ifname, e.err);
      ifdata_failed = true;
    }
  }

  let interface_data: Vec<InterfaceInfo> = interface_results.into_iter().filter_map(Result::ok).collect();
  match args.output.unwrap_or_default() {
    OutputFormat::Json => {
      println!("{}", serde_json::to_string(&interface_data)?);
    }
    OutputFormat::Text => {
      let mac_default = MacAddress::default();
      for interface in &interface_data {
        println!("{} - {}: <{}> mtu {}", interface.ifindex, interface.ifname, interface.flags, interface.mtu);
        println!(
          "    link {} broadcast {}",
          interface.mac_address.as_ref().unwrap_or(&mac_default),
          interface.broadcast.as_ref().unwrap_or(&mac_default)
        );
        for addr in &interface.addr_info {
          print!("    {} {}/{}", addr.family, addr.address, addr.prefixlen);
          if let Some(broadcast) = addr.broadcast {
            print!(" broadcast {broadcast}");
          }
          println!(" scope {}", addr.scope);
        }
      }
    }
  }

  if bad_user_ifs {
    exit(65); // EX_DATAERR
  } else if ifdata_failed {
    exit(71); // EX_OSERR
  }

  Ok(())
}

fn collect_ifnames(ifindices: &[u32]) -> IfNamesResult {
  let mut names = Vec::with_capacity(ifindices.len());
  let mut failed_indices = Vec::with_capacity(ifindices.len());
  for &index in ifindices {
    match index_to_name(index) {
      Some(name) => names.push(name),
      None => failed_indices.push(index),
    }
  }
  IfNamesResult { names, had_errors: !failed_indices.is_empty() }
}

fn index_to_name(index: u32) -> Option<String> {
  let cstr = match if_indextoname(index) {
    Ok(name) => name,
    Err(e) => {
      eprintln!("Failed to find interface with index {index}: {e:?}");
      return None;
    }
  };

  let name = match cstr.into_string() {
    Ok(name) => name,
    Err(e) => {
      eprintln!("Invalid UTF-8 in name of interface {index}: {:?}", e.into_cstring());
      return None;
    }
  };

  if name.is_empty() {
    eprintln!("Failed to find interface with index {index}: does not exist");
    return None;
  }

  Some(name)
}

struct IfNamesResult {
  names: Vec<String>,
  had_errors: bool,
}

/// Retrieves interface data from the system and returns as text or structured data. By default, searches for all
/// interfaces and outputs JSON.
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Args {
  /// Output format for interface data. Default: json
  #[arg(short, long)]
  output: Option<OutputFormat>,

  /// ifname to search for, exclusive with ifindex. Can be passed multiple times. Default: all interfaces
  #[arg(long, group = "ifparams")]
  ifname: Option<Vec<String>>,

  /// ifindex to search for, exclusive with ifname. Can be passed multiple times. Default: all interfaces
  #[arg(long, group = "ifparams")]
  ifindex: Option<Vec<u32>>,
}

#[derive(Clone, Debug, ValueEnum)]
enum OutputFormat {
  /// JSON output
  Json,
  /// Plain text output
  Text,
}

impl Default for OutputFormat {
  fn default() -> Self {
    Self::Json
  }
}
