//! FIFA 17 Blaze redirector: the first hop.
//!
//! ```text
//!   FIFA 17 ──TLS──> redirector ──"connect to <ip>:<port>"──> plaintext Fire2 Blaze
//! ```
//!
//! A different protocol from Blaze itself: HTTPS with an XML body (DirtySDK's
//! ProtoHttp), not Fire2. Exactly one request is ever seen —
//! `POST /redirector/getServerInstance` — observed 9 times across every live
//! session with no other path.
//!
//! # Scope
//!
//! This module owns the **response**, which is game-specific. It does not own
//! TLS or HTTP transport; that belongs to a host, exactly as the Blaze adapter
//! owns dispatch while `openfut-blaze-host` owns the socket.
//!
//! # A TLS constraint that is not this module's problem, but is recorded here
//!
//! Every observed handshake negotiated `AES256-GCM-SHA384` — TLS 1.2 with
//! **static RSA key exchange** (`TLS_RSA_WITH_AES_256_GCM_SHA384`), against a
//! Python server offering `ALL:@SECLEVEL=0` and an RSA-2048 certificate
//! (DirtySDK rejects ECDSA). `rustls` supports only forward-secret (EC)DHE
//! suites, so it cannot serve that negotiation. Whether the client *offers*
//! ECDHE at all is UNKNOWN — OpenSSL follows client preference by default, so
//! preferring static RSA does not prove it is the only option. Instrument a
//! real ClientHello before choosing a TLS stack.

use std::fmt::Write as _;

use crate::blaze::config::AdapterConfig;

/// The only request path the client ever uses.
pub const REQUEST_PATH: &str = "/redirector/getServerInstance";

/// Where the client is told to find Blaze.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlazeEndpoint {
    /// Advertised hostname or dotted-quad.
    pub host: String,
    pub port: u16,
    /// Whether the Blaze hop is TLS. **0 for FIFA 17** — the second hop is
    /// plaintext Fire2, which this field independently confirms.
    pub secure: bool,
}

impl BlazeEndpoint {
    /// Both host and port come from configuration. Neither is a protocol
    /// constant: the client goes wherever this response sends it, so hardcoding
    /// either would make the deployment un-relocatable.
    pub fn from_config(cfg: &AdapterConfig) -> BlazeEndpoint {
        BlazeEndpoint {
            host: cfg.endpoints.advertise.clone(),
            port: cfg.endpoints.blaze_port,
            secure: false,
        }
    }
}

/// Dotted-quad to the decimal `u32` the client expects in `<ip>`.
///
/// Host byte order, so `127.0.0.1` is `2130706433` (`0x7F000001`). A non-IPv4
/// advertise value (a hostname) has no numeric form; the oracle falls back to
/// loopback rather than failing, and that behaviour is reproduced — the client
/// reads `<hostname>` too, so the numeric field is not the only route.
pub fn ip_to_u32(addr: &str) -> u32 {
    let octets: Vec<u32> = addr
        .split('.')
        .filter_map(|p| p.parse::<u32>().ok())
        .filter(|n| *n <= 255)
        .collect();
    if octets.len() == 4 {
        (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
    } else {
        (127 << 24) | 1
    }
}

/// The `<serverinstanceinfo>` XML body.
///
/// `<address member="0">` is a `ServerAddress` union; member 0 selects the
/// `ipAddress` variant `{hostname, ip, port}`. Tabs and newlines are part of
/// the byte-exact output — the client does not care, but parity does.
pub fn server_instance_info_xml(endpoint: &BlazeEndpoint) -> String {
    let mut s = String::with_capacity(320);
    s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
    s.push_str("<serverinstanceinfo>\n");
    s.push_str("\t<address member=\"0\">\n");
    s.push_str("\t\t<valu>\n");
    let _ = writeln!(s, "\t\t\t<hostname>{}</hostname>", endpoint.host);
    let _ = writeln!(s, "\t\t\t<ip>{}</ip>", ip_to_u32(&endpoint.host));
    let _ = writeln!(s, "\t\t\t<port>{}</port>", endpoint.port);
    s.push_str("\t\t</valu>\n");
    s.push_str("\t</address>\n");
    let _ = writeln!(s, "\t<secure>{}</secure>", u8::from(endpoint.secure));
    s.push_str("\t<trialservicename></trialservicename>\n");
    s.push_str("\t<defaultdnsaddress>0</defaultdnsaddress>\n");
    s.push_str("</serverinstanceinfo>\n");
    s
}

/// The complete HTTP response, headers included.
///
/// `Connection: close` is the oracle's behaviour and the client accepts it —
/// the redirector is a one-shot hop, unlike the long-lived Blaze connection.
pub fn redirect_response(cfg: &AdapterConfig) -> Vec<u8> {
    let body = server_instance_info_xml(&BlazeEndpoint::from_config(cfg));
    let mut out = String::with_capacity(body.len() + 128);
    out.push_str("HTTP/1.1 200 OK\r\n");
    out.push_str("Content-Type: application/xml\r\n");
    let _ = write!(out, "Content-Length: {}\r\n", body.len());
    out.push_str("Connection: close\r\n\r\n");
    out.push_str(&body);
    out.into_bytes()
}

/// Is this request line the one the client sends?
///
/// Diagnostics only: the oracle answers *any* request with the same body, so
/// dispatch does not branch on this. Reproduced as-is — a redirector that
/// started 404ing unexpected paths would be a behaviour change, not a fix.
pub fn is_get_server_instance(request_line: &str) -> bool {
    request_line.starts_with("POST ") && request_line.contains(REQUEST_PATH)
}

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

    fn cfg(advertise: &str) -> AdapterConfig {
        let mut c = AdapterConfig::loopback();
        c.endpoints.advertise = advertise.into();
        c
    }

    #[test]
    fn ip_encoding_is_host_order_decimal() {
        assert_eq!(ip_to_u32("127.0.0.1"), 2_130_706_433);
        assert_eq!(ip_to_u32("198.51.100.7"), 3_325_256_711);
        assert_eq!(ip_to_u32("203.0.113.42"), 3_405_803_818);
    }

    #[test]
    fn a_non_ipv4_advertise_falls_back_to_loopback_like_the_oracle() {
        // The hostname element still carries the real value, so this is not a
        // dead end for the client.
        assert_eq!(ip_to_u32("blaze.example.com"), 2_130_706_433);
        assert_eq!(ip_to_u32("10.0.0"), 2_130_706_433);
        assert_eq!(ip_to_u32("999.1.1.1"), 2_130_706_433);
    }

    #[test]
    fn secure_is_zero_confirming_the_plaintext_second_hop() {
        let body = server_instance_info_xml(&BlazeEndpoint::from_config(&cfg("203.0.113.42")));
        assert!(body.contains("<secure>0</secure>"));
    }

    #[test]
    fn response_advertises_the_configured_address_not_a_hardcoded_one() {
        let r = String::from_utf8(redirect_response(&cfg("198.51.100.7"))).unwrap();
        assert!(r.contains("<hostname>198.51.100.7</hostname>"));
        assert!(r.contains("<ip>3325256711</ip>"));
        assert!(r.contains("<port>42130</port>"));
        assert!(!r.contains("127.0.0.1"));
    }

    #[test]
    fn content_length_matches_the_body_exactly() {
        let bytes = redirect_response(&cfg("203.0.113.42"));
        let text = String::from_utf8(bytes).unwrap();
        let (head, body) = text.split_once("\r\n\r\n").expect("header/body split");
        let declared: usize = head
            .lines()
            .find_map(|l| l.strip_prefix("Content-Length: "))
            .and_then(|v| v.trim().parse().ok())
            .expect("content-length present");
        assert_eq!(
            declared,
            body.len(),
            "a wrong length would truncate the XML"
        );
    }

    #[test]
    fn recognises_the_only_observed_request_line() {
        assert!(is_get_server_instance(
            "POST /redirector/getServerInstance HTTP/1.1"
        ));
        assert!(!is_get_server_instance(
            "GET /redirector/getServerInstance HTTP/1.1"
        ));
        assert!(!is_get_server_instance("POST /something/else HTTP/1.1"));
    }
}
