//! FIFA 17 `/match` + `/match/end` wire ↔ Core match-economy mapping.
//!
//! This module owns the FIFA 17-specific match protocol: the `endReason` enum
//! (wire atom 260), the `PUT …/match/end` payload shape, and the reward-response
//! body. It is pure — no state, no Core calls. The host wires it to OpenFUT
//! Core's authoritative `complete_match` transaction:
//!
//! 1. [`parse_match_end`] turns the client payload into a [`MatchEnd`].
//! 2. [`MatchResult::core_token`] gives Core the canonical, game-independent
//!    result string — Core never sees a FIFA `endReason`.
//! 3. Core applies the economy exactly once and returns the authoritative coin
//!    numbers, which the host renders back through [`reward_response`].
//!
//! Keeping every FIFA 17 constant here (never in Core) is the layering contract:
//! a second title's adapter maps its own wire onto the same canonical tokens.

use serde_json::{json, Value};

/// Participation award added to every match reward. FIFA 17 economy parameter
/// (oracle `MATCH_PARTICIPATION`, production default `0`). Core owns the coin
/// balance; this is only the wire body's cosmetic `participationAward` field.
pub const MATCH_PARTICIPATION: i64 = 0;

/// Canonical match result. The FIFA 17 `endReason` enum (atom 260) is the
/// AUTHORITATIVE source [STATIC_REVERSED]; this is the normalized shape the host
/// forwards to Core.
///
/// * `Win` / `Draw` / `Loss` — a decided match.
/// * `Dnf` — the reporting player abandoned/quit (`DNF`/`QUIT`). Economically a
///   loss (LIVE_PROVEN: `endReason=DNF` → loss reward), tracked in its own Core
///   statistics bucket.
/// * `NoContest` — a voided match; zero economic effect.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchResult {
    Win,
    Draw,
    Loss,
    Dnf,
    NoContest,
}

impl MatchResult {
    /// The canonical Core result token — the ONLY match datum the adapter hands
    /// Core. Matches `openfut_core::models::match_result::MatchResultKind`'s serde
    /// representation exactly (`win`/`draw`/`loss`/`dnf`/`no_contest`).
    pub fn core_token(self) -> &'static str {
        match self {
            MatchResult::Win => "win",
            MatchResult::Draw => "draw",
            MatchResult::Loss => "loss",
            MatchResult::Dnf => "dnf",
            MatchResult::NoContest => "no_contest",
        }
    }
}

/// Map the FIFA 17 `endReason` enum onto a canonical [`MatchResult`].
///
/// The enum is authoritative when present [STATIC_REVERSED]. `DNF`/`QUIT` are the
/// reporting player's abandon (→ `Dnf`, loss economics, LIVE_PROVEN); the
/// `DNF_WIN`/`DNF_DRAW`/`DNF_LOSS` variants carry a decided outcome (the opponent
/// abandoned) and map to that outcome. `NO_CONTEST` voids the match. An
/// absent/unknown reason is a conservative `Draw`, matching the oracle default.
pub fn result_from_end_reason(end_reason: Option<&str>) -> MatchResult {
    match end_reason.unwrap_or("").to_ascii_uppercase().as_str() {
        "WIN" => MatchResult::Win,
        "DRAW" => MatchResult::Draw,
        "LOSS" => MatchResult::Loss,
        "DNF" | "QUIT" => MatchResult::Dnf,
        "DNF_WIN" => MatchResult::Win,
        "DNF_DRAW" => MatchResult::Draw,
        "DNF_LOSS" => MatchResult::Loss,
        "NO_CONTEST" => MatchResult::NoContest,
        _ => MatchResult::Draw,
    }
}

/// A parsed `PUT …/match/end` payload: the fields the host needs to drive Core.
/// Unknown fields are ignored.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatchEnd {
    /// The raw `endReason` string, if the client sent one.
    pub end_reason: Option<String>,
    /// Canonical result derived from `end_reason`.
    pub result: MatchResult,
    /// `matchReportId` from the payload (observed `0` on the live path).
    pub match_report_id: i64,
    /// Goals scored by the reporting player — `myMatchStats[0]` (goals is the
    /// first of the 15 ints). Absent on `DNF`/`QUIT` (stats omitted) → `0`.
    pub goals_for: i64,
    /// Opponent goals — `opponentMatchStats[0]`. Absent on `DNF`/`QUIT` → `0`.
    pub goals_against: i64,
}

/// Parse the FIFA 17 match-end body. Returns `None` for a body that is not a
/// JSON object (malformed). A well-formed object with a missing/unknown
/// `endReason` still parses — the result defaults to `Draw`.
pub fn parse_match_end(body: &[u8]) -> Option<MatchEnd> {
    let v: Value = serde_json::from_slice(body).ok()?;
    if !v.is_object() {
        return None;
    }
    let end_reason = v
        .get("endReason")
        .and_then(Value::as_str)
        .map(str::to_string);
    let result = result_from_end_reason(end_reason.as_deref());
    let match_report_id = v.get("matchReportId").and_then(Value::as_i64).unwrap_or(0);
    Some(MatchEnd {
        end_reason,
        result,
        match_report_id,
        goals_for: first_stat(&v, "myMatchStats"),
        goals_against: first_stat(&v, "opponentMatchStats"),
    })
}

/// `myMatchStats`/`opponentMatchStats` are 15 ints with goals first
/// [STATIC_REVERSED]; the arrays are OMITTED on DNF/QUIT, so a missing array is
/// `0` goals, not an error.
fn first_stat(v: &Value, key: &str) -> i64 {
    v.get(key)
        .and_then(Value::as_array)
        .and_then(|a| a.first())
        .and_then(Value::as_i64)
        .unwrap_or(0)
}

/// Build the FIFA 17 match-reward response body (the `destroy_match_body` shape,
/// [STATIC_REVERSED]).
///
/// `all_coins` is Core's AUTHORITATIVE post-credit balance; `match_coins` is the
/// amount Core granted for THIS match (mirrored into `gameModeAward.coins`, where
/// the client reads it). Emits ONLY the reversed fields — it NEVER emits
/// `bidTokens` or `qualifiedChampionEventId`, which are client freeze traps.
pub fn reward_response(all_coins: i64, match_coins: i64) -> Value {
    json!({
        "allCoins": all_coins,
        "matchCoins": match_coins,
        "seasonCoins": 0,
        "tournamentCoins": 0,
        "boostConis": 0, // EA's misspelling (atom 96), preserved on the wire.
        "participationAward": MATCH_PARTICIPATION,
        "teamOfTournamentWinner": false,
        "gameModeAward": { "coins": match_coins },
    })
}

/// Build the FIFA 17 `POST …/match` create ack. Zero economic effect: it only
/// hands the client a match id + start time. `id` doubles as the per-match
/// identity the host later keys Core's exactly-once completion on.
pub fn create_response(id: i64, start_epoch: i64) -> Value {
    json!({
        "startDateTime": start_epoch,
        "reportIdEnabled": false,
        "id": id,
    })
}

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

    #[test]
    fn every_end_reason_maps_to_canonical_result() {
        assert_eq!(result_from_end_reason(Some("WIN")), MatchResult::Win);
        assert_eq!(result_from_end_reason(Some("DRAW")), MatchResult::Draw);
        assert_eq!(result_from_end_reason(Some("LOSS")), MatchResult::Loss);
        assert_eq!(result_from_end_reason(Some("DNF")), MatchResult::Dnf);
        assert_eq!(result_from_end_reason(Some("QUIT")), MatchResult::Dnf);
        assert_eq!(
            result_from_end_reason(Some("NO_CONTEST")),
            MatchResult::NoContest
        );
        assert_eq!(result_from_end_reason(Some("DNF_WIN")), MatchResult::Win);
        assert_eq!(result_from_end_reason(Some("DNF_DRAW")), MatchResult::Draw);
        assert_eq!(result_from_end_reason(Some("DNF_LOSS")), MatchResult::Loss);
        // Case-insensitive.
        assert_eq!(result_from_end_reason(Some("dnf_win")), MatchResult::Win);
        // Unknown / absent → conservative draw.
        assert_eq!(result_from_end_reason(Some("weird")), MatchResult::Draw);
        assert_eq!(result_from_end_reason(None), MatchResult::Draw);
    }

    #[test]
    fn core_tokens_match_core_serde() {
        assert_eq!(MatchResult::Win.core_token(), "win");
        assert_eq!(MatchResult::Draw.core_token(), "draw");
        assert_eq!(MatchResult::Loss.core_token(), "loss");
        assert_eq!(MatchResult::Dnf.core_token(), "dnf");
        assert_eq!(MatchResult::NoContest.core_token(), "no_contest");
    }

    #[test]
    fn parse_match_end_reads_reason_and_goals() {
        let body = br#"{"matchReportId":7,"endReason":"WIN","myMatchStats":[3,1,2,0,0,0,0,0,0,0,0,0,0,0,0],"opponentMatchStats":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}"#;
        let end = parse_match_end(body).expect("parses");
        assert_eq!(end.result, MatchResult::Win);
        assert_eq!(end.match_report_id, 7);
        assert_eq!(end.goals_for, 3);
        assert_eq!(end.goals_against, 1);
    }

    #[test]
    fn parse_match_end_dnf_omits_stats_as_zero() {
        // The live DNF payload: stats arrays omitted entirely.
        let body = br#"{"matchReportId":0,"endReason":"DNF","items":[],"matchData":"","matchStatusFlags":0}"#;
        let end = parse_match_end(body).expect("parses");
        assert_eq!(end.result, MatchResult::Dnf);
        assert_eq!(end.goals_for, 0);
        assert_eq!(end.goals_against, 0);
    }

    #[test]
    fn parse_match_end_unknown_reason_defaults_draw() {
        let end = parse_match_end(br#"{"endReason":"BANANA"}"#).expect("parses");
        assert_eq!(end.result, MatchResult::Draw);
        assert_eq!(end.end_reason.as_deref(), Some("BANANA"));
    }

    #[test]
    fn parse_match_end_rejects_malformed() {
        assert!(parse_match_end(b"not json").is_none());
        assert!(parse_match_end(b"[]").is_none());
        assert!(parse_match_end(b"42").is_none());
    }

    #[test]
    fn reward_response_has_only_reversed_fields() {
        let body = reward_response(29_876_876, 100);
        assert_eq!(body["allCoins"], 29_876_876);
        assert_eq!(body["matchCoins"], 100);
        assert_eq!(body["gameModeAward"]["coins"], 100);
        assert_eq!(body["seasonCoins"], 0);
        assert_eq!(body["tournamentCoins"], 0);
        assert_eq!(body["boostConis"], 0);
        assert_eq!(body["participationAward"], 0);
        assert_eq!(body["teamOfTournamentWinner"], false);
        // The freeze traps must never appear.
        assert!(body.get("bidTokens").is_none());
        assert!(body.get("qualifiedChampionEventId").is_none());
    }

    #[test]
    fn create_response_shape() {
        let body = create_response(100_004_838, 1_700_000_000);
        assert_eq!(body["id"], 100_004_838);
        assert_eq!(body["reportIdEnabled"], false);
        assert_eq!(body["startDateTime"], 1_700_000_000);
    }
}
