//! Outputs RDAP to RPSL.

use std::{str::FromStr, sync::LazyLock};

use chrono::DateTime;
use icann_rdap_common::{
    httpdata::HttpData,
    prelude::{Entity, Event, Notice, ObjectCommonFields, PublicId, RdapResponse, Remark},
};
use strum::IntoEnumIterator;
use strum::{EnumIter, EnumString};

use strum::Display as EnumDisplay;

pub mod autnum;
pub mod domain;
pub mod entity;
pub mod error;
pub mod help;
pub mod nameserver;
pub mod network;
pub mod search;
pub mod ttl;

pub(crate) const _LINE_LENGTH: usize = 78;

#[derive(Clone, Copy)]
pub struct RpslParams<'a> {
    pub http_data: &'a HttpData,
}

pub trait ToRpsl {
    fn to_rpsl(&self, param: RpslParams) -> String;
}

impl ToRpsl for RdapResponse {
    fn to_rpsl(&self, params: RpslParams) -> String {
        let mut rpsl = String::from_str("\n").unwrap();
        if let Some(query_uri) = params.http_data.request_uri() {
            let received = params.http_data.received().to_rfc2822();
            rpsl.push_str("# RPSL generated by ICANN RDAP Client\n");
            rpsl.push_str("# Query\n");
            rpsl.push_str(&format!("# {query_uri}\n"));
            rpsl.push_str("# Received\n");
            rpsl.push_str(&format!("# {received}\n\n"));
        }
        let variant_rpsl = match &self {
            Self::Entity(entity) => entity.to_rpsl(params),
            Self::Domain(domain) => domain.to_rpsl(params),
            Self::Nameserver(nameserver) => nameserver.to_rpsl(params),
            Self::Autnum(autnum) => autnum.to_rpsl(params),
            Self::Network(network) => network.to_rpsl(params),
            Self::DomainSearchResults(results) => results.to_rpsl(params),
            Self::EntitySearchResults(results) => results.to_rpsl(params),
            Self::NameserverSearchResults(results) => results.to_rpsl(params),
            Self::IpSearchResults(results) => results.to_rpsl(params),
            Self::AutnumSearchResults(results) => results.to_rpsl(params),
            Self::ErrorResponse(error) => error.to_rpsl(params),
            Self::Help(help) => help.to_rpsl(params),
        };
        rpsl.push_str(&variant_rpsl);
        rpsl
    }
}

/// The max length of the attribute names.
pub static ATTR_NAME_LEN: LazyLock<usize> = LazyLock::new(|| {
    AttrName::iter()
        .max_by_key(|x| x.to_string().len())
        .map_or(10, |x| x.to_string().len())
        + 1
});

/// Rpsl Attribute Names
#[derive(EnumIter, EnumString, Debug, EnumDisplay, PartialEq, Eq, Clone, Copy)]
pub enum AttrName {
    #[strum(serialize = "abuse-c")]
    AbuseC,
    #[strum(serialize = "admin-c")]
    AdminC,
    #[strum(serialize = "address")]
    Address,
    #[strum(serialize = "as-name")]
    AsName,
    #[strum(serialize = "aut-num")]
    Autnum,
    #[strum(serialize = "autnum-range")]
    AutnumRange,
    #[strum(serialize = "billing-c")]
    BillingC,
    #[strum(serialize = "cidr")]
    Cidr,
    #[strum(serialize = "created")]
    Created,
    #[strum(serialize = "deleg-signed")]
    DelegationSigned,
    #[strum(serialize = "deletion")]
    Deletion,
    #[strum(serialize = "domain")]
    Domain,
    #[strum(serialize = "ds-rdata")]
    DsRdata,
    #[strum(serialize = "e-mail")]
    Email,
    #[strum(serialize = "enumexp")]
    EnumExp,
    #[strum(serialize = "error-code")]
    ErrorCode,
    #[strum(serialize = "error-desc")]
    ErrorDescription,
    #[strum(serialize = "error-title")]
    ErrorTitle,
    #[strum(serialize = "expiration")]
    Expiration,
    #[strum(serialize = "fax-no")]
    FaxNo,
    #[strum(serialize = "full-name")]
    FullName,
    #[strum(serialize = "inetnum")]
    Inetnum,
    #[strum(serialize = "inet6num")]
    Inet6num,
    #[strum(serialize = "key-rdata")]
    KeyData,
    #[strum(serialize = "last-dbupdate")]
    LastDbUpdate,
    #[strum(serialize = "last-modified")]
    LastModified,
    #[strum(serialize = "locked")]
    Locked,
    #[strum(serialize = "max-siglife")]
    MaxSigLife,
    #[strum(serialize = "mnt-by")]
    Mntby,
    #[strum(serialize = "mntner")]
    Mntner,
    #[strum(serialize = "netname")]
    NetName,
    #[strum(serialize = "nic-hdl")]
    NicHdl,
    #[strum(serialize = "noc-c")]
    NocC,
    #[strum(serialize = "notify")]
    Notify,
    #[strum(serialize = "nserver")]
    Nserver,
    #[strum(serialize = "organization")]
    Organization,
    #[strum(serialize = "org-name")]
    OrgName,
    #[strum(serialize = "other-c")]
    OtherC,
    #[strum(serialize = "other-event")]
    OtherEvent,
    #[strum(serialize = "parent-hdl")]
    ParentHandle,
    #[strum(serialize = "phone")]
    Phone,
    #[strum(serialize = "person")]
    Person,
    #[strum(serialize = "public-id")]
    PublicId,
    #[strum(serialize = "recreated")]
    Recreated,
    #[strum(serialize = "registrant")]
    Registrant,
    #[strum(serialize = "registrar")]
    Registrar,
    #[strum(serialize = "regrexp")]
    RegrExp,
    #[strum(serialize = "remarks")]
    Remarks,
    #[strum(serialize = "role")]
    Role,
    #[strum(serialize = "status")]
    Status,
    #[strum(serialize = "source")]
    Source,
    #[strum(serialize = "tech-c")]
    TechC,
    #[strum(serialize = "transfer")]
    Transfer,
    #[strum(serialize = "dns-ttl")]
    Ttl,
    #[strum(serialize = "type")]
    Type,
    #[strum(serialize = "unlocked")]
    Unlocked,
    #[strum(serialize = "variant")]
    Variant,
    #[strum(serialize = "zone-signed")]
    ZoneSigned,
}

/// Trait to get the RPSL object key reference.
///
/// An RPSL object key reference is the attribute/value pair that identifies
/// the type of RPSL object/role and the specific instance.
///
/// This is not a Rust reference.
pub trait KeyRef {
    /// Returns key attribute name and value.
    fn key_ref(&self, params: RpslParams) -> (AttrName, String);
}

/// Takes the string to push a rpsl attribute with an optional value that is mandatory.
///
/// Returns that string back, so ownership is temporary.
pub fn and_push_mandatory_attribute(
    mut rpsl: String,
    name: AttrName,
    value: Option<&str>,
    default: &str,
) -> String {
    let attr_val = value.unwrap_or(default);
    rpsl = push_mandatory_attribute(rpsl, name, attr_val);
    rpsl
}

/// Takes the string to push an rpsl attribute only if a value is provided.
pub fn push_optional_attribute(mut rpsl: String, name: AttrName, value: Option<&str>) -> String {
    if let Some(str) = value {
        rpsl = push_mandatory_attribute(rpsl, name, str);
    }
    rpsl
}

/// Takes the string to push a rpsl attribute that is mandatory.
///
/// Returns that string back, so ownership is temporary.
pub fn push_mandatory_attribute(mut rpsl: String, name: AttrName, value: &str) -> String {
    let attr_name = format!("{name}:");
    let value = if value.is_empty() {
        "EMPTY VALUE"
    } else {
        value
    };
    let str = format!("{attr_name:<ATTR_NAME_LEN$}  {value}\n");
    rpsl.push_str(&str);
    rpsl
}

/// Pushes those items all object classes have in common
pub fn push_obj_common<T: ObjectCommonFields>(
    mut rpsl: String,
    params: RpslParams,
    obj: &T,
) -> String {
    // handle
    rpsl = push_handle(rpsl, obj.handle());

    // status
    rpsl = push_status(rpsl, obj.status());

    // entity refs
    rpsl = push_entity_refs(rpsl, obj.entities());

    // push events
    rpsl = push_events(rpsl, obj.events());

    // push remarks
    rpsl = push_remarks(rpsl, obj.remarks());

    //end
    rpsl = push_source(rpsl, params);
    rpsl
}

pub fn push_source(mut rpsl: String, params: RpslParams) -> String {
    rpsl = push_mandatory_attribute(rpsl, AttrName::Source, params.http_data.host());
    rpsl
}

pub fn push_entities(mut rpsl: String, entities: &[Entity], params: RpslParams) -> String {
    for entity in entities {
        rpsl.push_str(&entity.to_rpsl(params));
    }
    rpsl
}

pub fn push_entity_refs(mut rpsl: String, entities: &[Entity]) -> String {
    for entity in entities {
        let key_value = entity_value(entity);
        if entity.roles().is_empty() {
            rpsl = push_mandatory_attribute(rpsl, AttrName::OtherC, &key_value);
        } else {
            for role in entity.roles() {
                let key_name = match role.to_ascii_lowercase().as_ref() {
                    "abuse" => AttrName::AbuseC,
                    "administrative" => AttrName::AdminC,
                    "billing" => AttrName::BillingC,
                    "noc" => AttrName::NocC,
                    "notifications" => AttrName::Notify,
                    "registrant" => AttrName::Registrant,
                    "registrar" => AttrName::Registrar,
                    "technical" => AttrName::TechC,
                    _ => AttrName::OtherC,
                };
                rpsl = push_mandatory_attribute(rpsl, key_name, &key_value);
            }
        }
    }
    rpsl
}

pub fn entity_value(entity: &Entity) -> String {
    let contact = entity.contact();
    entity
        .handle()
        .or_else(|| contact.as_ref().and_then(|c| c.full_name()))
        .unwrap_or("NAME/HANDLE UNAVAILABLE")
        .to_string()
}

pub fn push_notices(mut rpsl: String, notices: &[Notice]) -> String {
    for notice in notices {
        if let Some(title) = notice.title() {
            rpsl.push_str(&format!("# {title}\n"));
        }
        for line in notice.description() {
            rpsl.push_str(&format!("# {line}\n"));
        }
        for link in notice.links() {
            if let Some(href) = link.href() {
                rpsl.push_str(&format!("# {href}\n"));
            }
        }
        rpsl.push('\n');
    }
    rpsl
}

pub fn push_remarks(mut rpsl: String, remarks: &[Remark]) -> String {
    for remark in remarks {
        if let Some(title) = remark.title() {
            rpsl = push_mandatory_attribute(rpsl, AttrName::Remarks, title);
        }
        for (idx, line) in remark.description().iter().enumerate() {
            rpsl = push_mandatory_attribute(rpsl, AttrName::Remarks, &format!("{idx}. {line}"));
        }
        for link in remark.links() {
            if let Some(href) = link.href() {
                rpsl = push_mandatory_attribute(rpsl, AttrName::Remarks, href);
            }
        }
    }
    rpsl
}

pub fn push_events(mut rpsl: String, events: &[Event]) -> String {
    for event in events {
        if let Some(event_date) = event.event_date()
            && let Ok(event_date) = DateTime::parse_from_rfc3339(event_date)
        {
            let rpsl_date = event_date.format("%Y%m%d").to_string();
            let readable_date = event_date.to_rfc2822();
            let date_str = format!("{rpsl_date} ({readable_date})");
            if let Some(event_action) = event.event_action() {
                let att_name = match event_action.to_ascii_lowercase().as_str() {
                    "registration" => AttrName::Created,
                    "deletion" => AttrName::Deletion,
                    "enum validation expiration" => AttrName::EnumExp,
                    "expiration" => AttrName::Expiration,
                    "last update of rdap database" => AttrName::LastDbUpdate,
                    "last changed" => AttrName::LastModified,
                    "locked" => AttrName::Locked,
                    "reregistration" => AttrName::Recreated,
                    "registrar expiration" => AttrName::RegrExp,
                    "transfer" => AttrName::Transfer,
                    "unlocked" => AttrName::Unlocked,
                    _ => AttrName::OtherEvent,
                };
                rpsl = push_mandatory_attribute(rpsl, att_name, &date_str);
            } else {
                rpsl = push_mandatory_attribute(rpsl, AttrName::OtherEvent, &date_str);
            }
        }
    }
    rpsl
}

pub fn push_status(mut rpsl: String, status: &[String]) -> String {
    for stati in status {
        rpsl = push_mandatory_attribute(rpsl, AttrName::Status, stati);
    }
    rpsl
}

pub fn push_handle(mut rpsl: String, handle: Option<&str>) -> String {
    rpsl = and_push_mandatory_attribute(rpsl, AttrName::NicHdl, handle, "NO HANDLE");
    rpsl
}

pub fn push_public_ids(mut rpsl: String, public_ids: &[PublicId]) -> String {
    for id in public_ids {
        let Some(id_type) = id.id_type() else {
            continue;
        };
        let Some(identifier) = id.identifier() else {
            continue;
        };
        rpsl = push_mandatory_attribute(
            rpsl,
            AttrName::PublicId,
            &format!("{identifier} ({id_type})"),
        );
    }
    rpsl
}
