// SPDX-License-Identifier: Apache-2.0

use std::collections::HashMap;
use std::convert::TryFrom;

use log::warn;

use serde::Deserialize;

use super::super::{
    connection::dns::{
        nm_ip_dns_options_to_value, nm_ip_dns_search_to_value,
        nm_ip_dns_to_value, parse_nm_dns, parse_nm_dns_data,
        parse_nm_dns_options, parse_nm_dns_search,
    },
    connection::route::{
        nm_ip_routes_to_value, parse_nm_ip_route_data, NmIpRoute,
    },
    connection::route_rule::{
        nm_ip_rules_to_value, parse_nm_ip_rule_data, NmIpRouteRule,
    },
    connection::DbusDictionary,
    ErrorKind, NmError, ToDbusValue,
};

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(try_from = "zvariant::OwnedValue")]
#[non_exhaustive]
pub enum NmSettingIpMethod {
    Auto,
    Disabled,
    LinkLocal,
    Manual,
    Shared,
    Dhcp,   // IPv6 only,
    Ignore, // Ipv6 only,
}

impl Default for NmSettingIpMethod {
    fn default() -> Self {
        Self::Auto
    }
}

impl std::fmt::Display for NmSettingIpMethod {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::Auto => "auto",
                Self::Disabled => "disabled",
                Self::LinkLocal => "link-local",
                Self::Manual => "manual",
                Self::Shared => "shared",
                Self::Dhcp => "dhcp",
                Self::Ignore => "ignore",
            }
        )
    }
}

impl TryFrom<zvariant::OwnedValue> for NmSettingIpMethod {
    type Error = NmError;
    fn try_from(value: zvariant::OwnedValue) -> Result<Self, Self::Error> {
        let str_value = String::try_from(value)?;
        match str_value.as_str() {
            "auto" => Ok(Self::Auto),
            "disabled" => Ok(Self::Disabled),
            "link-local" => Ok(Self::LinkLocal),
            "manual" => Ok(Self::Manual),
            "shared" => Ok(Self::Shared),
            "dhcp" => Ok(Self::Dhcp),
            "ignore" => Ok(Self::Ignore),
            _ => Err(NmError::new(
                ErrorKind::InvalidArgument,
                format!("Invalid IP method {str_value}"),
            )),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Default, Deserialize)]
#[serde(try_from = "DbusDictionary")]
#[non_exhaustive]
pub struct NmSettingIp {
    pub method: Option<NmSettingIpMethod>,
    pub addresses: Vec<String>,
    pub routes: Vec<NmIpRoute>,
    pub route_rules: Vec<NmIpRouteRule>,
    pub dns_priority: Option<i32>,
    pub dns_search: Option<Vec<String>>,
    pub dns: Option<Vec<String>>,
    pub dns_options: Option<Vec<String>>,
    pub ignore_auto_dns: Option<bool>,
    pub never_default: Option<bool>,
    pub ignore_auto_routes: Option<bool>,
    pub route_table: Option<u32>,
    pub dhcp_client_id: Option<String>,
    pub dhcp_timeout: Option<i32>,
    pub gateway: Option<String>,
    pub may_fail: Option<bool>,
    pub route_metric: Option<i64>,
    // IPv6 only
    pub ra_timeout: Option<i32>,
    // IPv6 only
    pub addr_gen_mode: Option<i32>,
    // IPv6 only
    pub dhcp_duid: Option<String>,
    // IPv6 only
    pub dhcp_iaid: Option<String>,
    // IPv6 only
    pub token: Option<String>,
    pub dhcp_send_hostname: Option<bool>,
    pub dhcp_fqdn: Option<String>,
    pub dhcp_hostname: Option<String>,
    _other: HashMap<String, zvariant::OwnedValue>,
}

impl TryFrom<DbusDictionary> for NmSettingIp {
    type Error = NmError;
    fn try_from(mut v: DbusDictionary) -> Result<Self, Self::Error> {
        let mut setting = Self {
            method: _from_map!(v, "method", NmSettingIpMethod::try_from)?,
            addresses: _from_map!(v, "address-data", parse_nm_ip_address_data)?
                .unwrap_or_default(),
            routes: _from_map!(v, "route-data", parse_nm_ip_route_data)?
                .unwrap_or_default(),
            route_rules: _from_map!(v, "routing-rules", parse_nm_ip_rule_data)?
                .unwrap_or_default(),
            dns_search: _from_map!(v, "dns-search", parse_nm_dns_search)?,
            dns_priority: _from_map!(v, "dns-priority", i32::try_from)?,
            dns_options: _from_map!(v, "dns-options", parse_nm_dns_options)?,
            ignore_auto_dns: _from_map!(v, "ignore-auto-dns", bool::try_from)?,
            never_default: _from_map!(v, "never-default", bool::try_from)?,
            ignore_auto_routes: _from_map!(
                v,
                "ignore-auto-routes",
                bool::try_from
            )?,
            dhcp_client_id: _from_map!(v, "dhcp-client-id", String::try_from)?,
            dhcp_timeout: _from_map!(v, "dhcp-timeout", i32::try_from)?,
            ra_timeout: _from_map!(v, "ra-timeout", i32::try_from)?,
            addr_gen_mode: _from_map!(v, "addr-gen-mode", i32::try_from)?,
            dhcp_duid: _from_map!(v, "dhcp-duid", String::try_from)?,
            dhcp_iaid: _from_map!(v, "dhcp-iaid", String::try_from)?,
            route_table: _from_map!(v, "route-table", u32::try_from)?,
            gateway: _from_map!(v, "gateway", String::try_from)?,
            may_fail: _from_map!(v, "may-fail", bool::try_from)?,
            route_metric: _from_map!(v, "route-metric", i64::try_from)?,
            token: _from_map!(v, "token", String::try_from)?,
            dhcp_send_hostname: _from_map!(
                v,
                "dhcp-send-hostname",
                bool::try_from
            )?,
            dhcp_fqdn: _from_map!(v, "dhcp-fqdn", String::try_from)?,
            dhcp_hostname: _from_map!(v, "dhcp-hostname", String::try_from)?,
            ..Default::default()
        };

        if v.contains_key("dns-data") {
            setting.dns = _from_map!(v, "dns-data", parse_nm_dns_data)?;
            // NM 1.41 deprecated `dns` property in the favor of `dns-data`
            v.remove("dns");
        } else {
            setting.dns = _from_map!(v, "dns", parse_nm_dns)?;
        }

        // NM deprecated `addresses` property in the favor of `addresss-data`
        v.remove("addresses");
        // NM deprecated `routes` property in the favor of `routes-data`
        v.remove("routes");
        setting._other = v;
        Ok(setting)
    }
}

impl ToDbusValue for NmSettingIp {
    fn to_value(&self) -> Result<HashMap<&str, zvariant::Value>, NmError> {
        let mut ret = HashMap::new();
        if let Some(v) = &self.method {
            ret.insert("method", zvariant::Value::new(format!("{v}")));
        }
        let mut addresss_data = zvariant::Array::new(
            zvariant::Signature::from_str_unchecked("a{sv}"),
        );
        for addr_str in &self.addresses {
            let addr_str_split: Vec<&str> = addr_str.split('/').collect();
            if addr_str_split.len() != 2 {
                return Err(NmError::new(
                    ErrorKind::InvalidArgument,
                    format!("Invalid IP address {addr_str}"),
                ));
            }
            let prefix = addr_str_split[1].parse::<u32>().map_err(|e| {
                NmError::new(
                    ErrorKind::InvalidArgument,
                    format!(
                        "Invalid IP address prefix {}: {e}",
                        addr_str_split[1]
                    ),
                )
            })?;
            let mut addr_dict = zvariant::Dict::new(
                zvariant::Signature::from_str_unchecked("s"),
                zvariant::Signature::from_str_unchecked("v"),
            );
            addr_dict.append(
                zvariant::Value::new("address".to_string()),
                zvariant::Value::Value(Box::new(zvariant::Value::new(
                    addr_str_split[0].to_string(),
                ))),
            )?;
            addr_dict.append(
                zvariant::Value::new("prefix".to_string()),
                zvariant::Value::Value(Box::new(zvariant::Value::U32(prefix))),
            )?;
            addresss_data.append(zvariant::Value::Dict(addr_dict))?;
        }
        ret.insert("address-data", zvariant::Value::Array(addresss_data));
        ret.insert("route-data", nm_ip_routes_to_value(&self.routes)?);
        ret.insert("routing-rules", nm_ip_rules_to_value(&self.route_rules)?);
        if let Some(dns_servers) = self.dns.as_ref() {
            if !dns_servers.is_empty() {
                // We still use the `dns` instead of `dns-data` as the
                // `dns-data` is only supported by NM 1.41+ which is not widely
                // available yet. And we do not know the NM version yet in this
                // function context.
                ret.insert("dns", nm_ip_dns_to_value(dns_servers)?);
            }
        }
        if let Some(dns_searches) = self.dns_search.as_ref() {
            ret.insert("dns-search", nm_ip_dns_search_to_value(dns_searches)?);
        }
        if let Some(dns_options) = self.dns_options.as_ref() {
            ret.insert("dns-options", nm_ip_dns_options_to_value(dns_options)?);
        }
        if let Some(dns_priority) = self.dns_priority {
            ret.insert("dns-priority", zvariant::Value::new(dns_priority));
        }
        if let Some(v) = self.ignore_auto_dns {
            ret.insert("ignore-auto-dns", zvariant::Value::new(v));
        }
        if let Some(v) = self.ignore_auto_routes {
            ret.insert("ignore-auto-routes", zvariant::Value::new(v));
        }
        if let Some(v) = self.never_default {
            ret.insert("never-default", zvariant::Value::new(v));
        }
        if let Some(v) = &self.dhcp_client_id {
            ret.insert("dhcp-client-id", zvariant::Value::new(v));
        }
        if let Some(v) = self.dhcp_timeout {
            ret.insert("dhcp-timeout", zvariant::Value::new(v));
        }
        if let Some(v) = self.ra_timeout {
            ret.insert("ra-timeout", zvariant::Value::new(v));
        }
        if let Some(v) = self.addr_gen_mode {
            ret.insert("addr-gen-mode", zvariant::Value::new(v));
        }
        if let Some(v) = &self.dhcp_duid {
            ret.insert("dhcp-duid", zvariant::Value::new(v));
        }
        if let Some(v) = &self.dhcp_iaid {
            ret.insert("dhcp-iaid", zvariant::Value::new(v));
        }
        if let Some(v) = &self.route_table {
            ret.insert("route-table", zvariant::Value::new(v));
        }
        if let Some(v) = &self.gateway {
            ret.insert("gateway", zvariant::Value::new(v));
        }
        if let Some(v) = &self.may_fail {
            ret.insert("may-fail", zvariant::Value::new(v));
        }
        if let Some(v) = &self.route_metric {
            ret.insert("route-metric", zvariant::Value::new(v));
        }
        if let Some(v) = &self.token {
            ret.insert("token", zvariant::Value::new(v));
        }
        if let Some(v) = &self.dhcp_send_hostname {
            ret.insert("dhcp-send-hostname", zvariant::Value::new(v));
        }
        if let Some(v) = &self.dhcp_fqdn {
            ret.insert("dhcp-fqdn", zvariant::Value::new(v));
        }
        if let Some(v) = &self.dhcp_hostname {
            ret.insert("dhcp-hostname", zvariant::Value::new(v));
        }
        ret.extend(self._other.iter().map(|(key, value)| {
            (key.as_str(), zvariant::Value::from(value.clone()))
        }));
        Ok(ret)
    }
}

fn parse_nm_ip_address_data(
    value: zvariant::OwnedValue,
) -> Result<Vec<String>, NmError> {
    let mut addresses = Vec::new();
    for nm_addr in <Vec<zvariant::OwnedValue>>::try_from(value)? {
        let nm_addr_display = format!("{nm_addr:?}");
        let mut nm_addr =
            match <HashMap<String, zvariant::OwnedValue>>::try_from(nm_addr) {
                Ok(a) => a,
                Err(e) => {
                    warn!(
                        "Failed to convert {} to HashMap: {}",
                        nm_addr_display, e
                    );
                    continue;
                }
            };
        let address = if let Some(a) = nm_addr
            .remove("address")
            .and_then(|a| String::try_from(a).ok())
        {
            a
        } else {
            warn!("Failed to find address property from {:?}", nm_addr);

            continue;
        };
        let prefix = if let Some(a) =
            nm_addr.remove("prefix").and_then(|a| u32::try_from(a).ok())
        {
            a
        } else {
            warn!("Failed to find address property from {:?}", nm_addr);

            continue;
        };
        addresses.push(format!("{address}/{prefix}"));
    }
    Ok(addresses)
}
