//! Shared helpers for encrypted paste payloads.

use anyhow::{Context, Result};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;

/// Decode the shared paste key from raw bytes, hex, or base64.
///
/// Inputs: the raw operator-supplied secret, optionally prefixed with `hex:` or
/// `raw:`.
/// Outputs: a 32-byte key suitable for ChaCha20-Poly1305.
/// # Errors
///
/// Returns an error when the input is not valid base64/hex or does not decode
/// to exactly 32 bytes, and is not already an exact 32-byte secret.
/// Why: both the client and server enforce the same secret format, so this
/// logic lives in one place instead of drifting across crates.
pub fn decode_shared_key(raw: &str) -> Result<[u8; 32]> {
    let trimmed = raw.trim();
    let bytes = if let Some(payload) = trimmed.strip_prefix("raw:") {
        payload.as_bytes().to_vec()
    } else {
        let payload = trimmed.strip_prefix("hex:").unwrap_or(trimmed);
        if payload.len() == 64 && payload.chars().all(|c| c.is_ascii_hexdigit()) {
            hex_to_bytes(payload)?
        } else {
            match STANDARD.decode(payload.as_bytes()) {
                Ok(decoded) if decoded.len() == 32 => decoded,
                Ok(_) | Err(_) if payload.len() == 32 => payload.as_bytes().to_vec(),
                Ok(_) => anyhow::bail!("LESAVKA_PASTE_KEY must decode to 32 bytes"),
                Err(err) => {
                    return Err(err).context(
                        "LESAVKA_PASTE_KEY must be a 32-byte raw secret, 32-byte base64, or 64-char hex",
                    );
                }
            }
        }
    };

    if bytes.len() != 32 {
        anyhow::bail!("LESAVKA_PASTE_KEY must decode to 32 bytes");
    }

    let mut key = [0u8; 32];
    key.copy_from_slice(&bytes);
    Ok(key)
}

/// Trim paste text to the configured character budget.
///
/// Inputs: the full clipboard text plus the maximum character count allowed by
/// the current deployment.
/// Outputs: the original text when already within the limit, or a truncated
/// clone that contains exactly the leading `max_chars` Unicode scalar values.
/// Why: client and server need the same truncation semantics so operators see
/// predictable behavior regardless of where the limit is enforced first.
#[must_use]
pub fn truncate_text(text: &str, max_chars: usize) -> String {
    if text.chars().count() <= max_chars {
        text.to_string()
    } else {
        text.chars().take(max_chars).collect()
    }
}

fn hex_to_bytes(raw: &str) -> Result<Vec<u8>> {
    let chars: Vec<char> = raw.chars().collect();
    let mut bytes = Vec::with_capacity(chars.len() / 2);

    for index in (0..chars.len()).step_by(2) {
        let hi = chars[index].to_digit(16).context("hex decode failed")?;
        let lo = chars[index + 1].to_digit(16).context("hex decode failed")?;
        bytes.push(u8::try_from((hi << 4) | lo).context("hex decode failed")?);
    }

    Ok(bytes)
}

#[cfg(test)]
mod tests {
    use super::{decode_shared_key, truncate_text};

    const HEX_KEY: &str = "hex:00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
    const B64_KEY: &str = "ABEiM0RVZneImaq7zN3u/wARIjNEVWZ3iJmqu8zd7v8=";
    const RAW_KEY: &str = "0123456789abcdef0123456789abcdef";

    #[test]
    fn decode_shared_key_accepts_hex_and_base64() {
        let hex = decode_shared_key(HEX_KEY).expect("hex key should decode");
        let base64 = decode_shared_key(B64_KEY).expect("base64 key should decode");
        assert_eq!(hex, base64);
        assert_eq!(hex[0], 0x00);
        assert_eq!(hex[31], 0xff);
    }

    #[test]
    fn decode_shared_key_accepts_raw_32_byte_secret() {
        let raw = decode_shared_key(RAW_KEY).expect("raw key should decode");
        let explicit = decode_shared_key("raw:0123456789abcdef0123456789abcdef")
            .expect("raw-prefixed key should decode");
        assert_eq!(raw, explicit);
        assert_eq!(raw, *RAW_KEY.as_bytes());
    }

    #[test]
    fn decode_shared_key_rejects_short_input() {
        let error = decode_shared_key("Zm9v").expect_err("short key must fail");
        assert!(error.to_string().contains("32 bytes"));
    }

    #[test]
    fn decode_shared_key_rejects_invalid_hex_payload() {
        let error = decode_shared_key(
            "hex:00112233445566778899aabbccddeeff00112233445566778899aabbccddeefg",
        )
        .expect_err("invalid hex key must fail");
        assert!(error.to_string().contains("32 bytes"));
    }

    #[test]
    fn decode_shared_key_rejects_wrong_length_raw_prefix() {
        let error = decode_shared_key("raw:too-short").expect_err("short raw key must fail");
        assert!(error.to_string().contains("32 bytes"));
    }

    #[test]
    fn truncate_text_preserves_unicode_boundaries() {
        assert_eq!(truncate_text("abc", 10), "abc");
        assert_eq!(truncate_text("naïve", 4), "naïv");
        assert_eq!(truncate_text("🙂🙂🙂", 2), "🙂🙂");
    }
}
