pub mod classify;
pub mod errors;
pub mod output;
pub mod schema;
pub mod sources;
pub mod transport;
pub mod types;
pub mod validate;

use std::pin::Pin;

use futures::stream::{FuturesUnordered, StreamExt};

use crate::sources::rdap::DEFAULT_TLDS;
use crate::transport::Transport;
use crate::types::{CheckResult, NameReport, Status};

type CheckFuture<'a> = Pin<Box<dyn std::future::Future<Output = CheckResult> + 'a>>;

pub async fn check_name<T: Transport>(transport: &T, name: &str, tlds: &[String]) -> NameReport {
    let mut results = Vec::new();
    let mut futures = build_check_futures(transport, name, tlds);
    while let Some(r) = futures.next().await {
        results.push(r);
    }
    results.sort_by_key(|r| source_order(&r.source));
    NameReport {
        name: name.to_string(),
        results,
    }
}

/// Streaming variant: invokes `on_result` for each `CheckResult` as soon as it
/// completes, in completion order. Intended for UIs that render per-source
/// progress (e.g. the wasm/web build).
pub async fn check_name_stream<T, F>(transport: &T, name: &str, tlds: &[String], mut on_result: F)
where
    T: Transport,
    F: FnMut(CheckResult),
{
    let mut futures = build_check_futures(transport, name, tlds);
    while let Some(r) = futures.next().await {
        on_result(r);
    }
}

pub fn default_tlds() -> Vec<String> {
    DEFAULT_TLDS.iter().map(|s| (*s).to_string()).collect()
}

fn build_check_futures<'a, T: Transport>(
    transport: &'a T,
    name: &'a str,
    tlds: &'a [String],
) -> FuturesUnordered<CheckFuture<'a>> {
    let futures: FuturesUnordered<CheckFuture<'a>> = FuturesUnordered::new();

    futures.push(Box::pin(wrap("crates.io", async move {
        sources::crates_io::check(transport, name).await
    })));
    futures.push(Box::pin(wrap("pypi", async move {
        sources::pypi::check(transport, name).await
    })));
    futures.push(Box::pin(wrap("npm", async move {
        sources::npm::check(transport, name).await
    })));
    futures.push(Box::pin(wrap("github", async move {
        sources::github::check(transport, name).await
    })));

    for tld in tlds {
        let source = format!(".{tld}");
        let tld = tld.clone();
        futures.push(Box::pin(async move {
            let status = match sources::rdap::check(transport, name, &tld).await {
                Ok(s) => s,
                Err(e) => Status::Unknown {
                    reason: e.to_string(),
                },
            };
            CheckResult { source, status }
        }));
    }

    futures
}

async fn wrap<F>(source: &'static str, f: F) -> CheckResult
where
    F: std::future::Future<Output = anyhow::Result<Status>>,
{
    let status = match f.await {
        Ok(s) => s,
        Err(e) => Status::Unknown {
            reason: e.to_string(),
        },
    };
    CheckResult {
        source: source.into(),
        status,
    }
}

fn source_order(source: &str) -> u8 {
    match source {
        "crates.io" => 0,
        "pypi" => 1,
        "npm" => 2,
        "github" => 3,
        s if s.starts_with('.') => 4,
        _ => 5,
    }
}
