//! Roster host configuration.
//!
//! Endpoints resolve through `openfut-host-config`, the single environment
//! reader, so every host builds client-visible addresses the same way. Only
//! transport settings are parsed here, and none of them are client-visible.

use std::time::Duration;

use openfut_adapter_fifa17::roster;
use openfut_host_config::{self as hostcfg, ConfigError};
use openfut_tls::{ProtocolVersion, TlsConfig};

/// How long the oracle holds a roster connection open after responding.
///
/// **Measured, and deliberately different from the redirector's.** The Blaze
/// redirector sleeps 300ms before closing (`time.sleep(0.3)` in
/// `redir_handle`); `roster_server.py` has no such sleep, and a lifecycle probe
/// against the oracle records `close_ms=0.0` for GET, HEAD and POST alike.
///
/// Copying the redirector's 300ms because "the other host does it" would have
/// been the obvious mistake: FIFA polls this endpoint roughly every 7-15s, so a
/// needless dwell per poll is a behaviour change, not a safety margin.
pub const ORACLE_CLOSE_DWELL: Duration = Duration::from_millis(0);

/// Compose the shared listener from the FIFA 17 adapter's observed TLS profile.
///
/// Identical construction to the redirector host. That is the point: the client
/// caches the certificate presented by the first service it reaches, so every
/// FIFA-facing listener must be built the same way from the same inputs.
fn fifa17_tls(cert: impl Into<String>, key: impl Into<String>) -> Result<TlsConfig, ConfigError> {
    use openfut_adapter_fifa17::tls as profile;
    let ver = |s: &str| {
        ProtocolVersion::parse(s).map_err(|e| ConfigError(format!("FIFA 17 TLS profile: {e}")))
    };
    Ok(TlsConfig::new(
        cert,
        key,
        profile::CIPHER_LIST,
        ver(profile::MIN_VERSION)?,
        ver(profile::MAX_VERSION)?,
    ))
}

#[derive(Debug, Clone)]
pub struct RosterConfig {
    /// BIND: where this listener binds. Never client-visible.
    pub listen_addr: String,
    /// Required, with no default, so this host can never collide with the
    /// Python roster server it runs beside.
    pub listen_port: u16,
    pub tls: TlsConfig,
    /// The `Server:` header to emit.
    ///
    /// Environment-derived, not protocol-derived: it carries the *oracle's*
    /// Python version, so it changes when the container's Python does. Default
    /// is what the container was last observed to send; overridable so matching
    /// a redeployed oracle is a configuration change, not a release.
    pub server_header: String,
    /// Dwell before closing an answered connection. Overridable ONLY so the
    /// causal experiment can be run without rebuilding.
    pub close_dwell: Duration,
}

impl RosterConfig {
    pub fn from_env() -> Result<RosterConfig, ConfigError> {
        let adapter = hostcfg::adapter_from_env()?;

        let listen_port = hostcfg::required_port(
            "OPENFUT_ROSTER_HOST_PORT",
            "this host runs beside the working Python roster server and must \
             not collide with it, so the port is explicit and has no default",
        )?;
        let listen_addr = hostcfg::optional("OPENFUT_ROSTER_HOST_BIND", &adapter.endpoints.bind);

        let cert = hostcfg::required(
            "OPENFUT_ROSTER_CERT",
            "path to the RSA certificate; it MUST be the same certificate every \
             other FIFA-facing service presents, or the client's cached copy \
             will not match and the handshake fails with nothing logged",
        )?;
        let key = hostcfg::required("OPENFUT_ROSTER_KEY", "path to the matching private key")?;

        let tls = fifa17_tls(cert, key)?;

        let server_header =
            hostcfg::optional("OPENFUT_ROSTER_SERVER_HEADER", roster::ORACLE_SERVER);

        let close_dwell = match hostcfg::optional_opt("OPENFUT_ROSTER_CLOSE_DWELL_MS") {
            None => ORACLE_CLOSE_DWELL,
            // Invalid input is an error, never a silent fallback: a typo that
            // quietly restored the default would make the causal experiment
            // report the wrong answer.
            Some(v) => Duration::from_millis(v.trim().parse().map_err(|_| {
                ConfigError(format!(
                    "OPENFUT_ROSTER_CLOSE_DWELL_MS is not a number of milliseconds: {v:?}"
                ))
            })?),
        };

        Ok(RosterConfig {
            listen_addr,
            listen_port,
            tls,
            server_header,
            close_dwell,
        })
    }

    pub fn listen_on(&self) -> String {
        format!("{}:{}", self.listen_addr, self.listen_port)
    }

    /// Test fixture: an ephemeral port and a certificate pair supplied by the
    /// caller, so tests never depend on a deployed path.
    #[doc(hidden)]
    pub fn for_test(cert: &str, key: &str) -> RosterConfig {
        RosterConfig {
            listen_addr: "127.0.0.1".into(),
            listen_port: 0,
            tls: fifa17_tls(cert, key).expect("the adapter's TLS profile must be valid"),
            server_header: roster::ORACLE_SERVER.to_string(),
            close_dwell: ORACLE_CLOSE_DWELL,
        }
    }
}
