//! OpenFUT clean-room LSX responder for FIFA 17 -- Rust port of
//! `fifa17-recon/tools/lsx_responder_v2.py` (v2, EVENT-PUSHING).
//!
//! v1 (`lsx_responder.py`) was REQUEST-DRIVEN ONLY. It answered every verb the
//! client asked for and never sent an unsolicited frame. That is exactly why the
//! client never issued GetAuthCode and never sent Blaze Authentication::login.
//!
//! THE ORIGIN SDK HAS TWO INDEPENDENT FLAGS, FED BY TWO DIFFERENT MECHANISMS:
//!
//!   1. "internet is reachable" -> OriginMgr online byte [0x1448a3ac0],
//!      fed by the REQUEST verb `GetInternetConnectedState -> connected="1"`
//!      (v1 already beat this; live-confirmed == 1).
//!   2. "a user is LOGGED IN"   -> `OriginMgr.m_isLoggedIn` [OriginMgr+0x13],
//!      fed ONLY by a server-PUSHED `<Event sender="LOGIN_EVENT"><Login/>`.
//!      There is NO request verb that can set it.
//!
//! v1 fed (1) and never fed (2), so `m_isLoggedIn` was 0 for the whole session,
//! FIFA never enqueued an auth-code request into FirstPartyAuthTokenRetriever
//! (both request slots live-read as 0x0), DoTick @0x146f199c0 exited immediately,
//! OriginRequestAuthCodeSync @0x1470db3c0 was never called, LoginRequest.AUTH
//! could never be filled -> no Blaze login -> "Unable to retrieve account
//! information."
//!
//! The binary evidence for the push (dispatcher @0x146f1e060 case 2, the `<Login>`
//! matcher @0x147102880 and its silent sender strcmp, the service-name tables at
//! 0x144341420 / sdk+0x3b0, the `strcmp(v,"false")` truthiness of `IsLoggedIn`)
//! lives in the Python's module docstring; the specific facts that constrain code
//! are repeated at the site that depends on them. Nothing here is derived from
//! the 2021 EA/FIFA leak: every constant came from our own static+dynamic
//! analysis of binaries we own plus traffic we captured ourselves.
//!
//! Transport: TCP `127.0.0.1:4216`, every message NUL-terminated (send strlen+1).

pub mod crypto;
pub mod events;
pub mod identity;
pub mod protocol;

/// Loopback port the Steampunks Origin emulator stub binds; we must own it
/// BEFORE FIFA 17 starts so the stub's own `bind()` fails.
pub const LSX_PORT: u16 = 4216;

/// Emu's own advertised challenge (any 32 hex chars work; the client echoes it
/// back).
pub const CHALLENGE_KEY: &str = "2b8ee7faea76e8a34f5f5d20e5328e32";
pub const BUILD: &str = "release";
pub const VERSION: &str = "10,4,13,6637";

/// Success-signal files the run's watch steps poll. Written only from a REAL
/// GetAuthCode on a live connection.
pub const AUTHCODE_FILE: &str = "/tmp/openfut_authcode.txt";
pub const CLIENTID_FILE: &str = "/tmp/openfut_lsx_clientid.txt";

/// How the modules read environment knobs. Indirected through a closure so the
/// tests can pin an environment without mutating the process (env vars are
/// process-global and `cargo test` runs threads in parallel).
pub type Env<'a> = &'a dyn Fn(&str) -> Option<String>;

/// The real environment. Present-but-empty is Some(""), which matters: the
/// Python's `os.environ.get(...)` returns "" for `FOO=`, and "" is not None, so
/// an empty setting WINS over the built-in default.
pub fn os_env(name: &str) -> Option<String> {
    std::env::var_os(name).map(|v| v.to_string_lossy().into_owned())
}

/// The Python's recurring `os.environ.get(NAME, "0") != "0"` rule: ANY value
/// other than the literal "0" enables the knob (including the empty string).
pub fn env_flag(env: Env<'_>, name: &str, default_on: bool) -> bool {
    match env(name) {
        Some(v) => v != "0",
        None => default_on,
    }
}

/// An env knob was set to something the Python's own `int()`/`float()` would
/// reject. The Python dies at import in that case; we refuse to start for the
/// same reason -- a silently different persona, or a silently absent heartbeat,
/// is worse than a loud failure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigError {
    pub var: &'static str,
    pub value: String,
    pub expected: &'static str,
}

impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{} must be {} (got {:?})",
            self.var, self.expected, self.value
        )
    }
}

impl std::error::Error for ConfigError {}

/// Every diagnostic line carries the `[lsx] ` prefix and is flushed per line:
/// the launcher pipes our stdout+stderr into its log buffer and parses some of
/// these lines, so the formats are a contract.
#[macro_export]
macro_rules! log {
    ($($arg:tt)*) => {{
        use std::io::Write;
        let mut out = std::io::stdout().lock();
        let _ = writeln!(out, "[lsx] {}", format_args!($($arg)*));
        let _ = out.flush();
    }};
}
