//! Durable FIFA 17 club identity backed by the shared `fut_account.json` shape.
//!
//! The Python oracle and the launcher use snake-case keys in one account file.
//! Rust owns the rename mutation now, but preserves every unrelated account key
//! so Blaze/POW and rollback keep reading the same source of truth.

use std::fs::{self, OpenOptions};
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

use parking_lot::Mutex;
use serde_json::{Map, Value};

pub const DEFAULT_CLUB_NAME: &str = "OpenFUT";
pub const DEFAULT_CLUB_ABBR: &str = "OFC";
pub const DEFAULT_ESTABLISHED: &str = "2026";

const CLUB_NAME_MIN: usize = 5;
const CLUB_NAME_MAX: usize = 15;
const CLUB_ABBR_MIN: usize = 1;
const CLUB_ABBR_MAX: usize = 3;

/// The FIFA-facing club fields read by `user`, `userMassInfo`, and account sync.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClubIdentity {
    pub name: String,
    pub abbr: String,
    pub established: String,
}

type FileSignature = (u64, u64, i64, i64, u64);

#[derive(Debug)]
struct AccountState {
    raw: Map<String, Value>,
    club: ClubIdentity,
    signature: Option<FileSignature>,
}

/// Result of applying a rename body. Every result maps to the protocol's `200 {}`;
/// the distinction exists for logging and tests, not for changing wire behavior.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenameOutcome {
    Updated,
    Unchanged,
    Rejected(&'static str),
    PersistFailed(String),
}

/// Thread-safe account state persisted by atomic replacement on a valid rename.
pub struct AccountStore {
    path: PathBuf,
    state: Mutex<AccountState>,
}

impl AccountStore {
    /// Load the shared account file. Missing or malformed files use the proven
    /// production defaults; the first valid rename creates/repairs the file.
    pub fn open(path: impl Into<PathBuf>) -> Self {
        let path = path.into();
        let raw = read_raw(&path);
        let club = club_from_raw(&raw);
        let signature = file_signature(&path);
        Self {
            path,
            state: Mutex::new(AccountState {
                raw,
                club,
                signature,
            }),
        }
    }

    pub fn club(&self) -> ClubIdentity {
        let mut state = self.state.lock();
        self.refresh_if_changed(&mut state);
        state.club.clone()
    }

    /// Apply `{clubName, clubAbbr}` with the exact Python-oracle semantics:
    /// either field may be omitted/null, invalid input is ignored, and the caller
    /// always acknowledges with `200 {}`. A valid change is durable before it
    /// becomes visible to Rust readers.
    pub fn rename_from_body(&self, body: &[u8]) -> RenameOutcome {
        let value: Value = match serde_json::from_slice(body) {
            Ok(value) => value,
            Err(_) => return RenameOutcome::Rejected("malformed_json"),
        };
        let Value::Object(request) = value else {
            return RenameOutcome::Rejected("body_not_object");
        };

        let mut state = self.state.lock();
        self.refresh_if_changed(&mut state);
        let name = match optional_string(&request, "clubName") {
            Ok(Some(value)) => value.trim().to_owned(),
            Ok(None) => state.club.name.clone(),
            Err(()) => return RenameOutcome::Rejected("club_name_not_string"),
        };
        let abbr = match optional_string(&request, "clubAbbr") {
            Ok(Some(value)) => value.trim().to_owned(),
            Ok(None) => state.club.abbr.clone(),
            Err(()) => return RenameOutcome::Rejected("club_abbr_not_string"),
        };

        if !valid_name(&name) {
            return RenameOutcome::Rejected("club_name_length");
        }
        if !valid_abbr(&abbr) {
            return RenameOutcome::Rejected("club_abbr_length");
        }
        if name == state.club.name && abbr == state.club.abbr {
            return RenameOutcome::Unchanged;
        }

        let mut updated = state.raw.clone();
        updated.insert("club_name".into(), Value::String(name.clone()));
        updated.insert("club_abbr".into(), Value::String(abbr.clone()));
        let bytes = match serde_json::to_vec_pretty(&updated) {
            Ok(bytes) => bytes,
            Err(error) => return RenameOutcome::PersistFailed(error.to_string()),
        };
        if let Err(error) = persist_atomically(&self.path, &bytes) {
            return RenameOutcome::PersistFailed(error.to_string());
        }

        state.raw = updated;
        state.club.name = name;
        state.club.abbr = abbr;
        state.signature = file_signature(&self.path);
        RenameOutcome::Updated
    }

    fn refresh_if_changed(&self, state: &mut AccountState) {
        let signature = file_signature(&self.path);
        if signature == state.signature {
            return;
        }
        let raw = read_raw(&self.path);
        state.club = club_from_raw(&raw);
        state.raw = raw;
        state.signature = file_signature(&self.path);
    }
}

fn read_raw(path: &Path) -> Map<String, Value> {
    fs::read(path)
        .ok()
        .and_then(|bytes| serde_json::from_slice(&bytes).ok())
        .unwrap_or_default()
}

fn club_from_raw(raw: &Map<String, Value>) -> ClubIdentity {
    ClubIdentity {
        name: valid_stored(raw, "club_name", valid_name)
            .unwrap_or(DEFAULT_CLUB_NAME)
            .to_owned(),
        abbr: valid_stored(raw, "club_abbr", valid_abbr)
            .unwrap_or(DEFAULT_CLUB_ABBR)
            .to_owned(),
        established: stored_established(raw).unwrap_or_else(|| DEFAULT_ESTABLISHED.to_owned()),
    }
}

fn file_signature(path: &Path) -> Option<FileSignature> {
    let metadata = fs::metadata(path).ok()?;
    Some((
        metadata.dev(),
        metadata.ino(),
        metadata.mtime(),
        metadata.mtime_nsec(),
        metadata.len(),
    ))
}

fn optional_string<'a>(object: &'a Map<String, Value>, key: &str) -> Result<Option<&'a str>, ()> {
    match object.get(key) {
        None | Some(Value::Null) => Ok(None),
        Some(Value::String(value)) => Ok(Some(value)),
        Some(_) => Err(()),
    }
}

fn valid_stored<'a>(
    raw: &'a Map<String, Value>,
    key: &str,
    valid: fn(&str) -> bool,
) -> Option<&'a str> {
    raw.get(key)
        .and_then(Value::as_str)
        .filter(|value| valid(value))
}

fn stored_established(raw: &Map<String, Value>) -> Option<String> {
    match raw.get("established")? {
        Value::String(value) if valid_established(value) => Some(value.clone()),
        Value::Number(value) => value.as_u64().map(|year| year.to_string()),
        _ => None,
    }
}

fn valid_name(value: &str) -> bool {
    let len = value.chars().count();
    (CLUB_NAME_MIN..=CLUB_NAME_MAX).contains(&len) && value == value.trim()
}

fn valid_abbr(value: &str) -> bool {
    let len = value.chars().count();
    (CLUB_ABBR_MIN..=CLUB_ABBR_MAX).contains(&len) && value == value.trim()
}

fn valid_established(value: &str) -> bool {
    !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())
}

fn persist_atomically(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
    if let Some(parent) = path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
    {
        fs::create_dir_all(parent)?;
    }
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let temp = path.with_extension(format!(
        "openfut-tmp-{}-{}",
        std::process::id(),
        COUNTER.fetch_add(1, Ordering::Relaxed)
    ));
    let result = (|| {
        let mut file = OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(&temp)?;
        file.write_all(bytes)?;
        file.sync_all()?;
        drop(file);
        fs::rename(&temp, path)
    })();
    if result.is_err() {
        let _ = fs::remove_file(&temp);
    }
    result
}

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

    fn temp_path(tag: &str) -> PathBuf {
        std::env::temp_dir().join(format!(
            "openfut-account-store-{}-{tag}.json",
            std::process::id()
        ))
    }

    #[test]
    fn valid_rename_preserves_unrelated_account_fields_and_reopens() {
        let path = temp_path("persist");
        let _ = fs::remove_file(&path);
        fs::write(
            &path,
            serde_json::to_vec(&json!({
                "persona_id": 33068179,
                "persona_name": "CAGE",
                "club_name": "OpenFUT",
                "club_abbr": "OFC",
                "established": 2016,
                "pow_level": 7
            }))
            .unwrap(),
        )
        .unwrap();

        let store = AccountStore::open(&path);
        assert_eq!(
            store.rename_from_body(br#"{"clubName":" Real FUT ","clubAbbr":"RF"}"#),
            RenameOutcome::Updated
        );
        assert_eq!(
            store.club(),
            ClubIdentity {
                name: "Real FUT".into(),
                abbr: "RF".into(),
                established: "2016".into()
            }
        );

        let raw: Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
        assert_eq!(raw["persona_id"], 33_068_179);
        assert_eq!(raw["persona_name"], "CAGE");
        assert_eq!(raw["pow_level"], 7);
        assert_eq!(raw["club_name"], "Real FUT");
        assert_eq!(raw["club_abbr"], "RF");
        assert_eq!(AccountStore::open(&path).club(), store.club());
        let _ = fs::remove_file(path);
    }

    #[test]
    fn external_atomic_replacement_is_reloaded_before_read_and_rename() {
        let path = temp_path("external-replace");
        let _ = fs::remove_file(&path);
        fs::write(
            &path,
            serde_json::to_vec(&json!({
                "club_name": "OpenFUT",
                "club_abbr": "OFC",
                "pow_level": 7
            }))
            .unwrap(),
        )
        .unwrap();
        let store = AccountStore::open(&path);

        let replacement = serde_json::to_vec(&json!({
            "club_name": "Other Club",
            "club_abbr": "OC",
            "established": "2015",
            "pow_level": 11
        }))
        .unwrap();
        persist_atomically(&path, &replacement).unwrap();
        assert_eq!(
            store.club(),
            ClubIdentity {
                name: "Other Club".into(),
                abbr: "OC".into(),
                established: "2015".into()
            }
        );

        assert_eq!(
            store.rename_from_body(br#"{"clubName":"Third Club"}"#),
            RenameOutcome::Updated
        );
        let raw: Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
        assert_eq!(raw["club_name"], "Third Club");
        assert_eq!(raw["club_abbr"], "OC");
        assert_eq!(raw["pow_level"], 11);
        let _ = fs::remove_file(path);
    }

    #[test]
    fn partial_invalid_and_malformed_renames_match_oracle_no_change_semantics() {
        let path = temp_path("reject");
        let _ = fs::remove_file(&path);
        let store = AccountStore::open(&path);

        assert_eq!(
            store.rename_from_body(br#"{"clubName":"Open FUT"}"#),
            RenameOutcome::Updated
        );
        assert_eq!(store.club().abbr, DEFAULT_CLUB_ABBR);
        let before = store.club();
        assert_eq!(
            store.rename_from_body(br#"{"clubName":"x","clubAbbr":"TOOLONG"}"#),
            RenameOutcome::Rejected("club_name_length")
        );
        assert_eq!(
            store.rename_from_body(br#"{"clubName":3}"#),
            RenameOutcome::Rejected("club_name_not_string")
        );
        assert_eq!(
            store.rename_from_body(b"not-json"),
            RenameOutcome::Rejected("malformed_json")
        );
        assert_eq!(store.club(), before);
        let _ = fs::remove_file(path);
    }
}
