//! Adapter configuration: identity and endpoints.
//!
//! Everything deployment-dependent lives here, injected by the caller. No
//! address, port or persona is baked into the response builders — the
//! client/server split exists precisely because the Python responders used to
//! assume loopback, and rebuilding that assumption in Rust would undo it.
//!
//! Note the deliberate asymmetry between *bind* and *advertise*: an advertised
//! URL must carry the address the CLIENT can reach, which on a two-machine
//! deployment is not the address the server binds.

/// The forged account the whole stack agrees on.
///
/// Identity has to be byte-identical across LSX, Blaze, POW and UTAS or the
/// client rejects the session, so this is one struct passed everywhere rather
/// than constants per responder.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Identity {
    pub persona_id: i64,
    pub persona_name: String,
    /// blazeId / userId. Must be non-zero or login is refused.
    pub user_id: i64,
    /// XREF externalId.
    pub ext_id: i64,
    pub email: String,
    /// Must equal `PreAuthResponse.NASP`.
    pub namespace: String,
    /// `Blaze::ClientPlatformType`; 4 = pc.
    pub client_platform: i64,
    /// `PersonaStatus::Code`; 2 = ACTIVE.
    pub persona_status: i64,
    /// `Blaze::UserSessionType`; 0 = normal user.
    pub user_session_type: i64,
    /// Fallback locale as a packed four-char int (`'enUS'`). Overwritten per
    /// session by the client's own preAuth `LANG`/`LOC`.
    pub account_locale: i64,
    /// `AccountInfo.LN`, e.g. `"en_US"`.
    pub locale: String,
    /// EA offer id.
    pub content_id: String,
    pub entitlement_tag: String,
    /// Must contain `"FIFA17PCBoxContent"` or `"FIFA16PC"` or FUT drops the
    /// entitlement and the store comes up empty.
    pub entitlement_group: String,
    pub title_id: String,
    pub client_id: String,
    pub platform: String,
}

impl Default for Identity {
    /// The project's fixed synthetic offline identity.
    ///
    /// A default, not a constant: the launcher can select a different persona,
    /// and FUT saves are isolated per persona id.
    fn default() -> Identity {
        Identity {
            persona_id: 33_068_179,
            persona_name: "CAGE".into(),
            user_id: 33_068_179,
            ext_id: 33_068_179,
            email: "cage@openfut.local".into(),
            namespace: "cem_ea_id".into(),
            client_platform: 4,
            persona_status: 2,
            user_session_type: 0,
            account_locale: 0x656E_5553, // 'enUS'
            locale: "en_US".into(),
            content_id: "1027460".into(),
            entitlement_tag: "ONLINE_ACCESS".into(),
            entitlement_group: "FIFA17PCBoxContent".into(),
            title_id: "309111".into(),
            client_id: "FIFA17-PC-SERVER-BLAZE".into(),
            platform: "pc".into(),
        }
    }
}

/// Where the client should be told to go next.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Endpoints {
    /// Address handed to the CLIENT for every next hop. On a split deployment
    /// this is the backend's LAN address as the game machine sees it.
    pub advertise: String,
    /// Address the server binds. Not interchangeable with `advertise`.
    pub bind: String,
    /// `host:port` for POW content.
    pub pow_content_host: String,
    /// `host:port` for the POW/EASFC API.
    pub pow_host: String,
    /// Blaze port ADVERTISED to the client by the redirector.
    ///
    /// Our choice, not a protocol constant — the client goes wherever
    /// `<serverinstanceinfo>` sends it. Configurable so a sidecar can be
    /// advertised on a different port without a rebuild.
    pub blaze_port: u16,
    /// UTAS/RS4 port in generated `FUT_RS4_*` URLs.
    ///
    /// 8099 is the client's own built-in default (`http://easw.easports.com:8099/`
    /// in CardsDLL), so it is the sane value — but it is still deployment
    /// configuration, not a constant we are entitled to bake in.
    pub utas_port: u16,
    pub telemetry_port: i64,
    pub ticker_port: i64,
    pub qos_port: i64,
}

// NOTE: there is deliberately NO `impl Default for Endpoints`.
//
// A default would silently supply loopback, and a remote deployment that forgot
// to set an address would then advertise `127.0.0.1` to a client on another
// machine — failing far from the cause. Choosing loopback has to be an explicit
// act, so it is a named constructor.

impl Endpoints {
    /// Endpoints for a backend the client reaches at `advertise`.
    ///
    /// POW hosts DERIVE from the advertised host, matching what the deployed
    /// Python entrypoint does (`POW_HOST="${POW_HOST:-$ADV:8094}"`). They must
    /// not fall back to loopback independently: that would leave a remote
    /// deployment emitting loopback POW URLs while every other URL was correct.
    pub fn advertising(advertise: impl Into<String>) -> Endpoints {
        let advertise = advertise.into();
        Endpoints {
            pow_content_host: format!("{advertise}:8080"),
            pow_host: format!("{advertise}:8094"),
            bind: advertise.clone(),
            advertise,
            blaze_port: 42130,
            utas_port: 8099,
            telemetry_port: 9988,
            ticker_port: 8999,
            qos_port: 17502,
        }
    }

    /// Explicit local-only / oracle mode: game and backend on one host.
    ///
    /// Named rather than defaulted so that "everything is loopback" is always a
    /// decision someone made, and greppable.
    pub fn loopback() -> Endpoints {
        Endpoints::advertising("127.0.0.1")
    }
}

/// Full adapter configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdapterConfig {
    pub identity: Identity,
    pub endpoints: Endpoints,
    /// `PreAuthResponse.SVER`. Carries a trailing newline in the oracle; kept
    /// because it is on the wire, not because it is meaningful.
    pub server_version: String,
}

/// `PreAuthResponse.SVER`. On the wire, so it is config rather than a literal.
pub const DEFAULT_SERVER_VERSION: &str = "Blaze 15.1.1.3.0 (OpenFUT)\n";

// No `Default` here either, for the same reason as `Endpoints`.

impl AdapterConfig {
    /// Adapter serving a client that reaches this backend at `advertise`.
    pub fn advertising(advertise: impl Into<String>) -> AdapterConfig {
        AdapterConfig {
            identity: Identity::default(),
            endpoints: Endpoints::advertising(advertise),
            server_version: DEFAULT_SERVER_VERSION.into(),
        }
    }

    /// Explicit local-only / oracle mode.
    pub fn loopback() -> AdapterConfig {
        AdapterConfig::advertising("127.0.0.1")
    }

    /// `http://<advertise>:8099/` — the RS4/UTAS base.
    ///
    /// The trailing slash and the scheme are both mandatory: CardsDLL's
    /// `ServerSettings::resolve` uses the value verbatim once it contains
    /// `"://"`, and the auth path breaks without the slash.
    pub fn utas_base(&self) -> String {
        format!(
            "http://{}:{}/",
            self.endpoints.advertise, self.endpoints.utas_port
        )
    }

    /// `http://<bind>:42131` — the Nucleus OAuth stub.
    ///
    /// This derives from **bind**, not advertise, faithfully reproducing the
    /// Python oracle. On the live split deployment that makes it
    /// `http://0.0.0.0:42131`, which the client cannot dial — see the crate
    /// README and the vault. Reproduced deliberately: changing it would break
    /// byte parity with the only configuration ever proven to work, and the
    /// fix belongs in a separate, live-validated change.
    pub fn nucleus_base(&self) -> String {
        format!("http://{}:42131", self.endpoints.bind)
    }

    pub fn pow_content_url(&self) -> String {
        format!("http://{}", self.endpoints.pow_content_host)
    }
}

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

    #[test]
    fn utas_base_keeps_scheme_and_trailing_slash() {
        let mut cfg = AdapterConfig::loopback();
        cfg.endpoints.advertise = "10.0.0.5".into();
        assert_eq!(cfg.utas_base(), "http://10.0.0.5:8099/");
    }

    #[test]
    fn nucleus_follows_bind_not_advertise() {
        // Documents the oracle's behaviour, including its consequence.
        let mut cfg = AdapterConfig::loopback();
        cfg.endpoints.advertise = "10.0.0.5".into();
        cfg.endpoints.bind = "0.0.0.0".into();
        assert_eq!(cfg.nucleus_base(), "http://0.0.0.0:42131");
    }

    #[test]
    fn pow_content_url_has_no_trailing_slash() {
        let mut cfg = AdapterConfig::loopback();
        cfg.endpoints.pow_content_host = "10.0.0.5:8085".into();
        assert_eq!(cfg.pow_content_url(), "http://10.0.0.5:8085");
    }
}
