//! FUT roster-update transport host.
//!
//! ```text
//!   FIFA 17 ──HTTPS GET /fifa17/fut/rosterupdate.xml──> <rosterupdate version="0"/>
//! ```
//!
//! # Division of labour
//!
//! ```text
//!   openfut-tls              how to build the TLS acceptor
//!   adapter-fifa17::tls      what FIFA 17's TLS was observed to be
//!   adapter-fifa17::roster   what bytes the answer is
//!   this crate               accept, read, drain, write, close
//! ```
//!
//! This host contains no roster content and no cipher choice. It owns the
//! socket and the connection lifecycle, nothing else.
//!
//! # Lifecycle is part of the contract
//!
//! The redirector proved that byte-identical responses are not behavioural
//! parity: answering while the client is still sending leaves unread data in
//! the receive queue and Linux turns the close into an RST rather than a FIN.
//! So the oracle's lifecycle was *measured* rather than inferred, with a probe
//! run against a replica of `roster_server.py` (the live one is single-threaded
//! and was serving a live game at the time):
//!
//! ```text
//!   dwell after responding   0 ms      (the redirector's is 300 ms -- NOT shared)
//!   request body             drained   POST is answered only after the body arrives
//!   close                    clean FIN, never RST
//!   keep-alive               none      one request per connection
//! ```
//!
//! Every one of those is reproduced here, and asserted by `tests/`.
//!
//! # Why the roster matters more than its 67 bytes suggest
//!
//! `checkFUTRostersFlow` downloads this before entering FUT; failure aborts
//! with *"An error occurred downloading the FUT Squad Update"*. Because it is a
//! separate TLS connection from the redirector, it is also where a certificate
//! mismatch anywhere in the stack first becomes visible to the player.

use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

use openfut_adapter_fifa17::roster::{self, Method};
// Shared with the redirector: see openfut-http for why this is not duplicated.
use openfut_http::drain_body;
pub use openfut_http::BodyRead;
// The acceptor comes from the shared crate, so this host never names OpenSSL.
use openfut_tls::{peer_opening, PeerOpening, SslAcceptor};

pub mod config;
pub use config::RosterConfig;

/// Commit this binary was built from, stamped by `build.rs`.
pub const BUILD_COMMIT: &str = env!("OPENFUT_BUILD_COMMIT");

const MAX_HEAD: usize = 16 * 1024;

pub fn identity() -> String {
    format!(
        "openfut-roster-host v{} commit={} profile={} openssl={}",
        env!("CARGO_PKG_VERSION"),
        BUILD_COMMIT,
        if cfg!(debug_assertions) {
            "debug"
        } else {
            "release"
        },
        openfut_tls::openssl_version(),
    )
}

/// Machine-readable `key=value`, matching the redirector's banner so the two
/// hosts can be compared at a glance.
///
/// `cert_sha256` is printed unprompted: serving a different certificate from
/// the rest of the stack raises no error at startup and breaks the client much
/// later with nothing logged anywhere.
pub fn banner(cfg: &RosterConfig) -> String {
    format!(
        "{} listen={} server_header={:?} close_dwell_ms={} cert_sha256={} ciphers={}",
        identity(),
        cfg.listen_on(),
        cfg.server_header,
        cfg.close_dwell.as_millis(),
        openfut_tls::certificate_fingerprint(&cfg.tls.cert_path)
            .unwrap_or_else(|e| format!("unreadable ({e})")),
        cfg.tls.cipher_list,
    )
}

/// What the host did on a connection, beyond the bytes it sent.
///
/// Recorded because the divergence that cost two gate attempts on the
/// redirector — a request body left unread — is invisible to any comparison of
/// the response.
#[derive(Debug, Clone)]
pub struct ConnOutcome {
    pub id: u64,
    pub method: Option<Method>,
    pub path: String,
    pub request_bytes: usize,
    pub body: BodyRead,
    pub response_bytes: usize,
}

pub type Outcomes = Arc<std::sync::Mutex<Vec<ConnOutcome>>>;

/// How many bare port probes have been classified before reaching the acceptor.
///
/// Counted, not only logged, for the same reason [`ConnOutcome`] exists: a test
/// that asserts on client-visible symptoms cannot tell a probe that was
/// classified from one that merely failed quietly, so removing the
/// classification would leave the suite green. This makes it assertable — and
/// mirrors the redirector, the host that first needed the distinction.
pub type ProbeCount = Arc<AtomicUsize>;

/// Bounded: a host polled every few seconds must not accumulate forever.
const OUTCOME_HISTORY: usize = 64;

fn record(outcomes: &Outcomes, o: ConnOutcome) {
    if let Ok(mut v) = outcomes.lock() {
        if v.len() >= OUTCOME_HISTORY {
            v.remove(0);
        }
        v.push(o);
    }
}

pub struct Server {
    pub local_addr: std::net::SocketAddr,
    listener: TcpListener,
    acceptor: Arc<SslAcceptor>,
    cfg: Arc<RosterConfig>,
    outcomes: Outcomes,
    probes: ProbeCount,
}

impl Server {
    pub fn outcomes(&self) -> Outcomes {
        self.outcomes.clone()
    }

    /// A handle to the bare-probe counter, obtainable before [`Server::run`].
    pub fn probes(&self) -> ProbeCount {
        self.probes.clone()
    }

    pub fn run(self) -> std::io::Result<()> {
        let counter = AtomicU64::new(0);
        for incoming in self.listener.incoming() {
            let Ok(stream) = incoming else { continue };
            let id = counter.fetch_add(1, Ordering::Relaxed) + 1;
            let (acceptor, cfg) = (self.acceptor.clone(), self.cfg.clone());
            let outcomes = self.outcomes.clone();
            let probes = self.probes.clone();
            std::thread::spawn(move || handle(stream, id, &acceptor, &cfg, &outcomes, &probes));
        }
        Ok(())
    }
}

/// Build TLS, rehearse the retail handshake, and bind.
///
/// The rehearsal runs before the listener accepts anything, so a TLS
/// misconfiguration is a startup failure rather than a client-visible one.
pub fn bind(cfg: RosterConfig) -> std::io::Result<Server> {
    use openfut_adapter_fifa17::tls as profile;

    let acceptor = openfut_tls::build_acceptor(&cfg.tls)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()))?;

    let neg = openfut_tls::self_test(
        &cfg.tls,
        profile::OBSERVED_CLIENT_SUITES,
        profile::CLIENT_SNI,
    )
    .map_err(|e| {
        std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("TLS self-test failed: {e}"),
        )
    })?;
    log(&format!(
        "SELF-TEST OK: a FIFA-like client negotiates {} / {}",
        neg.version, neg.cipher
    ));

    let listener = TcpListener::bind(cfg.listen_on())?;
    let local_addr = listener.local_addr()?;
    Ok(Server {
        local_addr,
        listener,
        acceptor: Arc::new(acceptor),
        cfg: Arc::new(cfg),
        outcomes: Arc::new(std::sync::Mutex::new(Vec::new())),
        probes: ProbeCount::default(),
    })
}

pub fn serve(cfg: RosterConfig) -> std::io::Result<()> {
    let server = bind(cfg)?;
    log(&banner(&server.cfg));
    server.run()
}

fn log(msg: &str) {
    let t = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs_f64())
        .unwrap_or_default();
    println!("[{t:.3}] {msg}");
}

fn handle(
    stream: TcpStream,
    id: u64,
    acceptor: &SslAcceptor,
    cfg: &RosterConfig,
    outcomes: &Outcomes,
    probes: &ProbeCount,
) {
    let peer = stream
        .peer_addr()
        .map(|a| a.to_string())
        .unwrap_or_else(|_| "?".into());

    // Classify a bare port probe BEFORE the acceptor sees it. A `connect` then
    // drop (the launcher's preflight makes one per run) otherwise reaches the
    // acceptor as `unexpected EOF` — byte-identical to the certificate mismatch
    // that cost three live gates. Shared with the redirector via openfut-tls so
    // the two hosts can never diverge on this.
    if peer_opening(&stream) == PeerOpening::ClosedWithoutSpeaking {
        probes.fetch_add(1, Ordering::Relaxed);
        log(&format!(
            "conn-{id:04} {peer} PROBE: closed before sending a ClientHello (not a TLS fault)"
        ));
        return;
    }
    let mut tls = match acceptor.accept(stream) {
        Ok(s) => s,
        Err(e) => {
            log(&format!("conn-{id:04} {peer} TLS HANDSHAKE FAILED: {e}"));
            return;
        }
    };

    let mut buf = Vec::with_capacity(1024);
    if !read_head(&mut tls, &mut buf) {
        log(&format!("conn-{id:04} {peer} no request read"));
        return;
    }

    let head = String::from_utf8_lossy(&buf).to_string();
    let line0 = head.lines().next().unwrap_or_default().to_string();
    let method = Method::parse(&line0);
    let path = line0.split_whitespace().nth(1).unwrap_or("").to_string();

    // Drain BEFORE answering, exactly as the oracle does. `do_POST` reads
    // Content-Length bytes first; a lifecycle probe confirms the oracle does
    // not answer until the body arrives. Answering early would leave unread
    // data in the receive queue and turn our close into an RST.
    let body = drain_body(&mut tls, &mut buf);

    let response = match method {
        Some(m) => roster::roster_response(m, &cfg.server_header, &now_http_date()),
        // The oracle only defines GET/HEAD/POST; anything else is answered by
        // BaseHTTPRequestHandler's own 501, which this host does not claim to
        // reproduce. Closing without a response is honest about that rather
        // than inventing a reply the oracle never sends.
        None => {
            log(&format!(
                "conn-{id:04} {peer} unsupported method in {line0:?}; closing without a response"
            ));
            record(
                outcomes,
                ConnOutcome {
                    id,
                    method: None,
                    path,
                    request_bytes: buf.len(),
                    body,
                    response_bytes: 0,
                },
            );
            let _ = tls.shutdown();
            return;
        }
    };

    let _ = tls.write_all(&response);
    let _ = tls.flush();

    log(&format!(
        "conn-{id:04} {peer} {line0} req_bytes={} body={} -> {}B",
        buf.len(),
        body.describe(),
        response.len()
    ));
    record(
        outcomes,
        ConnOutcome {
            id,
            method,
            path,
            request_bytes: buf.len(),
            body,
            response_bytes: response.len(),
        },
    );

    // Zero by default: the oracle has no post-response sleep. Kept
    // configurable so the causal experiment is a config change.
    if !cfg.close_dwell.is_zero() {
        std::thread::sleep(cfg.close_dwell);
    }
    // One request per connection: the response says `Connection: close` and the
    // oracle closes. No keep-alive loop, measured and matched.
    let _ = tls.shutdown();
}

/// `Date:` for right now, in the oracle's format.
fn now_http_date() -> String {
    let secs = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs() as i64)
        .unwrap_or(0);
    roster::http_date(secs)
}

/// Read until the end of the request head, or give up.
fn read_head<S: Read>(stream: &mut S, buf: &mut Vec<u8>) -> bool {
    let mut chunk = [0u8; 1024];
    loop {
        match stream.read(&mut chunk) {
            Ok(0) | Err(_) => return !buf.is_empty(),
            Ok(n) => {
                buf.extend_from_slice(&chunk[..n]);
                if buf.windows(4).any(|w| w == b"\r\n\r\n") {
                    return true;
                }
                // A client that never terminates its head must not be able to
                // grow this buffer without bound.
                if buf.len() > MAX_HEAD {
                    return true;
                }
            }
        }
    }
}

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

    #[test]
    fn the_default_dwell_is_zero_unlike_the_redirectors() {
        // The redirector sleeps 300ms; the roster oracle does not. Inheriting
        // the wrong one would add a needless delay to a poll that runs every
        // few seconds, and would be a behaviour change rather than caution.
        assert_eq!(config::ORACLE_CLOSE_DWELL.as_millis(), 0);
    }

    #[test]
    fn identity_names_the_commit_and_linked_openssl() {
        let i = identity();
        assert!(i.contains("commit="), "{i}");
        assert!(i.contains("openssl="), "{i}");
    }

    #[test]
    fn a_body_is_reported_short_when_the_client_stops_early() {
        let mut buf = b"POST / HTTP/1.1\r\nContent-Length: 10\r\n\r\nabc".to_vec();
        let mut rest: &[u8] = b"";
        assert_eq!(
            drain_body(&mut rest, &mut buf),
            BodyRead::Short { got: 3, want: 10 }
        );
    }

    #[test]
    fn a_body_split_across_reads_is_completed() {
        let mut buf = b"POST / HTTP/1.1\r\nContent-Length: 8\r\n\r\nab".to_vec();
        let mut rest: &[u8] = b"cdefgh";
        assert_eq!(drain_body(&mut rest, &mut buf), BodyRead::Complete(8));
    }

    #[test]
    fn content_length_is_matched_case_insensitively() {
        let mut buf = b"POST / HTTP/1.1\r\ncOnTeNt-LeNgTh: 4\r\n\r\n".to_vec();
        let mut rest: &[u8] = b"abcd";
        assert_eq!(drain_body(&mut rest, &mut buf), BodyRead::Complete(4));
    }

    #[test]
    fn no_content_length_means_nothing_to_drain() {
        let mut buf = b"GET / HTTP/1.1\r\nHost: x\r\n\r\n".to_vec();
        let mut rest: &[u8] = b"";
        assert_eq!(drain_body(&mut rest, &mut buf), BodyRead::None);
    }
}
