//! The shared FIFA 17 FUT **item-shaping primitive**.
//!
//! One function shapes one owned FUT item into the numeric card object the FIFA
//! 17 client renders, and **every** route that emits a player item goes through
//! it — `/club` (via [`crate::fut::club_response`]) and squad projection (via
//! [`crate::fut::squad_projection`]) alike. There is deliberately no second copy
//! of the field set: an item is shaped in exactly one place so the two routes
//! can never drift.
//!
//! ## The asset-id boundary (load-bearing, evidence-grounded)
//!
//! FIFA renders a card by resolving `resourceId & 0xffffff` against the client's
//! OWN local players table (proven live): a real id renders a real footballer,
//! an **invented id renders a blank generic card**. OpenFUT Core's catalogue is
//! synthetic string ids (`card_pl_001`) with no FIFA asset id. So an
//! [`ItemIdentityResolver`] is injected; when it cannot supply a **real** FIFA
//! asset id for an item, the item carries no fabricated identity — the caller
//! decides what that means (`/club` drops and counts it; a squad refuses to
//! project a starter it cannot render, never faking one).
//!
//! Entity ids (`leagueId`/`teamid`/`nation`) come from a reverse resolver; an
//! unresolved name yields a neutral `0` (a valid int — non-fatal; it only means
//! "no badge/flag"), because those are not the identity the renderer keys on.

use serde_json::{json, Value};

use crate::fut::entities::ReverseEntityResolver;

/// One owned item in game-independent terms, as read from Core's inventory.
#[derive(Debug, Clone)]
pub struct CoreOwnedItem {
    /// Core owned-instance id (string). The stable per-copy identity — two
    /// copies of the same card definition have distinct `owned_card_id`s.
    pub owned_card_id: String,
    /// Core card-definition id (string), used for asset resolution.
    pub card_id: String,
    /// Effective overall rating.
    pub rating: u8,
    /// Effective position, e.g. "ST".
    pub position: String,
    pub nation: String,
    pub league: String,
    pub club: String,
    /// [pace, shooting, passing, dribbling, defending, physical].
    pub attributes: [u8; 6],
}

/// The FIFA-side numeric identity of an owned item. `asset_id` MUST be a real
/// FIFA player asset (low 24 bits the client resolves); `item_id` is the wire
/// instance id used for later item operations. Two owned copies of the same
/// definition share an `asset_id` but MUST have distinct `item_id`s.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Fifa17Identity {
    pub item_id: u32,
    /// Base FIFA player asset (low 24 bits the client resolves art/name from).
    pub asset_id: u32,
    /// Full versioned resource id = `(version << 24) | asset_id`. Equals
    /// `asset_id` for a version-0 base card. This is the wire
    /// `resourceId`/`definitionId`, kept DISTINCT from `asset_id` so a versioned
    /// (special) card never collapses onto its base on the wire.
    pub resource_id: u32,
    /// FIFA wire `rareflag` — the card's rare/special TYPE (e.g. 3=inform,
    /// 21..=24 = special programmes). Drives the client's special-card art;
    /// carried from the catalog, never hardcoded, so specials render as specials.
    pub rareflag: i64,
}

/// Supplies the FIFA numeric identity for a Core item. Returning `None` means
/// "no real FIFA asset id known" → the caller must not fabricate one.
pub trait ItemIdentityResolver {
    fn resolve(&self, item: &CoreOwnedItem) -> Option<Fifa17Identity>;
}

/// Diagnostics from shaping (safe to log — counts only).
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ShapeStats {
    pub emitted: usize,
    pub dropped_no_asset: usize,
}

/// Quick-sell / discard value by rating tier (mirrors Core's quick-sell table;
/// non-fatal display field).
fn discard_value(rating: u8) -> i64 {
    match rating {
        r if r >= 85 => 1500,
        r if r >= 80 => 900,
        r if r >= 75 => 600,
        r if r >= 65 => 300,
        _ => 150,
    }
}

/// Build one FIFA `_item` object. `resourceId`/`definitionId` carry the full
/// versioned resource id; `assetId`/`cardassetid` carry the base asset. For a
/// version-0 base card these coincide; for a special they differ and MUST NOT
/// be collapsed.
///
/// This is the single source of truth for a player item's on-wire shape; the
/// `/club` envelope and squad projection both call it, so their items are
/// identical by construction. `id` is the owned instance's resolved FIFA
/// identity — pass the resolver's answer for *this* owned copy so two copies of
/// one definition stay distinct on the wire.
pub fn shape_item(
    item: &CoreOwnedItem,
    id: Fifa17Identity,
    ent: &impl ReverseEntityResolver,
) -> Value {
    let asset = id.asset_id;
    let league_id = ent.league_id(&item.league).unwrap_or(0);
    let team_id = ent.team_id(&item.club).unwrap_or(0);
    let nation_id = ent.nation_id(&item.nation).unwrap_or(0);
    let attribute_list: Vec<Value> = item
        .attributes
        .iter()
        .enumerate()
        .map(|(i, v)| json!({ "index": i, "value": v }))
        .collect();
    json!({
        "id": id.item_id,
        "resourceId": id.resource_id,
        "assetId": asset,
        "cardassetid": asset,
        "definitionId": id.resource_id,
        "cardsubtypeid": 0,
        "itemType": "player",
        "rareflag": id.rareflag,
        "rating": item.rating,
        "preferredPosition": item.position,
        "nation": nation_id,
        "teamid": team_id,
        "leagueId": league_id,
        "playStyle": 250,
        "attributeList": attribute_list,
        "itemState": "free",
        "owners": 1,
        "untradeable": true,
        "contract": 7,
        "fitness": 99,
        "discardValue": discard_value(item.rating),
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fut::entities::Fifa17Entities;
    use std::collections::HashMap;

    fn entities() -> Fifa17Entities {
        Fifa17Entities::from_maps(
            HashMap::from([(13, "Premier League".to_string())]),
            HashMap::from([(52, "Argentina".to_string())]),
            HashMap::from([(5, "Chelsea".to_string())]),
        )
    }

    fn item(owned: &str, card: &str, rating: u8, pos: &str) -> CoreOwnedItem {
        CoreOwnedItem {
            owned_card_id: owned.into(),
            card_id: card.into(),
            rating,
            position: pos.into(),
            nation: "Argentina".into(),
            league: "Premier League".into(),
            club: "Chelsea".into(),
            attributes: [90, 88, 70, 85, 40, 78],
        }
    }

    #[test]
    fn shapes_real_identity_and_reverse_entity_ids() {
        let ent = entities();
        let it = shape_item(
            &item("oc1", "card_ch_1", 86, "CDM"),
            Fifa17Identity {
                item_id: 100000001,
                asset_id: 20801,
                resource_id: 20801,
                rareflag: 1,
            },
            &ent,
        );
        assert_eq!(it["id"], 100000001, "wire instance id");
        assert_eq!(it["resourceId"], 20801);
        assert_eq!(it["assetId"], 20801);
        assert_eq!(
            it["definitionId"], 20801,
            "version byte 0 => resourceId==assetId==definitionId"
        );
        assert_eq!(it["rating"], 86);
        assert_eq!(it["preferredPosition"], "CDM");
        assert_eq!(it["leagueId"], 13);
        assert_eq!(it["teamid"], 5);
        assert_eq!(it["nation"], 52);
        assert_eq!(it["itemType"], "player");
        assert_eq!(it["attributeList"].as_array().unwrap().len(), 6);
        assert_eq!(it["attributeList"][0], json!({"index":0,"value":90}));
    }

    #[test]
    fn two_owned_copies_of_one_definition_stay_distinct_on_the_wire() {
        // fifa17_101490 has two owned instances: same definition/asset, two
        // distinct owned ids and two distinct wire ids. Shaping each from its
        // own identity must NEVER collapse them.
        let ent = entities();
        let a = shape_item(
            &item("oc-a", "fifa17_101490", 84, "ST"),
            Fifa17Identity {
                item_id: 100000030,
                asset_id: 101490,
                resource_id: 101490,
                rareflag: 1,
            },
            &ent,
        );
        let b = shape_item(
            &item("oc-b", "fifa17_101490", 84, "ST"),
            Fifa17Identity {
                item_id: 100000031,
                asset_id: 101490,
                resource_id: 101490,
                rareflag: 1,
            },
            &ent,
        );
        assert_eq!(
            a["resourceId"], b["resourceId"],
            "same definition => same asset"
        );
        assert_ne!(
            a["id"], b["id"],
            "distinct owned copies keep distinct wire ids"
        );
        assert_eq!(a["id"], 100000030);
        assert_eq!(b["id"], 100000031);
    }

    #[test]
    fn versioned_special_keeps_resourceid_distinct_from_assetid() {
        // A versioned (special) card: resourceId/definitionId carry the full
        // versioned id; assetId/cardassetid stay the base asset. They MUST NOT
        // collapse. (resource 117617092 = version 7 of asset 176580.)
        let ent = entities();
        let it = shape_item(
            &item("oc-v", "fifa17_117617092", 92, "ST"),
            Fifa17Identity {
                item_id: 100000384,
                asset_id: 176580,
                resource_id: 117617092,
                rareflag: 3,
            },
            &ent,
        );
        assert_eq!(
            it["resourceId"], 117617092,
            "versioned resource id on the wire"
        );
        assert_eq!(it["definitionId"], 117617092);
        assert_eq!(it["assetId"], 176580, "base asset id preserved");
        assert_eq!(it["cardassetid"], 176580);
        assert_eq!(
            it["rareflag"], 3,
            "special rareflag carried, not hardcoded 1"
        );
    }
}
