// Copyright 2024 Saorsa Labs Ltd.
//
// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
//
// Full details available at https://saorsalabs.com/licenses

//! Validate first-party Saorsa ant-quic bootstrap nodes.
//!
//! This is intentionally separate from `test-public-endpoints`, which validates
//! generic HTTP/3 QUIC interoperability. Bootstrap validation exercises the
//! first-party ant-quic P2P path against hard-coded VPS IP literals.

use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::{Duration, Instant};

use ant_quic::{P2pConfig, P2pEndpoint};
use clap::Parser;
use serde::{Deserialize, Serialize};

#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Path to Saorsa bootstrap node YAML file.
    #[arg(short, long, default_value = "docs/saorsa-bootstrap-nodes.yaml")]
    config: PathBuf,

    /// Output file for JSON results.
    #[arg(short, long)]
    output: Option<PathBuf>,

    /// Specific node names to test (comma-separated).
    #[arg(short, long)]
    nodes: Option<String>,
}

#[derive(Debug, Deserialize)]
struct BootstrapDatabase {
    nodes: Vec<BootstrapNode>,
    validation: BootstrapValidationConfig,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
struct BootstrapNode {
    name: String,
    address: SocketAddr,
    provider: String,
    region: String,
    role: String,
    notes: String,
}

#[derive(Debug, Clone, Deserialize)]
struct BootstrapValidationConfig {
    threshold_percent: f32,
    connect_timeout_seconds: u64,
    per_node_timeout_seconds: u64,
    require_external_address: bool,
}

impl BootstrapValidationConfig {
    fn validate(&self) -> anyhow::Result<()> {
        if !self.threshold_percent.is_finite() {
            anyhow::bail!("validation threshold_percent must be finite");
        }
        if !(0.0..=100.0).contains(&self.threshold_percent) {
            anyhow::bail!("validation threshold_percent must be between 0 and 100 inclusive");
        }
        if self.connect_timeout_seconds == 0 {
            anyhow::bail!("validation connect_timeout_seconds must be greater than zero");
        }
        if self.per_node_timeout_seconds == 0 {
            anyhow::bail!("validation per_node_timeout_seconds must be greater than zero");
        }
        Ok(())
    }
}

#[derive(Debug, Serialize)]
struct BootstrapValidationResult {
    node: BootstrapNode,
    success: bool,
    connected_peers: usize,
    external_addr: Option<SocketAddr>,
    elapsed_ms: u128,
    error: Option<String>,
}

#[derive(Debug, Serialize)]
struct BootstrapValidationSummary {
    total_nodes: usize,
    passed_nodes: usize,
    failed_nodes: usize,
    success_rate: f32,
    threshold_percent: f32,
    threshold_met: bool,
}

#[derive(Debug, Serialize)]
struct BootstrapValidationReport {
    summary: BootstrapValidationSummary,
    results: Vec<BootstrapValidationResult>,
}

fn filter_nodes(
    nodes: Vec<BootstrapNode>,
    filter: Option<&str>,
) -> anyhow::Result<Vec<BootstrapNode>> {
    let Some(filter) = filter else {
        return Ok(nodes);
    };
    let wanted: std::collections::BTreeSet<_> = filter
        .split(',')
        .map(str::trim)
        .filter(|name| !name.is_empty())
        .collect();
    let available: std::collections::BTreeSet<_> =
        nodes.iter().map(|node| node.name.as_str()).collect();
    let missing: Vec<_> = wanted.difference(&available).copied().collect();

    if !missing.is_empty() {
        anyhow::bail!(
            "unknown Saorsa bootstrap node(s) requested by --nodes: {}",
            missing.join(", ")
        );
    }

    Ok(nodes
        .into_iter()
        .filter(|node| wanted.contains(node.name.as_str()))
        .collect())
}

fn summarize(
    results: Vec<BootstrapValidationResult>,
    threshold_percent: f32,
) -> BootstrapValidationReport {
    let total_nodes = results.len();
    let passed_nodes = results.iter().filter(|result| result.success).count();
    let failed_nodes = total_nodes.saturating_sub(passed_nodes);
    let success_rate = if total_nodes == 0 {
        0.0
    } else {
        (passed_nodes as f32 / total_nodes as f32) * 100.0
    };
    let threshold_met = total_nodes > 0 && success_rate >= threshold_percent;

    BootstrapValidationReport {
        summary: BootstrapValidationSummary {
            total_nodes,
            passed_nodes,
            failed_nodes,
            success_rate,
            threshold_percent,
            threshold_met,
        },
        results,
    }
}

async fn validate_node(
    node: BootstrapNode,
    validation: BootstrapValidationConfig,
) -> BootstrapValidationResult {
    let start = Instant::now();
    let result = validate_node_inner(&node, &validation).await;

    match result {
        Ok((connected_peers, external_addr)) => {
            let success = connected_peers > 0
                && (!validation.require_external_address || external_addr.is_some());
            BootstrapValidationResult {
                node,
                success,
                connected_peers,
                external_addr,
                elapsed_ms: start.elapsed().as_millis(),
                error: if success {
                    None
                } else {
                    Some("connected but did not satisfy validation requirements".to_string())
                },
            }
        }
        Err(error) => BootstrapValidationResult {
            node,
            success: false,
            connected_peers: 0,
            external_addr: None,
            elapsed_ms: start.elapsed().as_millis(),
            error: Some(error),
        },
    }
}

fn remaining_validation_time(deadline: tokio::time::Instant) -> Option<Duration> {
    deadline
        .checked_duration_since(tokio::time::Instant::now())
        .filter(|duration| !duration.is_zero())
}

async fn connect_known_peers_with_deadline<Connect, ConnectError, Shutdown, ShutdownFuture>(
    connect_timeout: Duration,
    validation_deadline: tokio::time::Instant,
    connect: Connect,
    shutdown: Shutdown,
) -> Result<usize, String>
where
    Connect: std::future::Future<Output = Result<usize, ConnectError>>,
    ConnectError: ToString,
    Shutdown: FnOnce() -> ShutdownFuture,
    ShutdownFuture: std::future::Future<Output = ()>,
{
    let Some(remaining) = remaining_validation_time(validation_deadline) else {
        shutdown().await;
        return Err("validation timed out".to_string());
    };
    let timeout_error = if remaining <= connect_timeout {
        "validation timed out"
    } else {
        "connect_known_peers timed out"
    };

    match tokio::time::timeout(connect_timeout.min(remaining), connect).await {
        Ok(Ok(count)) => Ok(count),
        Ok(Err(error)) => {
            shutdown().await;
            Err(error.to_string())
        }
        Err(_) => {
            shutdown().await;
            Err(timeout_error.to_string())
        }
    }
}

async fn validate_node_inner(
    node: &BootstrapNode,
    validation: &BootstrapValidationConfig,
) -> Result<(usize, Option<SocketAddr>), String> {
    let per_node_timeout = Duration::from_secs(validation.per_node_timeout_seconds);
    let validation_deadline = tokio::time::Instant::now() + per_node_timeout;
    let config = P2pConfig::builder()
        .bind_addr(
            "[::]:0"
                .parse::<SocketAddr>()
                .map_err(|error| error.to_string())?,
        )
        .known_peer(node.address)
        .port_mapping_enabled(false)
        .build()
        .map_err(|error| error.to_string())?;

    let endpoint_timeout = remaining_validation_time(validation_deadline)
        .ok_or_else(|| "validation timed out".to_string())?;
    let endpoint = tokio::time::timeout(endpoint_timeout, P2pEndpoint::new(config))
        .await
        .map_err(|_| "validation timed out".to_string())?
        .map_err(|error| error.to_string())?;

    let connected = connect_known_peers_with_deadline(
        Duration::from_secs(validation.connect_timeout_seconds),
        validation_deadline,
        endpoint.connect_known_peers(),
        || endpoint.shutdown(),
    )
    .await?;

    let external_addr = endpoint.external_addr();
    endpoint.shutdown().await;
    Ok((connected, external_addr))
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    let args = Args::parse();
    let config = std::fs::read_to_string(&args.config)?;
    let database: BootstrapDatabase = serde_yaml::from_str(&config)?;
    database.validation.validate()?;
    let nodes = filter_nodes(database.nodes, args.nodes.as_deref())?;

    if nodes.is_empty() {
        anyhow::bail!(
            "no Saorsa bootstrap nodes configured for filter '{}' in {}",
            args.nodes.as_deref().unwrap_or("all"),
            args.config.display()
        );
    }

    println!("================================================");
    println!("Saorsa Bootstrap Node Validation");
    println!("================================================");

    let mut results = Vec::new();
    for node in nodes {
        println!("Testing {} ({})...", node.name, node.address);
        let result = validate_node(node, database.validation.clone()).await;
        println!(
            "  {} connected_peers={} external_addr={:?} elapsed={}ms{}",
            if result.success { "OK" } else { "FAILED" },
            result.connected_peers,
            result.external_addr,
            result.elapsed_ms,
            result
                .error
                .as_ref()
                .map(|error| format!(" error={error}"))
                .unwrap_or_default()
        );
        results.push(result);
    }

    let report = summarize(results, database.validation.threshold_percent);
    println!("\nSummary:");
    println!("Total nodes tested: {}", report.summary.total_nodes);
    println!(
        "Successful connections: {} ({:.1}%)",
        report.summary.passed_nodes, report.summary.success_rate
    );
    println!("Threshold: {:.1}%", report.summary.threshold_percent);

    if let Some(output) = args.output {
        std::fs::write(&output, serde_json::to_string_pretty(&report)?)?;
        println!("Results saved to: {}", output.display());
    }

    if !report.summary.threshold_met {
        anyhow::bail!(
            "bootstrap validation success rate {:.1}% below threshold {:.1}%",
            report.summary.success_rate,
            report.summary.threshold_percent
        );
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{
        Arc,
        atomic::{AtomicUsize, Ordering},
    };

    fn node(name: &str) -> BootstrapNode {
        BootstrapNode {
            name: name.to_string(),
            address: "142.93.199.50:9000".parse().expect("valid addr"),
            provider: "DigitalOcean".to_string(),
            region: "NYC".to_string(),
            role: "bootstrap".to_string(),
            notes: "test".to_string(),
        }
    }

    fn result(name: &str, success: bool) -> BootstrapValidationResult {
        BootstrapValidationResult {
            node: node(name),
            success,
            connected_peers: usize::from(success),
            external_addr: None,
            elapsed_ms: 1,
            error: (!success).then(|| "failed".to_string()),
        }
    }

    fn validation_config(threshold_percent: f32) -> BootstrapValidationConfig {
        BootstrapValidationConfig {
            threshold_percent,
            connect_timeout_seconds: 10,
            per_node_timeout_seconds: 20,
            require_external_address: true,
        }
    }

    #[tokio::test]
    async fn connect_deadline_shutdown_on_validation_timeout() {
        let shutdowns = Arc::new(AtomicUsize::new(0));
        let shutdowns_for_future = Arc::clone(&shutdowns);
        let error = connect_known_peers_with_deadline(
            Duration::from_secs(10),
            tokio::time::Instant::now() + Duration::from_millis(1),
            std::future::pending::<Result<usize, String>>(),
            move || {
                let shutdowns = Arc::clone(&shutdowns_for_future);
                async move {
                    shutdowns.fetch_add(1, Ordering::SeqCst);
                }
            },
        )
        .await
        .err();

        assert_eq!(error.as_deref(), Some("validation timed out"));
        assert_eq!(shutdowns.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn validation_config_rejects_invalid_thresholds() {
        for threshold_percent in [-1.0, 100.1, f32::INFINITY, f32::NAN] {
            let config = validation_config(threshold_percent);
            assert!(config.validate().is_err());
        }
    }

    #[test]
    fn validation_config_rejects_zero_timeouts() {
        let mut config = validation_config(80.0);
        config.connect_timeout_seconds = 0;
        assert!(config.validate().is_err());

        let mut config = validation_config(80.0);
        config.per_node_timeout_seconds = 0;
        assert!(config.validate().is_err());
    }

    #[test]
    fn validation_config_accepts_boundary_thresholds() {
        assert!(validation_config(0.0).validate().is_ok());
        assert!(validation_config(100.0).validate().is_ok());
    }

    #[test]
    fn filter_nodes_without_filter_returns_all_nodes() -> anyhow::Result<()> {
        let nodes = vec![node("a"), node("b")];
        let filtered = filter_nodes(nodes, None)?;
        assert_eq!(filtered.len(), 2);
        assert_eq!(filtered[0].name, "a");
        assert_eq!(filtered[1].name, "b");
        Ok(())
    }

    #[test]
    fn filter_nodes_selects_requested_names() -> anyhow::Result<()> {
        let nodes = vec![node("a"), node("b")];
        let filtered = filter_nodes(nodes, Some("b"))?;
        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0].name, "b");
        Ok(())
    }

    #[test]
    fn filter_nodes_trims_empty_filter_entries() -> anyhow::Result<()> {
        let nodes = vec![node("a"), node("b"), node("c")];
        let filtered = filter_nodes(nodes, Some(" b, ,c "))?;
        let names: Vec<_> = filtered.into_iter().map(|node| node.name).collect();
        assert_eq!(names, vec!["b", "c"]);
        Ok(())
    }

    #[test]
    fn filter_nodes_unknown_filter_reports_missing_name() -> anyhow::Result<()> {
        let nodes = vec![node("a"), node("b")];
        let Err(error) = filter_nodes(nodes, Some("missing")) else {
            anyhow::bail!("unknown filter should fail");
        };
        assert!(error.to_string().contains("missing"));
        Ok(())
    }

    #[test]
    fn filter_nodes_mixed_unknown_filter_reports_missing_name() -> anyhow::Result<()> {
        let nodes = vec![node("a"), node("b")];
        let Err(error) = filter_nodes(nodes, Some("a,missing")) else {
            anyhow::bail!("mixed unknown filter should fail");
        };
        assert!(error.to_string().contains("missing"));
        Ok(())
    }

    #[test]
    fn summarize_requires_non_empty_results_for_threshold() {
        let report = summarize(Vec::new(), 50.0);
        assert_eq!(report.summary.total_nodes, 0);
        assert_eq!(report.summary.passed_nodes, 0);
        assert_eq!(report.summary.failed_nodes, 0);
        assert_eq!(report.summary.success_rate, 0.0);
        assert!(!report.summary.threshold_met);
    }

    #[test]
    fn summarize_marks_all_success_above_threshold() {
        let report = summarize(vec![result("a", true), result("b", true)], 80.0);
        assert_eq!(report.summary.total_nodes, 2);
        assert_eq!(report.summary.passed_nodes, 2);
        assert_eq!(report.summary.failed_nodes, 0);
        assert_eq!(report.summary.success_rate, 100.0);
        assert!(report.summary.threshold_met);
    }

    #[test]
    fn summarize_marks_threshold_edge_as_met() {
        let report = summarize(vec![result("a", true), result("b", false)], 50.0);
        assert_eq!(report.summary.total_nodes, 2);
        assert_eq!(report.summary.passed_nodes, 1);
        assert_eq!(report.summary.failed_nodes, 1);
        assert_eq!(report.summary.success_rate, 50.0);
        assert!(report.summary.threshold_met);
    }

    #[test]
    fn summarize_marks_below_threshold_as_not_met() {
        let report = summarize(
            vec![result("a", true), result("b", false), result("c", false)],
            50.0,
        );
        assert_eq!(report.summary.total_nodes, 3);
        assert_eq!(report.summary.passed_nodes, 1);
        assert_eq!(report.summary.failed_nodes, 2);
        assert!((report.summary.success_rate - 33.333336).abs() < f32::EPSILON);
        assert!(!report.summary.threshold_met);
    }

    #[test]
    fn summarize_preserves_results_for_json_report() {
        let report = summarize(vec![result("a", true), result("b", false)], 50.0);
        assert_eq!(report.results.len(), 2);
        assert_eq!(report.results[0].node.name, "a");
        assert_eq!(report.results[1].error.as_deref(), Some("failed"));
    }
}
