extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use serde_yaml::Value;
use syn::LitStr;
use syn::parse_macro_input;

#[proc_macro]
pub fn ip(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as LitStr);
    let ip_str = input.value();

    if let Ok(_) = ip_str.parse::<std::net::Ipv4Addr>() {
        let expanded =
            quote! { std::net::IpAddr::V4(#ip_str.parse::<std::net::Ipv4Addr>().unwrap()) };
        return TokenStream::from(expanded);
    }

    if let Ok(_) = ip_str.parse::<std::net::Ipv6Addr>() {
        let expanded =
            quote! { std::net::IpAddr::V6(#ip_str.parse::<std::net::Ipv6Addr>().unwrap()) };
        return TokenStream::from(expanded);
    }

    panic!("Invalid IP address: {}", ip_str);
}

#[proc_macro]
pub fn ipv4(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as LitStr);
    let ip_str = input.value();

    if let Ok(_) = ip_str.parse::<std::net::Ipv4Addr>() {
        let expanded = quote! { #ip_str.parse::<std::net::Ipv4Addr>().unwrap() };
        return TokenStream::from(expanded);
    }

    panic!("Invalid IPv4 address: {}", ip_str);
}

#[proc_macro]
pub fn mac_address(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as LitStr);
    let mac_str = input.value();

    match parse_mac_address(&mac_str) {
        Ok(bytes) => {
            let b0 = bytes[0];
            let b1 = bytes[1];
            let b2 = bytes[2];
            let b3 = bytes[3];
            let b4 = bytes[4];
            let b5 = bytes[5];

            quote! {
            harmony_types::net::MacAddress( [#b0, #b1, #b2, #b3, #b4, #b5] )
            }
            .into()
        }
        Err(err) => syn::Error::new(input.span(), err).to_compile_error().into(),
    }
}

fn parse_mac_address(mac: &str) -> Result<[u8; 6], String> {
    let parts: Vec<&str> = mac.split(':').collect();
    if parts.len() != 6 {
        return Err("MAC address must contain exactly six octets separated by colons".to_string());
    }

    let mut bytes = [0u8; 6];
    for (i, part) in parts.iter().enumerate() {
        match u8::from_str_radix(part, 16) {
            Ok(byte) => bytes[i] = byte,
            Err(_) => return Err(format!("Invalid MAC address octet: {}", part)),
        }
    }

    Ok(bytes)
}

/// Verify that input is valid yaml by trying to deserialize it using
/// serde_yaml::from_str::<serde_yaml::value::Value>(input)
///
/// panics: If yaml is not valid
#[proc_macro]
pub fn yaml(input: TokenStream) -> TokenStream {
    // TODO, accept a second argument that is the type to be deserialized to and validate at
    // compile-time that deserialization is possible
    //
    // It does not seem to be doable with the way macros are designed : we may pass an ident to the
    // macro, but the macro only has access to the ident, not the type itself.
    //
    // I also tried to create a restricted version of this macro, but this time serde got in the
    // way : my use case is to make sure that the yaml I am given can deserialize strictly (with
    // deny_unknown_attributes option on) to k8s-openapi types. But the k8s-openapi types are not
    // annotated with deny_unknown_attributes and there does not seem to be a way to tell serde to
    // deserialize in "strict mode" when calling the deserialization function itself. I tried
    // wrapping the types in something like :
    //
    // ```rust
    // #[derive(Deserialize, Debug)]
    // #[serde(deny_unknown_fields)]
    // struct Strict<T>(pub T);
    // ```
    //
    // But it still does actually deserialize T strictly. I gave up for now at this point. Will
    // find a solution some day!

    let yaml = parse_macro_input!(input as LitStr);

    serde_yaml::from_str::<Value>(yaml.value().as_str()).expect("Should be valid yaml");

    quote! {
        serde_yaml::from_str(#yaml)
    }
    .into()
}
