//! FIFA 17 Blaze component, command and notification IDs.
//!
//! This is exactly the knowledge that must NOT live in
//! `openfut-protocol-blaze`: the generic layer routes on numbers, and what
//! those numbers mean is per-title.
//!
//! # Provenance
//!
//! The Util table was recovered from FIFA17.exe's own `getCommandName` switch
//! (jump table `0x141b17af4`). The Authentication table could not be recovered
//! statically — the name pool is Denuvo-mutated — so it was obtained by CALLING
//! the client's own `getCommandName` (`0x146e0d2a0`) in-process over ids 1..320,
//! validated by reproducing the known Util names, and cross-checked against a
//! static REST-binding struct (`0x143896a80` → `trustedLogin = 0x0B`).
//! UserSessions notification ids come from the clean, unmutated
//! `getNotificationName` jump table at `0x141b03f70`.
//!
//! Names are for diagnostics only. Dispatch matches on the numeric constants.

pub mod component {
    pub const AUTHENTICATION: u16 = 0x0001;
    pub const GAME_MANAGER: u16 = 0x0004;
    pub const REDIRECTOR: u16 = 0x0005;
    pub const STATS: u16 = 0x0007;
    pub const UTIL: u16 = 0x0009;
    pub const CENSUS_DATA: u16 = 0x000A;
    pub const CLUBS: u16 = 0x000B;
    pub const MESSAGING: u16 = 0x000F;
    pub const ASSOCIATION_LISTS: u16 = 0x0019;
    pub const GAME_REPORTING: u16 = 0x001C;
    pub const SPONSORED_EVENTS: u16 = 0x081C;
    pub const OSDK_SETTINGS: u16 = 0x08C9;
    pub const USER_SESSIONS: u16 = 0x7802;
}

pub mod util {
    pub const FETCH_CLIENT_CONFIG: u16 = 0x0001;
    pub const PING: u16 = 0x0002;
    pub const PRE_AUTH: u16 = 0x0007;
    pub const POST_AUTH: u16 = 0x0008;
    pub const USER_SETTINGS_LOAD: u16 = 0x000A;
    pub const USER_SETTINGS_SAVE: u16 = 0x000B;
    pub const FETCH_QOS_CONFIG: u16 = 0x0015;
    pub const SET_CLIENT_METRICS: u16 = 0x0016;
    pub const SET_CLIENT_STATE: u16 = 0x001C;
}

pub mod auth {
    pub const LOGIN: u16 = 0x000A;
    pub const TRUSTED_LOGIN: u16 = 0x000B;
    pub const LIST_USER_ENTITLEMENTS2: u16 = 0x001D;
    pub const GET_ACCOUNT: u16 = 0x001E;
    pub const LIST_ENTITLEMENTS: u16 = 0x0020;
    pub const GET_AUTH_TOKEN: u16 = 0x0024;
    pub const GRANT_ENTITLEMENT2: u16 = 0x0027;
    pub const LIST_PERSONA_ENTITLEMENTS2: u16 = 0x0030;
    pub const EXPRESS_LOGIN: u16 = 0x003C;
    /// Routine "drop any stale session" step before PCLogin, NOT a failure.
    pub const LOGOUT: u16 = 0x0046;
    pub const GET_PERSONA: u16 = 0x005A;
    pub const LIST_PERSONAS: u16 = 0x0064;
}

pub mod user_sessions {
    pub const UPDATE_NETWORK_INFO: u16 = 0x0014;

    /// Notification ids live in a separate number space from commands.
    pub mod notify {
        pub const EXTENDED_DATA_UPDATE: u16 = 0x0001;
        pub const USER_ADDED: u16 = 0x0002;
        pub const USER_REMOVED: u16 = 0x0003;
        pub const USER_UPDATED: u16 = 0x0005;
        pub const USER_AUTHENTICATED: u16 = 0x0008;
        pub const USER_UNAUTHENTICATED: u16 = 0x0009;
        pub const SERVER_DRAINING: u16 = 0x000C;
    }
}

pub mod association_lists {
    pub const GET_LISTS: u16 = 0x0006;
}

pub mod census_data {
    pub const SUBSCRIBE_TO_CENSUS_DATA_UPDATES: u16 = 0x0005;
}

/// Components advertised in `PreAuthResponse.CIDS`.
///
/// Order is the oracle's and is preserved: `CIDS` is a TDF list, and list
/// elements are NOT reordered by the encoder the way struct members are.
pub const ADVERTISED_COMPONENT_IDS: [i64; 9] = [
    component::AUTHENTICATION as i64,
    component::GAME_MANAGER as i64,
    component::REDIRECTOR as i64,
    component::STATS as i64,
    component::UTIL as i64,
    component::MESSAGING as i64,
    component::ASSOCIATION_LISTS as i64,
    component::GAME_REPORTING as i64,
    component::USER_SESSIONS as i64,
];

pub fn component_name(component: u16) -> Option<&'static str> {
    Some(match component {
        component::AUTHENTICATION => "Authentication",
        component::GAME_MANAGER => "GameManager",
        component::REDIRECTOR => "Redirector",
        component::STATS => "Stats",
        component::UTIL => "Util",
        component::CENSUS_DATA => "CensusData",
        component::CLUBS => "Clubs",
        component::MESSAGING => "Messaging",
        component::ASSOCIATION_LISTS => "AssociationLists",
        component::GAME_REPORTING => "GameReporting",
        component::SPONSORED_EVENTS => "SponsoredEvents",
        component::OSDK_SETTINGS => "OSDKSettings",
        component::USER_SESSIONS => "UserSessions",
        _ => return None,
    })
}

pub fn command_name(component: u16, command: u16) -> Option<&'static str> {
    Some(match (component, command) {
        (component::UTIL, 0x01) => "fetchClientConfig",
        (component::UTIL, 0x02) => "ping",
        (component::UTIL, 0x03) => "setClientData",
        (component::UTIL, 0x04) => "localizeStrings",
        (component::UTIL, 0x05) => "getTelemetryServer",
        (component::UTIL, 0x06) => "getTickerServer",
        (component::UTIL, 0x07) => "preAuth",
        (component::UTIL, 0x08) => "postAuth",
        (component::UTIL, 0x0A) => "userSettingsLoad",
        (component::UTIL, 0x0B) => "userSettingsSave",
        (component::UTIL, 0x0C) => "userSettingsLoadAll",
        (component::UTIL, 0x0E) => "userSettingsDelete",
        (component::UTIL, 0x0F) => "userSettingsLoadAllForUser",
        (component::UTIL, 0x14) => "filterForProfanity",
        (component::UTIL, 0x15) => "fetchQosConfig",
        (component::UTIL, 0x16) => "setClientMetrics",
        (component::UTIL, 0x17) => "setConnectionState",
        (component::UTIL, 0x19) => "getUserOptions",
        (component::UTIL, 0x1A) => "setUserOptions",
        (component::UTIL, 0x1B) => "suspendUserPing",
        (component::UTIL, 0x1C) => "setClientState",

        (component::AUTHENTICATION, 0x0A) => "login",
        (component::AUTHENTICATION, 0x0B) => "trustedLogin",
        (component::AUTHENTICATION, 0x14) => "updateAccount",
        (component::AUTHENTICATION, 0x15) => "upgradeAccount",
        (component::AUTHENTICATION, 0x1D) => "listUserEntitlements2",
        (component::AUTHENTICATION, 0x1E) => "getAccount",
        (component::AUTHENTICATION, 0x1F) => "grantEntitlement",
        (component::AUTHENTICATION, 0x20) => "listEntitlements",
        (component::AUTHENTICATION, 0x22) => "getUseCount",
        (component::AUTHENTICATION, 0x23) => "decrementUseCount",
        (component::AUTHENTICATION, 0x24) => "getAuthToken",
        (component::AUTHENTICATION, 0x26) => "getPasswordRules",
        (component::AUTHENTICATION, 0x27) => "grantEntitlement2",
        (component::AUTHENTICATION, 0x2B) => "modifyEntitlement2",
        (component::AUTHENTICATION, 0x2C) => "consumecode",
        (component::AUTHENTICATION, 0x2D) => "passwordForgot",
        (component::AUTHENTICATION, 0x2F) => "getPrivacyPolicyContent",
        (component::AUTHENTICATION, 0x30) => "listPersonaEntitlements2",
        (component::AUTHENTICATION, 0x33) => "checkAgeReq",
        (component::AUTHENTICATION, 0x34) => "getOptIn",
        (component::AUTHENTICATION, 0x35) => "enableOptIn",
        (component::AUTHENTICATION, 0x36) => "disableOptIn",
        (component::AUTHENTICATION, 0x3C) => "expressLogin",
        (component::AUTHENTICATION, 0x46) => "logout",
        (component::AUTHENTICATION, 0x5A) => "getPersona",
        (component::AUTHENTICATION, 0x64) => "listPersonas",
        (component::AUTHENTICATION, 0x65) => "expressCreateAccount",
        (component::AUTHENTICATION, 0xE6) => "createWalUserSession",
        (component::AUTHENTICATION, 0xF1) => "acceptLegalDocs",
        (component::AUTHENTICATION, 0xF2) => "getEmailOptInSettings",
        (component::AUTHENTICATION, 0xF6) => "getTermsOfServiceContent",
        (component::AUTHENTICATION, 0x104) => "getOriginPersona",
        (component::AUTHENTICATION, 0x10E) => "checkEmail",
        (component::AUTHENTICATION, 0x118) => "getPersonaNameSuggestions",
        (component::AUTHENTICATION, 0x122) => "guestLogin",

        (component::CENSUS_DATA, 0x01) => "subscribeToCensusData",
        (component::CENSUS_DATA, 0x02) => "unsubscribeFromCensusData",
        (component::CENSUS_DATA, 0x03) => "getRegionCounts",
        (component::CENSUS_DATA, 0x04) => "getLatestCensusData",
        (component::CENSUS_DATA, 0x05) => "subscribeToCensusDataUpdates",

        (component::USER_SESSIONS, 0x14) => "updateNetworkInfo",
        (component::ASSOCIATION_LISTS, 0x06) => "getLists",
        _ => return None,
    })
}

pub fn notification_name(component: u16, notify_id: u16) -> Option<&'static str> {
    if component != component::USER_SESSIONS {
        return None;
    }
    Some(match notify_id {
        0x01 => "UserSessionExtendedDataUpdate",
        0x02 => "UserAdded",
        0x03 => "UserRemoved",
        0x05 => "UserUpdated",
        0x08 => "UserAuthenticated",
        0x09 => "UserUnauthenticated",
        0x0C => "ServerDraining",
        _ => return None,
    })
}

/// Human-readable label for a route, for logs and captures.
pub fn describe(component: u16, command: u16, is_notification: bool) -> String {
    let comp = component_name(component)
        .map(str::to_string)
        .unwrap_or_else(|| format!("Component:0x{component:04x}"));
    if is_notification {
        if let Some(n) = notification_name(component, command) {
            return format!("{comp}::<{n}>");
        }
        return format!("{comp}::<notify:0x{command:04x}>");
    }
    match command_name(component, command) {
        Some(name) => format!("{comp}::{name}"),
        None => format!("{comp}::cmd:0x{command:04x}"),
    }
}

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

    #[test]
    fn describes_the_first_rpc_fifa_sends() {
        assert_eq!(
            describe(component::UTIL, util::PRE_AUTH, false),
            "Util::preAuth"
        );
    }

    #[test]
    fn commands_and_notifications_are_separate_number_spaces() {
        // 0x0002 is UserSessions "UserAdded" as a notification, but is not a
        // known UserSessions *command*.
        assert_eq!(
            describe(component::USER_SESSIONS, 0x0002, true),
            "UserSessions::<UserAdded>"
        );
        assert_eq!(
            describe(component::USER_SESSIONS, 0x0002, false),
            "UserSessions::cmd:0x0002"
        );
    }

    #[test]
    fn unknown_routes_degrade_to_numbers() {
        assert_eq!(
            describe(0x1234, 0x0001, false),
            "Component:0x1234::cmd:0x0001"
        );
    }

    #[test]
    fn advertised_components_are_in_oracle_order() {
        // A list, not a struct: the encoder will NOT sort these, so the order
        // here is the order on the wire.
        assert_eq!(ADVERTISED_COMPONENT_IDS[0], 0x0001);
        assert_eq!(ADVERTISED_COMPONENT_IDS[8], 0x7802);
        assert_eq!(ADVERTISED_COMPONENT_IDS.len(), 9);
    }
}
