//! Blob domain logic — chunked file upload/download with SHA-256 verification.

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

use sha2::{Digest, Sha256};

use super::types::*;

/// Maximum blob size: 100 MB.
const MAX_BLOB_SIZE: u64 = 100 * 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,
}

/// Domain service for blob (file attachment) storage.
pub struct BlobService {
    pub data_dir: PathBuf,
}

impl BlobService {
    fn blobs_dir(&self) -> PathBuf {
        self.data_dir.join("blobs")
    }

    pub fn upload_blob(
        &self,
        req: UploadBlobReq,
        _auth: &CallerAuth,
    ) -> Result<UploadBlobResp, DomainError> {
        if req.blob_hash.len() != 32 {
            return Err(DomainError::BlobHashLength(req.blob_hash.len()));
        }
        if req.total_size > MAX_BLOB_SIZE {
            return Err(DomainError::BlobTooLarge(req.total_size));
        }
        if req.total_size == 0 {
            return Err(DomainError::BadParams("total_size must be > 0".into()));
        }
        if req
            .offset
            .checked_add(req.chunk.len() as u64)
            .is_none_or(|end| end > req.total_size)
        {
            return Err(DomainError::BadParams(format!(
                "chunk out of bounds: offset={} + chunk_len={} > total_size={}",
                req.offset,
                req.chunk.len(),
                req.total_size
            )));
        }

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

        std::fs::create_dir_all(&dir)
            .map_err(|e| DomainError::Io(format!("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"));

        // Already fully uploaded.
        if final_path.exists() {
            return Ok(UploadBlobResp {
                blob_id: req.blob_hash,
            });
        }

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

        // Check if upload is complete.
        let end = req.offset + req.chunk.len() as u64;
        if end == req.total_size {
            // Verify SHA-256.
            let mut vfile = std::fs::File::open(&part_path)
                .map_err(|e| DomainError::Io(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| DomainError::Io(format!("read: {e}")))?;
                if n == 0 {
                    break;
                }
                hasher.update(&buf[..n]);
            }
            let computed: [u8; 32] = hasher.finalize().into();
            if computed[..] != req.blob_hash[..] {
                let _ = std::fs::remove_file(&part_path);
                return Err(DomainError::BlobHashMismatch);
            }

            // Finalize.
            std::fs::rename(&part_path, &final_path)
                .map_err(|e| DomainError::Io(format!("rename .part: {e}")))?;

            // Write metadata.
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs();
            let meta = BlobMeta {
                mime_type: req.mime_type,
                total_size: req.total_size,
                uploaded_at: now,
            };
            if let Ok(json) = serde_json::to_string_pretty(&meta) {
                let _ = std::fs::write(&meta_path, json.as_bytes());
            }
        }

        Ok(UploadBlobResp {
            blob_id: req.blob_hash,
        })
    }

    pub fn download_blob(
        &self,
        req: DownloadBlobReq,
        _auth: &CallerAuth,
    ) -> Result<DownloadBlobResp, DomainError> {
        if req.blob_id.len() != 32 {
            return Err(DomainError::BlobHashLength(req.blob_id.len()));
        }

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

        if !blob_path.exists() {
            return Err(DomainError::BlobNotFound);
        }

        // Read metadata.
        let meta_json = std::fs::read_to_string(&meta_path)
            .map_err(|e| DomainError::Io(format!("read blob metadata: {e}")))?;
        let meta: BlobMeta = serde_json::from_str(&meta_json)
            .map_err(|e| DomainError::Io(format!("corrupt blob metadata: {e}")))?;

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

        if req.offset >= file_len {
            return Ok(DownloadBlobResp {
                chunk: vec![],
                total_size: meta.total_size,
                mime_type: meta.mime_type,
            });
        }

        file.seek(SeekFrom::Start(req.offset))
            .map_err(|e| DomainError::Io(format!("seek: {e}")))?;
        let remaining = (file_len - req.offset) as usize;
        let to_read = remaining.min(req.length.min(MAX_DOWNLOAD_CHUNK) as usize);
        let mut chunk = vec![0u8; to_read];
        file.read_exact(&mut chunk)
            .map_err(|e| DomainError::Io(format!("read chunk: {e}")))?;

        Ok(DownloadBlobResp {
            chunk,
            total_size: meta.total_size,
            mime_type: meta.mime_type,
        })
    }
}
