//! Cached session token stored next to the state file.
//!
//! File format (no password): two lines — username and hex-encoded session token.
//! File format (with password): QPCE-encrypted version of the above.
//! The token has a server-side 24h TTL; no client-side expiry tracking.

use std::path::{Path, PathBuf};

use anyhow::Context;

use super::state::{decrypt_state, encrypt_state, is_encrypted_state};

pub struct CachedSession {
    pub username: String,
    pub token_hex: String,
}

/// Derive the session cache path: `{state_path}.session`.
fn session_cache_path(state_path: &Path) -> PathBuf {
    state_path.with_extension("session")
}

/// Parse the two-line format (username + token_hex) from plaintext bytes.
fn parse_session_lines(text: &str) -> Option<CachedSession> {
    let mut lines = text.lines();
    let username = lines.next()?.trim().to_string();
    let token_hex = lines.next()?.trim().to_string();
    if username.is_empty() || token_hex.is_empty() {
        return None;
    }
    if hex::decode(&token_hex).is_err() {
        return None;
    }
    Some(CachedSession { username, token_hex })
}

/// Load a cached session token. Returns None if file is missing or malformed.
/// Decrypts if the file is QPCE-encrypted (requires `password`).
pub fn load_cached_session(state_path: &Path, password: Option<&str>) -> Option<CachedSession> {
    let path = session_cache_path(state_path);
    let raw = std::fs::read(&path).ok()?;

    if is_encrypted_state(&raw) {
        let pw = password?;
        let plaintext = decrypt_state(pw, &raw).ok()?;
        let text = String::from_utf8(plaintext).ok()?;
        parse_session_lines(&text)
    } else {
        let text = String::from_utf8(raw).ok()?;
        parse_session_lines(&text)
    }
}

/// Save a session token to the cache file (mode 0o600 on Unix).
/// Encrypts with QPCE if `password` is provided.
pub fn save_cached_session(
    state_path: &Path,
    username: &str,
    token_hex: &str,
    password: Option<&str>,
) -> anyhow::Result<()> {
    let path = session_cache_path(state_path);
    let contents = format!("{username}\n{token_hex}\n");

    let bytes = match password {
        Some(pw) => encrypt_state(pw, contents.as_bytes())?,
        None => {
            #[cfg(not(unix))]
            tracing::warn!(
                "storing session token as plaintext (no password set); \
                 file permissions cannot be restricted on this platform"
            );
            contents.into_bytes()
        }
    };

    std::fs::write(&path, bytes).with_context(|| format!("write session cache {path:?}"))?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let perms = std::fs::Permissions::from_mode(0o600);
        std::fs::set_permissions(&path, perms).ok();
    }

    Ok(())
}

/// Remove the cached session file.
pub fn clear_cached_session(state_path: &Path) {
    let path = session_cache_path(state_path);
    std::fs::remove_file(&path).ok();
}

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

    #[test]
    fn plaintext_round_trip() {
        let dir = tempfile::tempdir().unwrap();
        let state_path = dir.path().join("state.bin");

        let token = hex::encode(b"session-token-bytes");
        save_cached_session(&state_path, "alice", &token, None).unwrap();
        let loaded = load_cached_session(&state_path, None).unwrap();
        assert_eq!(loaded.username, "alice");
        assert_eq!(loaded.token_hex, token);
    }

    #[test]
    fn encrypted_round_trip() {
        let dir = tempfile::tempdir().unwrap();
        let state_path = dir.path().join("state.bin");
        let password = "strong-password";

        let token = hex::encode(b"encrypted-token");
        save_cached_session(&state_path, "bob", &token, Some(password)).unwrap();

        // Encrypted file should start with QPCE magic
        let raw = std::fs::read(session_cache_path(&state_path)).unwrap();
        assert_eq!(&raw[..4], b"QPCE");

        let loaded = load_cached_session(&state_path, Some(password)).unwrap();
        assert_eq!(loaded.username, "bob");
        assert_eq!(loaded.token_hex, token);
    }

    #[test]
    fn wrong_password_returns_none() {
        let dir = tempfile::tempdir().unwrap();
        let state_path = dir.path().join("state.bin");

        let token = hex::encode(b"secret-token");
        save_cached_session(&state_path, "carol", &token, Some("correct")).unwrap();
        let result = load_cached_session(&state_path, Some("wrong"));
        assert!(result.is_none());
    }

    #[test]
    fn missing_file_returns_none() {
        let dir = tempfile::tempdir().unwrap();
        let state_path = dir.path().join("nonexistent.bin");
        assert!(load_cached_session(&state_path, None).is_none());
    }

    #[test]
    fn clear_removes_file() {
        let dir = tempfile::tempdir().unwrap();
        let state_path = dir.path().join("state.bin");

        let token = hex::encode(b"to-be-deleted");
        save_cached_session(&state_path, "dave", &token, None).unwrap();
        assert!(session_cache_path(&state_path).exists());

        clear_cached_session(&state_path);
        assert!(!session_cache_path(&state_path).exists());
    }

    #[test]
    fn malformed_content_returns_none() {
        let dir = tempfile::tempdir().unwrap();
        let state_path = dir.path().join("state.bin");
        let cache_path = session_cache_path(&state_path);

        // Not valid hex on second line
        std::fs::write(&cache_path, "alice\nnot-hex-data\n").unwrap();
        assert!(load_cached_session(&state_path, None).is_none());

        // Only one line
        std::fs::write(&cache_path, "alice\n").unwrap();
        assert!(load_cached_session(&state_path, None).is_none());

        // Empty file
        std::fs::write(&cache_path, "").unwrap();
        assert!(load_cached_session(&state_path, None).is_none());
    }
}
