//! Environment → [`HostConfig`]. Client-visible bind and the Python upstream are
//! REQUIRED with no default (host-family discipline: a defaulted port could
//! collide with the live oracle). `core_url` defaults to Bridge's convention.

use std::env;
const SBC_FAULT_ACK: &str = "staging-only-sbc-receipt-loss";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SbcPostCommitFault {
    #[default]
    Off,
    Drop,
    Malformed,
    Delay {
        millis: u64,
    },
}

#[derive(Debug, Clone)]
pub struct HostConfig {
    /// Where this host listens (the address FIFA reaches for UTAS). Required.
    pub listen_addr: String,
    /// Base URL of the Python UTAS oracle for fallback, e.g.
    /// `http://127.0.0.1:8199`. Required — must NOT be this host's own address.
    pub python_upstream: String,
    /// OpenFUT Core base URL. Default `http://127.0.0.1:8080` (Bridge convention).
    pub core_url: String,
    /// Directory holding `leagues.json`/`nations.json`/`teams.json`.
    pub tables_dir: String,
    /// FIFA 17 card-definition **identity catalog** (card id → FIFA asset id).
    /// Required production identity source: a `/club` item's `resourceId` comes
    /// from here. Startup fails if it cannot be loaded — never a placeholder.
    pub catalog_path: String,
    /// Persistent external-identity **store** file (owned-instance → stable wire
    /// id). Required: the wire `id` of every owned item is allocated/resolved
    /// here so it survives restart and reverses exactly.
    pub identity_store_path: String,
    /// The launcher-selected FIFA persona id, injected via `OPENFUT_PERSONA_ID`.
    /// Required, non-zero: it stamps `personaId` on the Core-backed
    /// `GET /squad/active`, and must match the persona LSX/Blaze/POW/UTAS use.
    pub persona_id: i64,
    /// Durable FIFA17 transfer-market listing DB (host-owned SQLite). Required
    /// for the economy cutover; must survive host restart (a real path, not a
    /// temp file). Env `OPENFUT_MARKET_DB`.
    pub market_db_path: String,
    /// Durable FIFA17 item-pile metadata DB (host-owned SQLite). Required; must
    /// survive host restart. Env `OPENFUT_PILE_DB`.
    pub pile_db_path: String,
    /// Durable client-data blob store (`clientdata`/`userHubData`), host-owned
    /// JSON file. NOT required: defaults to env `OPENFUT_CLIENTDATA_DB`, else the
    /// identity store's parent directory + `clientdata.json`. The blobs are
    /// non-authoritative client UI state, so a default path is safe.
    pub clientdata_path: String,
    /// Shared FIFA17 account JSON (`fut_account.py` shape), used for club-name
    /// reads and the Rust-owned rename mutation. Defaults to
    /// `OPENFUT_ACCOUNT_PATH`, then existing `FUT_ACCOUNT_PATH`, then the
    /// identity store's parent directory + `active_account.json`.
    pub account_path: String,
    /// Disabled by default. Non-off values require three explicit staging guards;
    /// see [`parse_sbc_post_commit_fault`].
    pub sbc_post_commit_fault: SbcPostCommitFault,
}

#[derive(Debug)]
pub struct ConfigError(pub String);

impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}
impl std::error::Error for ConfigError {}

fn required(key: &str) -> Result<String, ConfigError> {
    match env::var(key) {
        Ok(v) if !v.is_empty() => Ok(v),
        _ => Err(ConfigError(format!("{key} is required (no default)"))),
    }
}

/// Parse a required, non-zero i64 env var. A zero identity id is invalid (the
/// client rejects a zero persona), so unset/empty/non-integer/zero is a hard error.
fn required_i64_nonzero(key: &str) -> Result<i64, ConfigError> {
    let raw = required(key)?;
    let val: i64 = raw
        .parse()
        .map_err(|_| ConfigError(format!("{key} must be an integer, got {raw:?}")))?;
    if val == 0 {
        return Err(ConfigError(format!("{key} must be non-zero")));
    }
    Ok(val)
}

fn parse_sbc_post_commit_fault(
    raw: Option<&str>,
    environment: Option<&str>,
    ack: Option<&str>,
    listen_addr: &str,
) -> Result<SbcPostCommitFault, ConfigError> {
    let mode = match raw.filter(|value| !value.is_empty()).unwrap_or("off") {
        "off" => return Ok(SbcPostCommitFault::Off),
        "drop" => SbcPostCommitFault::Drop,
        "malformed" => SbcPostCommitFault::Malformed,
        value if value.starts_with("delay:") => {
            let millis = value["delay:".len()..].parse::<u64>().map_err(|_| {
                ConfigError(
                    "OPENFUT_FIFA17_SBC_POST_COMMIT_FAULT delay must be delay:<milliseconds>"
                        .into(),
                )
            })?;
            if !(1..=30_000).contains(&millis) {
                return Err(ConfigError(
                    "OPENFUT_FIFA17_SBC_POST_COMMIT_FAULT delay must be 1..=30000 ms".into(),
                ));
            }
            SbcPostCommitFault::Delay { millis }
        }
        value => {
            return Err(ConfigError(format!(
                "OPENFUT_FIFA17_SBC_POST_COMMIT_FAULT must be off, drop, malformed, or delay:<milliseconds>; got {value:?}"
            )));
        }
    };
    if environment != Some("staging") {
        return Err(ConfigError(
            "SBC post-commit faults require OPENFUT_ENVIRONMENT=staging".into(),
        ));
    }
    if ack != Some(SBC_FAULT_ACK) {
        return Err(ConfigError(format!(
            "SBC post-commit faults require OPENFUT_FIFA17_SBC_POST_COMMIT_FAULT_ACK={SBC_FAULT_ACK}"
        )));
    }
    if listen_addr.rsplit_once(':').map(|(_, port)| port) == Some("8099") {
        return Err(ConfigError(
            "SBC post-commit faults refuse the production UTAS port 8099".into(),
        ));
    }
    Ok(mode)
}

impl HostConfig {
    pub fn from_env() -> Result<Self, ConfigError> {
        let identity_store_path = required("OPENFUT_IDENTITY_STORE")?;
        let listen_addr = required("OPENFUT_UTAS_HOST_ADDR")?;
        let sbc_post_commit_fault = parse_sbc_post_commit_fault(
            env::var("OPENFUT_FIFA17_SBC_POST_COMMIT_FAULT")
                .ok()
                .as_deref(),
            env::var("OPENFUT_ENVIRONMENT").ok().as_deref(),
            env::var("OPENFUT_FIFA17_SBC_POST_COMMIT_FAULT_ACK")
                .ok()
                .as_deref(),
            &listen_addr,
        )?;
        let clientdata_path = env::var("OPENFUT_CLIENTDATA_DB")
            .ok()
            .filter(|v| !v.is_empty())
            .unwrap_or_else(|| default_clientdata_path(&identity_store_path));
        let account_path = env::var("OPENFUT_ACCOUNT_PATH")
            .ok()
            .filter(|v| !v.is_empty())
            .or_else(|| env::var("FUT_ACCOUNT_PATH").ok().filter(|v| !v.is_empty()))
            .unwrap_or_else(|| default_account_path(&identity_store_path));
        Ok(HostConfig {
            listen_addr,
            python_upstream: required("OPENFUT_UTAS_PYTHON_URL")?,
            core_url: env::var("OPENFUT_CORE_URL")
                .unwrap_or_else(|_| "http://127.0.0.1:8080".into()),
            tables_dir: env::var("OPENFUT_FIFA17_TABLES_DIR")
                .unwrap_or_else(|_| "fifa17-recon/data/tables".into()),
            catalog_path: required("OPENFUT_FIFA17_CATALOG")?,
            persona_id: required_i64_nonzero("OPENFUT_PERSONA_ID")?,
            market_db_path: required("OPENFUT_MARKET_DB")?,
            pile_db_path: required("OPENFUT_PILE_DB")?,
            identity_store_path,
            clientdata_path,
            account_path,
            sbc_post_commit_fault,
        })
    }
}

/// Default client-data blob path: the identity store's parent directory +
/// `clientdata.json` (co-located with the other host-owned durable state).
fn default_clientdata_path(identity_store_path: &str) -> String {
    std::path::Path::new(identity_store_path)
        .parent()
        .map(|p| p.join("clientdata.json"))
        .unwrap_or_else(|| std::path::PathBuf::from("clientdata.json"))
        .to_string_lossy()
        .into_owned()
}

fn default_account_path(identity_store_path: &str) -> String {
    std::path::Path::new(identity_store_path)
        .parent()
        .map(|p| p.join("active_account.json"))
        .unwrap_or_else(|| std::path::PathBuf::from("active_account.json"))
        .to_string_lossy()
        .into_owned()
}

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

    #[test]
    fn sbc_post_commit_faults_require_all_staging_guards() {
        assert_eq!(
            parse_sbc_post_commit_fault(None, None, None, "0.0.0.0:8099").unwrap(),
            SbcPostCommitFault::Off
        );
        assert!(parse_sbc_post_commit_fault(
            Some("drop"),
            None,
            Some(SBC_FAULT_ACK),
            "127.0.0.1:18199"
        )
        .is_err());
        assert!(parse_sbc_post_commit_fault(
            Some("drop"),
            Some("staging"),
            None,
            "127.0.0.1:18199"
        )
        .is_err());
        assert!(parse_sbc_post_commit_fault(
            Some("drop"),
            Some("staging"),
            Some(SBC_FAULT_ACK),
            "0.0.0.0:8099"
        )
        .is_err());
        assert_eq!(
            parse_sbc_post_commit_fault(
                Some("drop"),
                Some("staging"),
                Some(SBC_FAULT_ACK),
                "0.0.0.0:18199"
            )
            .unwrap(),
            SbcPostCommitFault::Drop
        );
        assert_eq!(
            parse_sbc_post_commit_fault(
                Some("delay:25"),
                Some("staging"),
                Some(SBC_FAULT_ACK),
                "0.0.0.0:18199"
            )
            .unwrap(),
            SbcPostCommitFault::Delay { millis: 25 }
        );
    }
}
