// Integrity-protected cache for cached account info.
//
// The user's subscription tier is used by `license_commands::current_edition`
// to unlock Premium features. Until this module, the subscription_status
// claim was read directly from a plaintext `account.json` on disk, which
// meant any local process (malware, nosy user, curl) could write
// `{"subscription_status": "active"}` and bypass the paywall without
// ever touching the Logto session. This module closes that trap.
//
// Approach: an HMAC-SHA256 signature is computed over the serialized
// AccountInfo bytes using a per-install key stored in the OS keychain
// (via the `keyring` crate, same backend as token_store). The signed
// payload is wrapped as `{"data": {...}, "sig": "<base64>"}`. On read,
// verification requires the same key; tampering with either `data` or
// `sig` invalidates the cache.
//
// Trust chain:
// - Key lives in the keychain, scoped to service "com.simpl.resultat",
//   user "account-hmac-key". Compromising it requires compromising the
//   keychain, which is the existing trust boundary for OAuth tokens.
// - A legacy unsigned `account.json` (from v0.7.x) is still readable
//   for display purposes (email, name, picture), but the gating path
//   uses `load_verified` which returns None for legacy payloads —
//   Premium features stay locked until the next token refresh rewrites
//   the file with a signature.

use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::fs;

use super::auth_commands::AccountInfo;
use super::token_store::{auth_dir, write_restricted};

const KEYCHAIN_SERVICE: &str = "com.simpl.resultat";
const KEYCHAIN_USER_HMAC: &str = "account-hmac-key";
const ACCOUNT_FILE: &str = "account.json";

type HmacSha256 = Hmac<Sha256>;

#[derive(Debug, Serialize, Deserialize)]
struct SignedAccount {
    data: AccountInfo,
    sig: String,
}

/// Read the HMAC key from the keychain, creating a fresh random key on
/// first use. The key is never persisted to disk — if the keychain is
/// unreachable, the whole cache signing/verification path falls back
/// to "not signed" which means gating stays locked until the keychain
/// is available again. This is intentional (fail-closed).
fn get_or_create_key() -> Result<[u8; 32], String> {
    let entry = keyring::Entry::new(KEYCHAIN_SERVICE, KEYCHAIN_USER_HMAC)
        .map_err(|e| format!("Keychain entry init failed: {}", e))?;
    match entry.get_password() {
        Ok(b64) => decode_key(&b64),
        Err(keyring::Error::NoEntry) => {
            use rand::RngCore;
            let mut key = [0u8; 32];
            rand::thread_rng().fill_bytes(&mut key);
            let encoded = encode_key(&key);
            entry
                .set_password(&encoded)
                .map_err(|e| format!("Keychain HMAC key write failed: {}", e))?;
            Ok(key)
        }
        Err(e) => Err(format!("Keychain HMAC key read failed: {}", e)),
    }
}

fn encode_key(key: &[u8]) -> String {
    use base64::{engine::general_purpose::STANDARD, Engine};
    STANDARD.encode(key)
}

fn decode_key(raw: &str) -> Result<[u8; 32], String> {
    use base64::{engine::general_purpose::STANDARD, Engine};
    let bytes = STANDARD
        .decode(raw.trim())
        .map_err(|e| format!("Invalid HMAC key encoding: {}", e))?;
    if bytes.len() != 32 {
        return Err(format!(
            "HMAC key must be 32 bytes, got {}",
            bytes.len()
        ));
    }
    let mut arr = [0u8; 32];
    arr.copy_from_slice(&bytes);
    Ok(arr)
}

fn sign(key: &[u8; 32], payload: &[u8]) -> String {
    use base64::{engine::general_purpose::STANDARD, Engine};
    let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can take any key length");
    mac.update(payload);
    STANDARD.encode(mac.finalize().into_bytes())
}

fn verify(key: &[u8; 32], payload: &[u8], sig_b64: &str) -> bool {
    use base64::{engine::general_purpose::STANDARD, Engine};
    let Ok(sig_bytes) = STANDARD.decode(sig_b64) else {
        return false;
    };
    let Ok(mut mac) = HmacSha256::new_from_slice(key) else {
        return false;
    };
    mac.update(payload);
    mac.verify_slice(&sig_bytes).is_ok()
}

/// Write the account cache as `{"data": {...}, "sig": "..."}`. The key
/// is fetched from (or created in) the keychain. Writes fall back to an
/// unsigned legacy-shape payload only when the keychain is unreachable
/// — this keeps the UI functional on keychain-less systems but means
/// the gating path will refuse to grant Premium until the keychain
/// comes back.
pub fn save(app: &tauri::AppHandle, account: &AccountInfo) -> Result<(), String> {
    let dir = auth_dir(app)?;
    let path = dir.join(ACCOUNT_FILE);

    match get_or_create_key() {
        Ok(key) => {
            // Serialise the AccountInfo alone (compact, deterministic
            // for a given struct layout), sign the bytes, then re-wrap
            // into the signed envelope. We write the signed envelope
            // as pretty-printed JSON for readability in the audit log.
            let data_bytes =
                serde_json::to_vec(account).map_err(|e| format!("Serialize error: {}", e))?;
            let sig = sign(&key, &data_bytes);
            let envelope = SignedAccount {
                data: account.clone(),
                sig,
            };
            let json = serde_json::to_string_pretty(&envelope)
                .map_err(|e| format!("Serialize envelope error: {}", e))?;
            write_restricted(&path, &json)
        }
        Err(err) => {
            eprintln!(
                "account_cache: keychain HMAC key unavailable, writing unsigned legacy payload ({})",
                err
            );
            // Fallback: unsigned payload. UI still works, but
            // `load_verified` will reject this file for gating.
            let json = serde_json::to_string_pretty(account)
                .map_err(|e| format!("Serialize error: {}", e))?;
            write_restricted(&path, &json)
        }
    }
}

/// Load the cached account for **display purposes**. Accepts both the
/// new signed envelope and legacy plaintext AccountInfo files. Does
/// NOT verify the signature — suitable for showing the user's name /
/// email / picture in the UI, but never for gating decisions.
pub fn load_unverified(app: &tauri::AppHandle) -> Result<Option<AccountInfo>, String> {
    let dir = auth_dir(app)?;
    let path = dir.join(ACCOUNT_FILE);
    if !path.exists() {
        return Ok(None);
    }
    let raw = fs::read_to_string(&path).map_err(|e| format!("Cannot read account: {}", e))?;

    // Prefer the signed envelope shape; fall back to legacy flat
    // AccountInfo so upgraded users see their account info immediately
    // rather than a blank card until the next token refresh.
    if let Ok(envelope) = serde_json::from_str::<SignedAccount>(&raw) {
        return Ok(Some(envelope.data));
    }
    if let Ok(flat) = serde_json::from_str::<AccountInfo>(&raw) {
        return Ok(Some(flat));
    }
    Err("Invalid account cache payload".to_string())
}

/// Load the cached account and verify the HMAC signature. Used by the
/// license gating path (`current_edition`). Returns Ok(None) when:
/// - no cache exists,
/// - the cache is in legacy unsigned shape (pre-v0.8 or post-fallback),
/// - the keychain HMAC key is unreachable,
/// - the signature does not verify.
///
/// Any of these states must cause Premium features to stay locked —
/// never accept an unverifiable payload for a gating decision.
pub fn load_verified(app: &tauri::AppHandle) -> Result<Option<AccountInfo>, String> {
    let dir = auth_dir(app)?;
    let path = dir.join(ACCOUNT_FILE);
    if !path.exists() {
        return Ok(None);
    }
    let raw = fs::read_to_string(&path).map_err(|e| format!("Cannot read account: {}", e))?;

    // Only signed envelopes are acceptable here. Legacy flat payloads
    // are treated as "no verified account".
    let Ok(envelope) = serde_json::from_str::<SignedAccount>(&raw) else {
        return Ok(None);
    };
    let Ok(key) = get_or_create_key() else {
        return Ok(None);
    };
    let data_bytes = match serde_json::to_vec(&envelope.data) {
        Ok(v) => v,
        Err(_) => return Ok(None),
    };
    if !verify(&key, &data_bytes, &envelope.sig) {
        return Ok(None);
    }
    Ok(Some(envelope.data))
}

/// Delete the cached account file AND the keychain HMAC key. Called on
/// logout so the next login generates a fresh key bound to the new
/// session.
pub fn delete(app: &tauri::AppHandle) -> Result<(), String> {
    let dir = auth_dir(app)?;
    let path = dir.join(ACCOUNT_FILE);
    if path.exists() {
        let _ = fs::remove_file(&path);
    }
    if let Ok(entry) = keyring::Entry::new(KEYCHAIN_SERVICE, KEYCHAIN_USER_HMAC) {
        let _ = entry.delete_credential();
    }
    Ok(())
}

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

    fn sample_account() -> AccountInfo {
        AccountInfo {
            email: "user@example.com".into(),
            name: Some("Test User".into()),
            picture: None,
            subscription_status: Some("active".into()),
        }
    }

    #[test]
    fn sign_then_verify_same_key() {
        let key = [7u8; 32];
        let payload = b"hello world";
        let sig = sign(&key, payload);
        assert!(verify(&key, payload, &sig));
    }

    #[test]
    fn verify_rejects_tampered_payload() {
        let key = [7u8; 32];
        let sig = sign(&key, b"original");
        assert!(!verify(&key, b"tampered", &sig));
    }

    #[test]
    fn verify_rejects_wrong_key() {
        let key_a = [7u8; 32];
        let key_b = [8u8; 32];
        let sig = sign(&key_a, b"payload");
        assert!(!verify(&key_b, b"payload", &sig));
    }

    #[test]
    fn envelope_roundtrip_serde() {
        let account = sample_account();
        let key = [3u8; 32];
        let data = serde_json::to_vec(&account).unwrap();
        let sig = sign(&key, &data);
        let env = SignedAccount {
            data: account.clone(),
            sig,
        };
        let json = serde_json::to_string(&env).unwrap();
        let decoded: SignedAccount = serde_json::from_str(&json).unwrap();
        let decoded_data = serde_json::to_vec(&decoded.data).unwrap();
        assert!(verify(&key, &decoded_data, &decoded.sig));
    }

    #[test]
    fn encode_decode_key_roundtrip() {
        let key = [42u8; 32];
        let encoded = encode_key(&key);
        let decoded = decode_key(&encoded).unwrap();
        assert_eq!(decoded, key);
    }

    #[test]
    fn decode_key_rejects_wrong_length() {
        use base64::{engine::general_purpose::STANDARD, Engine};
        let short = STANDARD.encode([1u8; 16]);
        assert!(decode_key(&short).is_err());
    }
}
