use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use thiserror::Error;
use url::Url;

#[derive(Debug, Error)]
pub enum SsrfError {
    #[error("Invalid URL: {0}")]
    InvalidUrl(String),

    #[error("Blocked scheme '{0}': only http and https are allowed")]
    BlockedScheme(String),

    #[error("Blocked host '{0}': private, reserved, or internal hostnames are not allowed")]
    BlockedHost(String),

    #[error("Blocked IP address {0}: {1}")]
    BlockedIp(String, &'static str),
}

/// Validates that `raw` is a safe, externally-reachable HTTP/HTTPS URL.
///
/// Rejects:
/// - Non-http/https schemes (`file://`, `ftp://`, `gopher://`, etc.)
/// - Localhost and `.local` / `.internal` TLD hostnames
/// - Cloud metadata endpoints (169.254.169.254, metadata.google.internal)
/// - RFC 1918 private ranges (10.x, 172.16-31.x, 192.168.x)
/// - RFC 6598 shared address space (100.64/10)
/// - IPv6 loopback, link-local, unique-local
///
/// Accepts bare literals without resolving DNS — DNS-rebinding is a separate
/// concern that must be addressed at the HTTP client layer with
/// `reqwest::ClientBuilder::resolve` or a DNS resolver allow-list.
pub fn validate_url(raw: &str) -> Result<Url, SsrfError> {
    let url = Url::parse(raw).map_err(|e: url::ParseError| SsrfError::InvalidUrl(e.to_string()))?;

    match url.scheme() {
        "http" | "https" => {}
        s => return Err(SsrfError::BlockedScheme(s.to_string())),
    }

    let host = url
        .host_str()
        .ok_or_else(|| SsrfError::InvalidUrl("URL has no host component".into()))?;

    validate_host(host)?;
    Ok(url)
}

/// Validates a hostname string or IPv4/IPv6 literal for SSRF safety.
///
/// Callable independently of `validate_url` when only the host is available
/// (e.g., validating `api_base` fields from config).
pub fn validate_host(host: &str) -> Result<(), SsrfError> {
    let lower = host.to_lowercase();

    // Block well-known dangerous literals before IP parsing.
    const BLOCKED_NAMES: &[&str] = &[
        "localhost",
        "0.0.0.0",
        "::1",
        "169.254.169.254",
        "metadata.google.internal",
        "instance-data",
        "link-local",
    ];
    if BLOCKED_NAMES.contains(&lower.as_str())
        || lower.ends_with(".local")
        || lower.ends_with(".internal")
        || lower.ends_with(".localdomain")
    {
        return Err(SsrfError::BlockedHost(host.to_string()));
    }

    // IP literal check — covers both plain addresses and IPv6 bracket notation.
    let ip_str = lower
        .strip_prefix('[')
        .and_then(|s| s.strip_suffix(']'))
        .unwrap_or(&lower);

    if let Ok(ip) = ip_str.parse::<IpAddr>() {
        return check_ip(ip, host);
    }

    Ok(())
}

fn check_ip(ip: IpAddr, raw: &str) -> Result<(), SsrfError> {
    let blocked = match ip {
        IpAddr::V4(v4) => blocked_v4(v4),
        IpAddr::V6(v6) => blocked_v6(v6),
    };
    match blocked {
        Some(reason) => Err(SsrfError::BlockedIp(raw.to_string(), reason)),
        None => Ok(()),
    }
}

fn blocked_v4(ip: Ipv4Addr) -> Option<&'static str> {
    let [a, b, c, d] = ip.octets();
    match (a, b, c, d) {
        (127, ..) => Some("loopback"),
        (10, ..) => Some("RFC 1918 private range"),
        (172, 16..=31, ..) => Some("RFC 1918 private range"),
        (192, 168, ..) => Some("RFC 1918 private range"),
        (169, 254, ..) => Some("link-local / cloud metadata endpoint"),
        (100, 64..=127, ..) => Some("shared address space (RFC 6598)"),
        (0, ..) => Some("unspecified address"),
        (255, ..) => Some("broadcast address"),
        _ => None,
    }
}

fn blocked_v6(ip: Ipv6Addr) -> Option<&'static str> {
    if ip.is_loopback() {
        return Some("loopback");
    }
    if ip.is_unspecified() {
        return Some("unspecified address");
    }
    let seg0 = ip.segments()[0];
    // fc00::/7 — unique local addresses (RFC 4193)
    if seg0 & 0xfe00 == 0xfc00 {
        return Some("unique local address (RFC 4193)");
    }
    // fe80::/10 — link-local addresses
    if seg0 & 0xffc0 == 0xfe80 {
        return Some("link-local address");
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn accepts_public_https() {
        assert!(validate_url("https://hooks.slack.com/services/T/B/token").is_ok());
        assert!(validate_url("https://discord.com/api/webhooks/123/abc").is_ok());
        assert!(validate_url("https://api.telegram.org/bot123:TOKEN/sendMessage").is_ok());
        assert!(validate_url("https://example.com/webhook?v=1").is_ok());
    }

    #[test]
    fn accepts_public_http() {
        assert!(validate_url("http://example.com/path").is_ok());
    }

    #[test]
    fn rejects_localhost_by_name() {
        assert!(validate_url("http://localhost/evil").is_err());
        assert!(validate_url("http://localhost:8080/path").is_err());
        assert!(validate_url("http://LOCALHOST/path").is_err());
    }

    #[test]
    fn rejects_loopback_ip() {
        assert!(validate_url("http://127.0.0.1/evil").is_err());
        assert!(validate_url("http://127.0.0.1:9200/").is_err());
        assert!(validate_url("http://127.255.255.255/").is_err());
    }

    #[test]
    fn rejects_private_ipv4_ranges() {
        assert!(validate_url("http://10.0.0.1/internal").is_err());
        assert!(validate_url("http://10.255.255.255/").is_err());
        assert!(validate_url("http://172.16.0.1/admin").is_err());
        assert!(validate_url("http://172.31.255.255/").is_err());
        assert!(validate_url("http://192.168.1.1/router").is_err());
    }

    #[test]
    fn rejects_cloud_metadata_endpoint() {
        assert!(validate_url("http://169.254.169.254/latest/meta-data/").is_err());
        assert!(validate_url("http://169.254.169.254/computeMetadata/v1/").is_err());
    }

    #[test]
    fn rejects_non_http_schemes() {
        assert!(validate_url("file:///etc/passwd").is_err());
        assert!(validate_url("ftp://example.com/data").is_err());
        assert!(validate_url("gopher://evil.com/path").is_err());
        assert!(validate_url("dict://127.0.0.1:11211/").is_err());
    }

    #[test]
    fn rejects_mdns_and_internal_tlds() {
        assert!(validate_url("http://surrealdb.local/db").is_err());
        assert!(validate_url("http://nats.internal/manage").is_err());
        assert!(validate_url("http://postgres.localdomain/").is_err());
    }

    #[test]
    fn rejects_metadata_hostname() {
        assert!(validate_host("metadata.google.internal").is_err());
        assert!(validate_host("instance-data").is_err());
    }

    #[test]
    fn rejects_ipv6_loopback() {
        assert!(validate_url("http://[::1]/evil").is_err());
    }

    #[test]
    fn rejects_ipv6_link_local() {
        assert!(validate_url("http://[fe80::1]/evil").is_err());
    }

    #[test]
    fn rejects_ipv6_unique_local() {
        assert!(validate_url("http://[fc00::1]/internal").is_err());
        assert!(validate_url("http://[fd12:3456:789a::1]/internal").is_err());
    }

    #[test]
    fn rejects_shared_address_space() {
        assert!(validate_url("http://100.64.0.1/").is_err());
        assert!(validate_url("http://100.127.255.255/").is_err());
    }
}
