use std::fmt::Write;
use std::net::SocketAddr;
use tokio::net::TcpListener;
use tracing::{info, warn};

#[derive(Debug)]
pub struct BindFailure {
    pub address: SocketAddr,
    pub error: std::io::Error,
}

#[derive(Debug)]
pub struct BindResult<T> {
    pub successful: Vec<T>,
    pub failures: Vec<BindFailure>,
}

impl<T> BindResult<T> {
    pub fn is_empty(&self) -> bool {
        self.successful.is_empty()
    }

    pub fn warn_partial_failures(&self, transport_name: &str) {
        for failure in &self.failures {
            warn!(
                "{} partial binding failure: {} - {}",
                transport_name, failure.address, failure.error
            );
        }
    }
}

pub async fn bind_tcp_addresses(
    addrs: &[SocketAddr],
    transport_name: &str,
) -> BindResult<TcpListener> {
    let mut successful = Vec::new();
    let mut failures = Vec::new();

    for addr in addrs {
        match TcpListener::bind(addr).await {
            Ok(listener) => {
                info!("MQTT broker {} listening on {}", transport_name, addr);
                successful.push(listener);
            }
            Err(e) => {
                warn!("Failed to bind {} on {} ({})", transport_name, addr, e);
                failures.push(BindFailure {
                    address: *addr,
                    error: e,
                });
            }
        }
    }

    BindResult {
        successful,
        failures,
    }
}

pub fn format_binding_error(
    transport_name: &str,
    failures: &[BindFailure],
    attempted_addrs: &[SocketAddr],
) -> String {
    let mut msg = format!("Failed to bind to any {transport_name} address. Attempted addresses:\n");

    for failure in failures {
        writeln!(
            msg,
            "  - {}: {} ({})",
            failure.address,
            failure.error,
            error_kind_to_hint(&failure.error)
        )
        .ok();
    }

    if attempted_addrs.len() > failures.len() {
        let successful_count = attempted_addrs.len() - failures.len();
        writeln!(
            msg,
            "\nNote: {successful_count} address(es) bound successfully."
        )
        .ok();
    }

    msg.push_str("\nSuggestion: ");
    if failures
        .iter()
        .any(|f| f.error.kind() == std::io::ErrorKind::AddrInUse)
    {
        let ports: Vec<u16> = failures.iter().map(|f| f.address.port()).collect();
        let ports_str = if ports.len() == 1 {
            format!("port {}", ports[0])
        } else {
            format!(
                "ports {}",
                ports
                    .iter()
                    .map(std::string::ToString::to_string)
                    .collect::<Vec<_>>()
                    .join(", ")
            )
        };
        write!(
            msg,
            "Check for processes using {ports_str} with 'lsof -i :<port>' or use different ports."
        )
        .ok();
    } else if failures
        .iter()
        .any(|f| f.error.kind() == std::io::ErrorKind::PermissionDenied)
    {
        msg.push_str("Run with elevated privileges or use ports > 1024.");
    } else {
        msg.push_str("Check network configuration and address availability.");
    }

    msg
}

fn error_kind_to_hint(error: &std::io::Error) -> &'static str {
    match error.kind() {
        std::io::ErrorKind::AddrInUse => "address already in use",
        std::io::ErrorKind::PermissionDenied => "permission denied",
        std::io::ErrorKind::AddrNotAvailable => "address not available",
        _ => "see error above",
    }
}
