//! # openfut-blaze-host
//!
//! A deliberately thin TCP host for the FIFA 17 Blaze RPC surface.
//!
//! ```text
//!   listener  →  Fire2 stream framing  →  per-connection Session
//!                                              │
//!                                    openfut-adapter-fifa17::dispatch
//!                                              │
//!                                    write returned frames, in order
//! ```
//!
//! ## What it owns
//!
//! A socket, a read buffer, one `Session` per connection, and diagnostics.
//! That is the complete list.
//!
//! ## What it must never acquire
//!
//! Coins, club state, packs, profiles, market state, UTAS logic. Those belong
//! to OpenFUT Core, reached later through the adapter. A transport host that
//! starts holding game state becomes a second backend, which is exactly the
//! architecture this migration exists to avoid.
//!
//! ## No TLS
//!
//! The Blaze main port is **plaintext**. Verified against the running Python
//! backend by sending a raw Fire2 `Util::ping` and receiving a plaintext
//! `PingResponse`; `blaze_responder_v3b.py::blaze_handle` uses the raw socket,
//! and only `redir_handle` wraps `ssl`. TLS belongs to the redirector phase.
//!
//! ## Running beside Python, never instead of it
//!
//! The listen port is required configuration with no default, so this cannot
//! silently collide with the working container. See the crate README for the
//! A/B procedure and the gate list.

pub mod capture;
pub mod config;
pub mod conn;
pub mod sanitize;
pub mod trace;

pub use config::HostConfig;
pub use conn::CloseReason;

use std::io;
use std::net::{SocketAddr, TcpListener};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

use openfut_adapter_fifa17::blaze::Adapter;

/// The two values a live host reads from the outside world.
///
/// Injectable so a test can replay a recorded conversation byte-for-byte: the
/// session key and the server clock appear inside response bodies, so with the
/// real ones no live run could ever reproduce a fixture exactly. This is the
/// only test seam in the crate, and it exists because the alternative is to
/// verify transport *structurally only* and lose the byte-level guarantee.
pub struct Hooks {
    pub session_key: Box<dyn Fn() -> String + Send + Sync>,
    pub now: Box<dyn Fn() -> i64 + Send + Sync>,
}

impl Default for Hooks {
    fn default() -> Hooks {
        Hooks {
            session_key: Box::new(conn::mint_session_key),
            now: Box::new(conn::unix_now),
        }
    }
}

impl std::fmt::Debug for Hooks {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Hooks { .. }")
    }
}

/// Build identity, stamped by `build.rs` and printed at startup.
///
/// `dirty` is the load-bearing field: a mutation-tested build is a dirty build,
/// and a live trace must never be attributable to one by accident.
pub struct BuildIdentity {
    pub commit: &'static str,
    pub dirty: &'static str,
    pub profile: &'static str,
    pub version: &'static str,
}

pub const BUILD: BuildIdentity = BuildIdentity {
    commit: env!("OPENFUT_BUILD_COMMIT"),
    dirty: env!("OPENFUT_BUILD_DIRTY"),
    profile: if cfg!(debug_assertions) {
        "debug"
    } else {
        "release"
    },
    version: env!("CARGO_PKG_VERSION"),
};

/// One line naming the binary and the generated data it carries.
pub fn build_banner() -> String {
    format!(
        "openfut-blaze-host v{} commit={} tree={} profile={} config-table={:016x}",
        BUILD.version,
        BUILD.commit,
        BUILD.dirty,
        BUILD.profile,
        openfut_adapter_fifa17::blaze::client_config::table_fingerprint()
    )
}

/// A bound listener plus everything a connection needs.
pub struct Server {
    pub local_addr: SocketAddr,
    listener: TcpListener,
    cfg: Arc<HostConfig>,
    adapter: Arc<Adapter>,
    tracer: Arc<trace::Tracer>,
    hooks: Arc<Hooks>,
    capture: Arc<capture::Capture>,
}

/// Bind without accepting, so a caller can learn the real port first (useful
/// when binding port 0).
pub fn bind(cfg: HostConfig, hooks: Hooks) -> io::Result<Server> {
    let listener = TcpListener::bind(cfg.listen_on())?;
    let local_addr = listener.local_addr()?;
    let tracer = Arc::new(trace::Tracer::new(cfg.trace_path.as_deref())?);
    let capture = Arc::new(match cfg.capture_path.as_deref() {
        Some(p) => capture::Capture::create(p)?,
        None => capture::Capture::disabled(),
    });
    let adapter = Arc::new(Adapter::new(cfg.adapter.clone()));
    Ok(Server {
        local_addr,
        listener,
        cfg: Arc::new(cfg),
        adapter,
        tracer,
        hooks: Arc::new(hooks),
        capture,
    })
}

impl Server {
    /// Accept forever, one thread per connection.
    ///
    /// A FIFA client opens a handful of connections, so a thread each mirrors
    /// the Python oracle and keeps the host readable; a work-stealing runtime
    /// would be weight without benefit.
    pub fn run(self) -> io::Result<()> {
        trace::log(&build_banner());
        if BUILD.dirty == "DIRTY" {
            trace::log(
                "WARNING: built from a modified working tree — do NOT treat results from \
                 this binary as parity evidence",
            );
        }
        trace::log(&format!(
            "listening on {} (advertise={}, config-bind={})",
            self.local_addr, self.cfg.adapter.endpoints.advertise, self.cfg.adapter.endpoints.bind
        ));
        // The trace is the artefact a live FIFA session is judged from, so it
        // must carry the same identity as the log.
        self.tracer.write_line(&format!("# {}", build_banner()));
        if self.tracer.enabled() {
            trace::log(&format!(
                "structural trace -> {}",
                self.cfg.trace_path.as_deref().unwrap_or("")
            ));
        }
        if self.capture.enabled() {
            trace::log(&format!(
                "RAW FRAME CAPTURE -> {} (forensic evidence: never commit, \
                 sanitize with blaze-sanitize before sharing)",
                self.cfg.capture_path.as_deref().unwrap_or("")
            ));
        }

        let counter = AtomicU64::new(0);
        for incoming in self.listener.incoming() {
            match incoming {
                Ok(stream) => {
                    let id = counter.fetch_add(1, Ordering::Relaxed) + 1;
                    let cfg = self.cfg.clone();
                    let adapter = self.adapter.clone();
                    let tracer = self.tracer.clone();
                    let hooks = self.hooks.clone();
                    let capture = self.capture.clone();
                    std::thread::spawn(move || {
                        conn::handle(stream, id, &cfg, &adapter, &tracer, &hooks, &capture);
                    });
                }
                Err(e) => trace::log(&format!("accept failed: {e}")),
            }
        }
        Ok(())
    }
}

/// Bind and serve until the process is killed.
pub fn serve(cfg: HostConfig) -> io::Result<()> {
    bind(cfg, Hooks::default())?.run()
}
