//! uploadBlob / downloadBlob RPCs: chunked file transfer with SHA-256 integrity verification.

use std::io::{Read, Seek, SeekFrom, Write};
use std::path::PathBuf;

use capnp::capability::Promise;
use quicproquo_proto::node_capnp::node_service;
use sha2::{Digest, Sha256};

use crate::auth::{coded_error, fmt_hex, validate_auth_context};
use crate::error_codes::*;

use super::NodeServiceImpl;

/// Maximum blob size: 50 MB.
const MAX_BLOB_SIZE: u64 = 50 * 1024 * 1024;

/// Maximum download chunk size: 256 KB.
const MAX_DOWNLOAD_CHUNK: u32 = 256 * 1024;

/// Metadata stored alongside each completed blob.
#[derive(serde::Serialize, serde::Deserialize)]
struct BlobMeta {
    mime_type: String,
    total_size: u64,
    uploaded_at: u64,
    uploader_key_prefix: String,
}

/// Resolve the blobs directory from the server's data_dir.
fn blobs_dir(data_dir: &std::path::Path) -> PathBuf {
    data_dir.join("blobs")
}

impl NodeServiceImpl {
    pub fn handle_upload_blob(
        &mut self,
        params: node_service::UploadBlobParams,
        mut results: node_service::UploadBlobResults,
    ) -> Promise<(), capnp::Error> {
        let p = match params.get() {
            Ok(p) => p,
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };

        let auth_ctx = match validate_auth_context(&self.auth_cfg, &self.sessions, p.get_auth()) {
            Ok(ctx) => ctx,
            Err(e) => return Promise::err(e),
        };

        let blob_hash = match p.get_blob_hash() {
            Ok(v) => v.to_vec(),
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };
        let chunk = match p.get_chunk() {
            Ok(v) => v.to_vec(),
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };
        let offset = p.get_offset();
        let total_size = p.get_total_size();
        let mime_type = match p.get_mime_type() {
            Ok(v) => match v.to_str() {
                Ok(s) => s.to_string(),
                Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
            },
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };

        // Validate blobHash length.
        if blob_hash.len() != 32 {
            return Promise::err(coded_error(
                E025_BLOB_HASH_LENGTH,
                format!("blobHash must be exactly 32 bytes, got {}", blob_hash.len()),
            ));
        }

        // Validate totalSize.
        if total_size > MAX_BLOB_SIZE {
            return Promise::err(coded_error(
                E024_BLOB_TOO_LARGE,
                format!("totalSize {} exceeds max blob size ({} bytes)", total_size, MAX_BLOB_SIZE),
            ));
        }
        if total_size == 0 {
            return Promise::err(coded_error(E020_BAD_PARAMS, "totalSize must be > 0"));
        }

        // Validate chunk bounds.
        if offset.checked_add(chunk.len() as u64).is_none_or(|end| end > total_size) {
            return Promise::err(coded_error(
                E020_BAD_PARAMS,
                format!(
                    "chunk out of bounds: offset={} + chunk_len={} > totalSize={}",
                    offset,
                    chunk.len(),
                    total_size
                ),
            ));
        }

        let blob_hex = hex::encode(&blob_hash);
        let dir = blobs_dir(&self.data_dir);

        // Ensure blobs directory exists.
        if let Err(e) = std::fs::create_dir_all(&dir) {
            return Promise::err(coded_error(
                E009_STORAGE_ERROR,
                format!("failed to create blobs directory: {e}"),
            ));
        }

        let part_path = dir.join(format!("{blob_hex}.part"));
        let final_path = dir.join(&blob_hex);
        let meta_path = dir.join(format!("{blob_hex}.meta"));

        // All file I/O is delegated to spawn_blocking to avoid stalling the Tokio event loop.
        let uploader_prefix = auth_ctx
            .identity_key
            .as_deref()
            .filter(|k| k.len() >= 4)
            .map(|k| hex::encode(&k[..4]))
            .unwrap_or_default();

        Promise::from_future(async move {
            let blob_hash_clone = blob_hash.clone();
            let part_path_clone = part_path.clone();
            let final_path_clone = final_path.clone();
            let meta_path_clone = meta_path.clone();
            let chunk_clone = chunk;
            let mime_clone = mime_type.clone();
            let prefix_clone = uploader_prefix.clone();

            let io_result = tokio::task::spawn_blocking(move || -> Result<Option<bool>, String> {
                // If the blob already exists (fully uploaded), return immediately.
                if final_path_clone.exists() {
                    return Ok(None); // signals "already done"
                }

                // Write chunk at the given offset.
                let mut file = std::fs::OpenOptions::new()
                    .create(true)
                    .write(true)
                    .truncate(false)
                    .open(&part_path_clone)
                    .map_err(|e| format!("open .part file: {e}"))?;
                file.seek(SeekFrom::Start(offset))
                    .map_err(|e| format!("seek: {e}"))?;
                file.write_all(&chunk_clone)
                    .map_err(|e| format!("write chunk: {e}"))?;
                file.sync_all()
                    .map_err(|e| format!("sync: {e}"))?;

                // Check if the blob is complete.
                let end = offset + chunk_clone.len() as u64;
                if end == total_size {
                    // Verify SHA-256 of the complete file.
                    let mut vfile = std::fs::File::open(&part_path_clone)
                        .map_err(|e| format!("open for verify: {e}"))?;
                    let mut hasher = Sha256::new();
                    let mut buf = [0u8; 64 * 1024];
                    loop {
                        let n = vfile.read(&mut buf).map_err(|e| format!("read: {e}"))?;
                        if n == 0 {
                            break;
                        }
                        hasher.update(&buf[..n]);
                    }
                    let computed: [u8; 32] = hasher.finalize().into();
                    if computed != blob_hash_clone.as_slice() {
                        let _ = std::fs::remove_file(&part_path_clone);
                        return Ok(Some(false)); // hash mismatch
                    }

                    // Hash matches — finalize the blob.
                    std::fs::rename(&part_path_clone, &final_path_clone)
                        .map_err(|e| format!("rename .part to final: {e}"))?;

                    // Write metadata file.
                    let now = std::time::SystemTime::now()
                        .duration_since(std::time::UNIX_EPOCH)
                        .unwrap_or_default()
                        .as_secs();

                    let meta = BlobMeta {
                        mime_type: mime_clone,
                        total_size,
                        uploaded_at: now,
                        uploader_key_prefix: prefix_clone,
                    };

                    if let Err(e) = (|| -> Result<(), String> {
                        let json = serde_json::to_string_pretty(&meta)
                            .map_err(|e| format!("serialize meta: {e}"))?;
                        std::fs::write(&meta_path_clone, json.as_bytes())
                            .map_err(|e| format!("write meta: {e}"))?;
                        Ok(())
                    })() {
                        tracing::warn!(error = %e, "failed to write blob metadata");
                    }

                    return Ok(Some(true)); // complete + verified
                }

                Ok(None) // chunk written, not yet complete
            })
            .await
            .map_err(|e| capnp::Error::failed(format!("spawn_blocking join: {e}")))?;

            match io_result {
                Ok(None) => {
                    // Already existed or chunk written (not yet complete).
                    results.get().set_blob_id(&blob_hash);
                }
                Ok(Some(true)) => {
                    // Complete and verified.
                    tracing::info!(
                        blob_hash_prefix = %fmt_hex(&blob_hash[..4]),
                        total_size = total_size,
                        mime_type = %mime_type,
                        uploader_prefix = %uploader_prefix,
                        "audit: blob_upload_complete"
                    );
                    results.get().set_blob_id(&blob_hash);
                }
                Ok(Some(false)) => {
                    return Err(coded_error(
                        E026_BLOB_HASH_MISMATCH,
                        "SHA-256 of uploaded data does not match blobHash",
                    ));
                }
                Err(e) => {
                    return Err(coded_error(E009_STORAGE_ERROR, e));
                }
            }

            Ok(())
        })
    }

    pub fn handle_download_blob(
        &mut self,
        params: node_service::DownloadBlobParams,
        mut results: node_service::DownloadBlobResults,
    ) -> Promise<(), capnp::Error> {
        let p = match params.get() {
            Ok(p) => p,
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };

        if let Err(e) = validate_auth_context(&self.auth_cfg, &self.sessions, p.get_auth()) {
            return Promise::err(e);
        }

        let blob_id = match p.get_blob_id() {
            Ok(v) => v.to_vec(),
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };
        let offset = p.get_offset();
        let length = p.get_length().min(MAX_DOWNLOAD_CHUNK);

        if blob_id.len() != 32 {
            return Promise::err(coded_error(
                E025_BLOB_HASH_LENGTH,
                format!("blobId must be exactly 32 bytes, got {}", blob_id.len()),
            ));
        }

        let blob_hex = hex::encode(&blob_id);
        let dir = blobs_dir(&self.data_dir);
        let blob_path = dir.join(&blob_hex);
        let meta_path = dir.join(format!("{blob_hex}.meta"));

        // Delegate all file I/O to spawn_blocking to avoid stalling the event loop.
        Promise::from_future(async move {
            let io_result = tokio::task::spawn_blocking(move || -> Result<(Vec<u8>, BlobMeta), capnp::Error> {
                // Check that the blob exists.
                if !blob_path.exists() {
                    return Err(coded_error(E027_BLOB_NOT_FOUND, "blob not found"));
                }

                // Read metadata.
                let meta: BlobMeta = match std::fs::read_to_string(&meta_path) {
                    Ok(json) => serde_json::from_str(&json).map_err(|e| {
                        coded_error(E009_STORAGE_ERROR, format!("corrupt blob metadata: {e}"))
                    })?,
                    Err(e) => {
                        return Err(coded_error(
                            E009_STORAGE_ERROR,
                            format!("read blob metadata: {e}"),
                        ));
                    }
                };

                // Read the requested chunk.
                let mut file = std::fs::File::open(&blob_path)
                    .map_err(|e| coded_error(E009_STORAGE_ERROR, format!("open blob: {e}")))?;
                let file_len = file
                    .metadata()
                    .map_err(|e| coded_error(E009_STORAGE_ERROR, format!("file metadata: {e}")))?
                    .len();

                if offset >= file_len {
                    return Ok((vec![], meta));
                }

                file.seek(SeekFrom::Start(offset))
                    .map_err(|e| coded_error(E009_STORAGE_ERROR, format!("seek: {e}")))?;
                let remaining = (file_len - offset) as usize;
                let to_read = remaining.min(length as usize);
                let mut buf = vec![0u8; to_read];
                file.read_exact(&mut buf)
                    .map_err(|e| coded_error(E009_STORAGE_ERROR, format!("read chunk: {e}")))?;
                Ok((buf, meta))
            })
            .await
            .map_err(|e| capnp::Error::failed(format!("spawn_blocking join: {e}")))?;

            let (chunk, meta) = io_result?;
            let mut r = results.get();
            r.set_chunk(&chunk);
            r.set_total_size(meta.total_size);
            r.set_mime_type(&meta.mime_type);
            Ok(())
        })
    }
}
