//! High-level paste operations.
//!
//! Each function combines local storage and DHT interaction
//! into a single call: put, get, delete, pin/unpin.

use std::time::Duration;

use tesseras_dht::Node;

use crate::base58;
use crate::crypto;
use crate::paste::{MAX_PASTE_SIZE, Paste};
use crate::store::PasteStore;

/// Timeout for blocking DHT lookups.
const OP_TIMEOUT: Duration = Duration::from_secs(30);

/// Errors from paste operations.
#[derive(Debug)]
pub enum PasteError {
    InvalidKey,
    NotFound,
    Expired,
    DecryptionFailed,
    TooLarge,
    Internal(String),
}

impl std::fmt::Display for PasteError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InvalidKey => write!(f, "invalid key"),
            Self::NotFound => write!(f, "not found"),
            Self::Expired => write!(f, "expired"),
            Self::DecryptionFailed => write!(f, "decryption failed"),
            Self::TooLarge => write!(f, "paste too large"),
            Self::Internal(msg) => write!(f, "internal: {msg}"),
        }
    }
}

/// Decode the hash portion of a key string ("hash#enckey" or "hash").
/// Returns the 32-byte hash.
fn parse_hash(key_str: &str) -> Result<Vec<u8>, PasteError> {
    let hash_b58 = key_str.split_once('#').map(|(h, _)| h).unwrap_or(key_str);
    let hash = base58::decode(hash_b58).ok_or(PasteError::InvalidKey)?;
    if hash.len() != 32 {
        return Err(PasteError::InvalidKey);
    }
    Ok(hash)
}

/// Store a paste. If `encrypt` is true, encrypts the content and
/// returns "hash#enckey" in base58. Otherwise returns just the hash.
pub fn put_paste(
    node: &mut Node,
    store: &PasteStore,
    content: &[u8],
    ttl_secs: u64,
    encrypt: bool,
) -> Result<String, PasteError> {
    if content.len() > MAX_PASTE_SIZE {
        return Err(PasteError::TooLarge);
    }

    let (paste_content, enc_key) = if encrypt {
        let key = crypto::generate_key();
        (crypto::encrypt(&key, content), Some(key))
    } else {
        (content.to_vec(), None)
    };

    let paste = Paste::new(paste_content, ttl_secs);
    let serialized = paste.to_bytes();
    let hash = Paste::content_key(&paste.content);

    store
        .put_paste(&hash, &serialized)
        .map_err(|e| PasteError::Internal(e.to_string()))?;

    let dht_ttl = std::cmp::min(ttl_secs, u16::MAX as u64) as u16;
    node.put(&hash, &serialized, dht_ttl, false);

    let hash_b58 = base58::encode(&hash);
    let label = if encrypt { "encrypted" } else { "public" };
    log::info!(
        "put: stored {label} paste {hash_b58} ({} bytes)",
        content.len()
    );

    match enc_key {
        Some(key) => Ok(format!("{hash_b58}#{}", base58::encode(&key))),
        None => Ok(hash_b58),
    }
}

/// Retrieve a paste by key ("hash#enckey" or bare "hash").
/// Tries local store first, then falls back to a blocking DHT lookup.
pub fn get_paste(
    node: &mut Node,
    store: &PasteStore,
    key_str: &str,
) -> Result<Vec<u8>, PasteError> {
    let (hash_b58, enc_key_b58) = match key_str.split_once('#') {
        Some((h, k)) => (h, Some(k)),
        None => (key_str, None),
    };

    let hash = base58::decode(hash_b58).ok_or(PasteError::InvalidKey)?;
    if hash.len() != 32 {
        return Err(PasteError::InvalidKey);
    }

    let data = if let Some(local) = store.get_paste(&hash) {
        local
    } else {
        let vals = node.get_blocking(&hash, OP_TIMEOUT);
        if vals.is_empty() {
            return Err(PasteError::NotFound);
        }
        // Verify DHT result: the content hash must match the
        // requested key to prevent a malicious node from
        // injecting arbitrary data.
        match vals.iter().find(|v| {
            Paste::from_bytes(v)
                .map(|p| Paste::content_key(&p.content) == *hash)
                .unwrap_or(false)
        }) {
            Some(v) => v.clone(),
            None => return Err(PasteError::NotFound),
        }
    };

    let paste = Paste::from_bytes(&data).ok_or(PasteError::InvalidKey)?;
    if paste.is_expired() && !store.is_pinned(&hash) {
        return Err(PasteError::Expired);
    }

    if let Some(kb58) = enc_key_b58 {
        let key_bytes = base58::decode(kb58).ok_or(PasteError::InvalidKey)?;
        if key_bytes.len() != crypto::KEY_SIZE {
            return Err(PasteError::InvalidKey);
        }
        let mut key = [0u8; crypto::KEY_SIZE];
        key.copy_from_slice(&key_bytes);
        crypto::decrypt(&key, &paste.content)
            .ok_or(PasteError::DecryptionFailed)
    } else {
        Ok(paste.content)
    }
}

/// Delete a paste from local store and the DHT.
pub fn delete_paste(
    node: &mut Node,
    store: &PasteStore,
    key_str: &str,
) -> Result<(), PasteError> {
    let hash = parse_hash(key_str)?;
    store.remove_paste(&hash);
    node.delete(&hash);
    let hash_b58 = key_str.split_once('#').map(|(h, _)| h).unwrap_or(key_str);
    log::info!("del: removed paste {hash_b58}");
    Ok(())
}

/// Parse a key and resolve the 32-byte hash (stripping any #enckey).
pub fn resolve_hash(key: &str) -> Result<Vec<u8>, PasteError> {
    parse_hash(key)
}
