use anyhow::Result;
use serde::Deserialize;

use crate::transport::{JsonOutcome, Transport};
use crate::types::{Status, TakenInfo};

pub const DEFAULT_TLDS: &[&str] = &["io", "dev", "com", "app"];

#[derive(Deserialize)]
struct Response {
    entities: Option<Vec<Entity>>,
}

#[derive(Deserialize)]
struct Entity {
    roles: Option<Vec<String>>,
    #[serde(rename = "vcardArray")]
    vcard_array: Option<serde_json::Value>,
}

pub async fn check<T: Transport>(transport: &T, name: &str, tld: &str) -> Result<Status> {
    let domain = format!("{name}.{tld}");
    for base in endpoints_for(tld) {
        let url = format!("{base}{domain}");
        let outcome: JsonOutcome<Response> = transport.fetch_json(&url).await?;
        match outcome {
            JsonOutcome::NotFound => return Ok(Status::Free),
            JsonOutcome::Found(body) => {
                let registrar = body.entities.as_deref().and_then(extract_registrar);
                return Ok(Status::Taken(Box::new(TakenInfo {
                    owner: registrar,
                    description: Some(format!("{domain} is registered")),
                    ..Default::default()
                })));
            }
            JsonOutcome::Other(_) => continue,
        }
    }
    Ok(Status::Unknown {
        reason: format!("no authoritative RDAP server answered for .{tld}"),
    })
}

fn endpoints_for(tld: &str) -> Vec<&'static str> {
    match tld {
        "io" => vec![
            "https://rdap.identitydigital.services/rdap/domain/",
            "https://rdap.nic.io/domain/",
            "https://rdap.org/domain/",
        ],
        "dev" | "app" | "page" | "new" | "foo" | "day" | "how" | "soy" => vec![
            "https://pubapi.registry.google/rdap/domain/",
            "https://rdap.org/domain/",
        ],
        "com" | "net" => vec![
            "https://rdap.verisign.com/com/v1/domain/",
            "https://rdap.org/domain/",
        ],
        "org" => vec![
            "https://rdap.publicinterestregistry.org/rdap/domain/",
            "https://rdap.org/domain/",
        ],
        "co" => vec!["https://rdap.nic.co/domain/", "https://rdap.org/domain/"],
        "ai" => vec![
            "https://rdap.identitydigital.services/rdap/domain/",
            "https://rdap.org/domain/",
        ],
        _ => vec!["https://rdap.org/domain/"],
    }
}

fn extract_registrar(entities: &[Entity]) -> Option<String> {
    let registrar = entities.iter().find(|e| {
        e.roles
            .as_ref()
            .is_some_and(|r| r.iter().any(|role| role == "registrar"))
    })?;
    let vcard = registrar
        .vcard_array
        .as_ref()?
        .as_array()?
        .get(1)?
        .as_array()?;
    for entry in vcard {
        let Some(arr) = entry.as_array() else {
            continue;
        };
        if arr.first().and_then(|v| v.as_str()) == Some("fn")
            && let Some(value) = arr.get(3).and_then(|v| v.as_str())
        {
            return Some(value.to_string());
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::transport::test_support::{Canned, MockTransport};

    fn url(tld: &str, base: &str, name: &str) -> String {
        format!("{base}{name}.{tld}")
    }

    #[test]
    fn io_tld_prefers_identity_digital() {
        let endpoints = endpoints_for("io");
        assert_eq!(
            endpoints.first().copied(),
            Some("https://rdap.identitydigital.services/rdap/domain/")
        );
    }

    #[test]
    fn unknown_tld_falls_back_to_rdap_org() {
        let endpoints = endpoints_for("zzz");
        assert_eq!(endpoints, vec!["https://rdap.org/domain/"]);
    }

    #[test]
    fn every_default_tld_has_an_authoritative_endpoint() {
        for tld in DEFAULT_TLDS {
            let endpoints = endpoints_for(tld);
            assert!(
                !endpoints[0].contains("rdap.org"),
                "default .{tld} should not start at rdap.org"
            );
        }
    }

    #[tokio::test]
    async fn returns_free_when_first_endpoint_returns_404() {
        let tld = "io";
        let primary = endpoints_for(tld)[0];
        let mock = MockTransport::new().json(&url(tld, primary, "freeexample"), Canned::NotFound);
        let status = check(&mock, "freeexample", tld).await.unwrap();
        assert!(matches!(status, Status::Free));
    }

    #[tokio::test]
    async fn falls_through_on_429_then_resolves_at_fallback() {
        let tld = "io";
        let endpoints = endpoints_for(tld);
        assert!(endpoints.len() >= 2, "test requires fallback endpoints");
        let mock = MockTransport::new()
            .json(&url(tld, endpoints[0], "takenexample"), Canned::Other(429))
            .json(
                &url(tld, endpoints[1], "takenexample"),
                Canned::Json(serde_json::json!({ "entities": [] })),
            );
        let status = check(&mock, "takenexample", tld).await.unwrap();
        assert!(matches!(status, Status::Taken(_)));
    }

    #[tokio::test]
    async fn returns_unknown_when_all_endpoints_fall_through() {
        let tld = "io";
        let endpoints = endpoints_for(tld);
        let mut mock = MockTransport::new();
        for ep in &endpoints {
            mock = mock.json(&url(tld, ep, "noresp"), Canned::Other(500));
        }
        let status = check(&mock, "noresp", tld).await.unwrap();
        assert!(matches!(status, Status::Unknown { .. }));
    }
}
