//! Erasure-coded distributed storage backend
//!
//! Implements StorageBackend using Reed-Solomon erasure coding for
//! storage-efficient redundancy.

use crate::chunk::{ChunkId, ChunkManager};
use crate::config::DistributedConfig;
use crate::erasure::Codec;
use crate::node::{NodeClientTrait, NodeRegistry};
use crate::placement::{ConsistentHashSelector, NodeSelector};
use async_trait::async_trait;
use bytes::Bytes;
use futures::future::BoxFuture;
use futures::stream::{FuturesUnordered, StreamExt};
use lightningstor_storage::{StorageBackend, StorageError, StorageResult};
use lightningstor_types::ObjectId;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{debug, error, warn};

/// Metadata for an object stored with erasure coding
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectMetadata {
    /// Number of chunks the object is split into
    pub chunk_count: usize,
    /// Original size of the object in bytes
    pub original_size: u64,
    /// Size of each chunk (except possibly the last)
    pub chunk_size: usize,
}

impl ObjectMetadata {
    /// Create new object metadata
    pub fn new(original_size: u64, chunk_count: usize, chunk_size: usize) -> Self {
        Self {
            chunk_count,
            original_size,
            chunk_size,
        }
    }

    /// Serialize to bytes
    pub fn to_bytes(&self) -> Vec<u8> {
        // Simple format: chunk_count (8 bytes) + original_size (8 bytes) + chunk_size (8 bytes)
        let mut bytes = Vec::with_capacity(24);
        bytes.extend_from_slice(&(self.chunk_count as u64).to_le_bytes());
        bytes.extend_from_slice(&self.original_size.to_le_bytes());
        bytes.extend_from_slice(&(self.chunk_size as u64).to_le_bytes());
        bytes
    }

    /// Deserialize from bytes
    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
        if bytes.len() < 24 {
            return None;
        }
        let chunk_count = u64::from_le_bytes(bytes[0..8].try_into().ok()?) as usize;
        let original_size = u64::from_le_bytes(bytes[8..16].try_into().ok()?);
        let chunk_size = u64::from_le_bytes(bytes[16..24].try_into().ok()?) as usize;
        Some(Self {
            chunk_count,
            original_size,
            chunk_size,
        })
    }

    /// Get the metadata key for an object
    pub fn metadata_key(object_id: &ObjectId) -> String {
        format!("{}_meta", object_id)
    }
}

/// Erasure-coded distributed storage backend
///
/// Stores objects by:
/// 1. Splitting into chunks (for large objects)
/// 2. Encoding each chunk into data + parity shards using Reed-Solomon
/// 3. Distributing shards across storage nodes
///
/// Can tolerate loss of up to `parity_shards` nodes without data loss.
pub struct ErasureCodedBackend {
    /// Erasure coding codec
    codec: Arc<Codec>,
    /// Node registry for discovering storage nodes
    node_registry: Arc<dyn NodeRegistry>,
    /// Node selector for placement decisions
    node_selector: Arc<dyn NodeSelector>,
    /// Chunk manager for splitting/reassembling
    chunk_manager: Arc<ChunkManager>,
    /// Configuration (kept for future use)
    #[allow(dead_code)]
    config: DistributedConfig,
    /// Number of data shards
    data_shards: usize,
    /// Number of parity shards
    parity_shards: usize,
}

impl ErasureCodedBackend {
    /// Create a new erasure-coded backend
    ///
    /// # Arguments
    /// * `config` - Distributed storage configuration (must have ErasureCoded redundancy mode)
    /// * `node_registry` - Registry for discovering storage nodes
    pub async fn new(
        config: DistributedConfig,
        node_registry: Arc<dyn NodeRegistry>,
    ) -> StorageResult<Self> {
        let (data_shards, parity_shards) = match &config.redundancy {
            crate::config::RedundancyMode::ErasureCoded {
                data_shards,
                parity_shards,
            } => (*data_shards, *parity_shards),
            _ => {
                return Err(StorageError::Backend(
                    "ErasureCodedBackend requires ErasureCoded redundancy mode".into(),
                ))
            }
        };

        let codec = Arc::new(
            Codec::new(data_shards, parity_shards)
                .map_err(|e| StorageError::Backend(e.to_string()))?,
        );
        let node_selector = Arc::new(ConsistentHashSelector::new());
        let chunk_manager = Arc::new(ChunkManager::new(config.chunk.clone()));

        Ok(Self {
            codec,
            node_registry,
            node_selector,
            chunk_manager,
            config,
            data_shards,
            parity_shards,
        })
    }

    /// Get the total number of shards (data + parity)
    pub fn total_shards(&self) -> usize {
        self.data_shards + self.parity_shards
    }

    /// Get the minimum number of shards needed to reconstruct data
    pub fn min_shards_for_read(&self) -> usize {
        self.data_shards
    }

    /// Select nodes for writing shards
    async fn select_nodes_for_write(&self) -> StorageResult<Vec<Arc<dyn NodeClientTrait>>> {
        let nodes = self
            .node_registry
            .get_healthy_nodes()
            .await
            .map_err(|e| StorageError::Backend(e.to_string()))?;

        let total_shards = self.total_shards();
        if nodes.len() < total_shards {
            return Err(StorageError::Backend(format!(
                "Not enough healthy nodes: need {}, have {}",
                total_shards,
                nodes.len()
            )));
        }

        self.node_selector
            .select_nodes(&nodes, total_shards)
            .await
            .map_err(|e| StorageError::Backend(e.to_string()))
    }

    /// Store object metadata to nodes
    async fn write_metadata(
        &self,
        object_id: &ObjectId,
        metadata: &ObjectMetadata,
    ) -> StorageResult<()> {
        let meta_key = ObjectMetadata::metadata_key(object_id);
        let meta_bytes = Bytes::from(metadata.to_bytes());

        let nodes = self
            .node_registry
            .get_healthy_nodes()
            .await
            .map_err(|e| StorageError::Backend(e.to_string()))?;

        // Store metadata on multiple nodes for redundancy
        let mut write_futures = Vec::new();
        for node in nodes.iter().take(self.total_shards()) {
            let node = node.clone();
            let key = meta_key.clone();
            let data = meta_bytes.clone();
            write_futures.push(async move {
                node.put_chunk(&key, 0, false, data).await
            });
        }

        let results = futures::future::join_all(write_futures).await;
        let success_count = results.iter().filter(|r| r.is_ok()).count();

        // Need at least one successful write
        if success_count == 0 {
            return Err(StorageError::Backend(
                "Failed to write object metadata to any node".into(),
            ));
        }

        debug!(
            object_id = %object_id,
            success_count,
            "Stored object metadata"
        );

        Ok(())
    }

    /// Read object metadata from nodes
    async fn read_metadata(&self, object_id: &ObjectId) -> StorageResult<ObjectMetadata> {
        let meta_key = ObjectMetadata::metadata_key(object_id);

        let nodes = self
            .node_registry
            .get_all_nodes()
            .await
            .map_err(|e| StorageError::Backend(e.to_string()))?;

        // Try to read metadata from any node
        for node in &nodes {
            if let Ok(data) = node.get_chunk(&meta_key, 0, false).await {
                if let Some(metadata) = ObjectMetadata::from_bytes(&data) {
                    return Ok(metadata);
                }
            }
        }

        Err(StorageError::NotFound(*object_id))
    }

    /// Delete object metadata from all nodes
    async fn delete_metadata(&self, object_id: &ObjectId) {
        let meta_key = ObjectMetadata::metadata_key(object_id);

        let nodes = match self.node_registry.get_all_nodes().await {
            Ok(nodes) => nodes,
            Err(_) => return,
        };

        let mut delete_futures = Vec::new();
        for node in &nodes {
            let node = node.clone();
            let key = meta_key.clone();
            delete_futures.push(async move {
                let _ = node.delete_chunk(&key).await;
            });
        }

        futures::future::join_all(delete_futures).await;
    }

    /// Write a single chunk with erasure coding
    async fn write_chunk(
        &self,
        object_id: &ObjectId,
        chunk_index: usize,
        chunk_data: &[u8],
    ) -> StorageResult<()> {
        // Encode the chunk
        let shards = self
            .codec
            .encode(chunk_data)
            .map_err(|e| StorageError::Backend(e.to_string()))?;

        // Select nodes for each shard
        let nodes = self.select_nodes_for_write().await?;

        // Write shards in parallel
        let mut write_futures = Vec::with_capacity(self.total_shards());
        for (shard_idx, (shard_data, node)) in shards.into_iter().zip(nodes.iter()).enumerate() {
            let is_parity = shard_idx >= self.data_shards;
            let chunk_id = ChunkId::new(object_id, chunk_index, shard_idx, is_parity);
            let node = node.clone();
            let shard_bytes = Bytes::from(shard_data);

            write_futures.push(async move {
                node.put_chunk(&chunk_id.to_key(), shard_idx as u32, is_parity, shard_bytes)
                    .await
            });
        }

        // Wait for all writes
        let results = futures::future::join_all(write_futures).await;
        let success_count = results.iter().filter(|r| r.is_ok()).count();
        let error_count = results.len() - success_count;

        debug!(
            object_id = %object_id,
            chunk_index,
            success_count,
            error_count,
            "Wrote erasure-coded chunk"
        );

        // Require full shard write; without repair loop partial writes reduce redundancy.
        let min_required = self.total_shards();
        if success_count < min_required {
            let errors: Vec<_> = results
                .into_iter()
                .filter_map(|r| r.err())
                .collect();
            error!(
                success_count,
                min_required,
                errors = ?errors,
                "Failed to write enough shards"
            );
            return Err(StorageError::Backend(format!(
                "Failed to write enough shards: {} of {} required succeeded",
                success_count, min_required
            )));
        }

        Ok(())
    }

    /// Read a single chunk with erasure decoding
    async fn read_chunk(
        &self,
        object_id: &ObjectId,
        chunk_index: usize,
        original_chunk_size: usize,
    ) -> StorageResult<Vec<u8>> {
        // Use all nodes for reads - unhealthy nodes might still have data we need
        // The erasure coding handles actual failures gracefully
        let nodes = self
            .node_registry
            .get_all_nodes()
            .await
            .map_err(|e| StorageError::Backend(e.to_string()))?;

        // Try to read all shards in parallel
        let mut shard_futures = FuturesUnordered::new();
        for shard_idx in 0..self.total_shards() {
            let is_parity = shard_idx >= self.data_shards;
            let chunk_id = ChunkId::new(object_id, chunk_index, shard_idx, is_parity);
            let nodes = nodes.clone();
            let node_selector = self.node_selector.clone();
            let chunk_key = chunk_id.to_key();

            shard_futures.push(async move {
                let preferred_id = node_selector
                    .select_for_read(&nodes, &chunk_key)
                    .await
                    .ok()
                    .map(|node| node.node_id().to_string());
                let mut readers: FuturesUnordered<BoxFuture<'static, Option<Bytes>>> =
                    FuturesUnordered::new();

                if let Some(preferred_id) = preferred_id.as_ref() {
                    if let Some(preferred) = nodes
                        .iter()
                        .find(|node| node.node_id() == preferred_id.as_str())
                        .cloned()
                    {
                        let key = chunk_key.clone();
                        readers.push(Box::pin(async move {
                            preferred
                                .get_chunk(&key, shard_idx as u32, is_parity)
                                .await
                                .ok()
                        }));
                    }
                }

                for node in &nodes {
                    if preferred_id
                        .as_ref()
                        .is_some_and(|preferred| preferred == node.node_id())
                    {
                        continue;
                    }
                    let node = node.clone();
                    let key = chunk_key.clone();
                    readers.push(Box::pin(async move {
                        node.get_chunk(&key, shard_idx as u32, is_parity).await.ok()
                    }));
                }

                while let Some(result) = readers.next().await {
                    if let Some(data) = result {
                        return (shard_idx, Some(data));
                    }
                }

                (shard_idx, None)
            });
        }

        let mut shard_results = vec![None; self.total_shards()];
        let mut available_count = 0usize;

        while let Some((shard_idx, shard)) = shard_futures.next().await {
            if shard.is_some() {
                available_count += 1;
            }
            shard_results[shard_idx] = shard;

            if available_count >= self.data_shards {
                break;
            }

            if available_count + shard_futures.len() < self.data_shards {
                break;
            }
        }

        // Count available shards
        debug!(
            object_id = %object_id,
            chunk_index,
            available_count,
            required = self.data_shards,
            "Read shards for decoding"
        );

        if available_count < self.data_shards {
            return Err(StorageError::Backend(format!(
                "Not enough shards for decoding: have {}, need {}",
                available_count, self.data_shards
            )));
        }

        // Decode
        self.codec
            .decode(
                shard_results
                    .into_iter()
                    .map(|shard| shard.map(|bytes| bytes.to_vec()))
                    .collect(),
                original_chunk_size,
            )
            .map_err(|e| StorageError::Backend(e.to_string()))
    }
}

impl ChunkId {
    fn new(object_id: &ObjectId, chunk_index: usize, shard_index: usize, is_parity: bool) -> Self {
        if is_parity {
            Self::parity_shard(object_id.to_string(), chunk_index, shard_index)
        } else {
            Self::data_shard(object_id.to_string(), chunk_index, shard_index)
        }
    }
}

#[async_trait]
impl StorageBackend for ErasureCodedBackend {
    async fn put_object(&self, object_id: &ObjectId, data: Bytes) -> StorageResult<()> {
        let original_size = data.len() as u64;
        debug!(object_id = %object_id, size = original_size, "Putting object with erasure coding");

        // Split data into chunks
        let chunk_size = self.chunk_manager.effective_chunk_size(data.len());
        let chunks = self.chunk_manager.split_with_chunk_size(&data, chunk_size);
        let chunk_count = chunks.len();

        // Write each chunk
        for (chunk_idx, chunk_data) in chunks.into_iter().enumerate() {
            self.write_chunk(object_id, chunk_idx, &chunk_data).await?;
        }

        // Store metadata
        let metadata = ObjectMetadata::new(original_size, chunk_count, chunk_size);
        self.write_metadata(object_id, &metadata).await?;

        debug!(
            object_id = %object_id,
            chunk_count,
            original_size,
            "Successfully stored object with erasure coding"
        );

        Ok(())
    }

    async fn get_object(&self, object_id: &ObjectId) -> StorageResult<Bytes> {
        debug!(object_id = %object_id, "Getting object with erasure decoding");

        // Read metadata to get chunk count and original size
        let metadata = self.read_metadata(object_id).await?;

        debug!(
            object_id = %object_id,
            chunk_count = metadata.chunk_count,
            original_size = metadata.original_size,
            "Read object metadata"
        );

        // Read all chunks and reassemble
        let mut all_data = Vec::with_capacity(metadata.original_size as usize);

        for chunk_idx in 0..metadata.chunk_count {
            // Calculate the expected size for this chunk
            let remaining = metadata.original_size as usize - all_data.len();
            let expected_chunk_size = remaining.min(metadata.chunk_size);

            let chunk_data = self
                .read_chunk(object_id, chunk_idx, expected_chunk_size)
                .await?;

            // Only take what we need (handles padding)
            let take_size = remaining.min(chunk_data.len());
            all_data.extend_from_slice(&chunk_data[..take_size]);
        }

        // Truncate to original size in case of any padding
        all_data.truncate(metadata.original_size as usize);

        debug!(
            object_id = %object_id,
            retrieved_size = all_data.len(),
            "Successfully retrieved object"
        );

        Ok(Bytes::from(all_data))
    }

    async fn delete_object(&self, object_id: &ObjectId) -> StorageResult<()> {
        debug!(object_id = %object_id, "Deleting object shards");

        // Try to read metadata to know how many chunks to delete
        let chunk_count = match self.read_metadata(object_id).await {
            Ok(metadata) => metadata.chunk_count,
            Err(_) => 1, // Fall back to single chunk if metadata not found
        };

        let nodes = self
            .node_registry
            .get_all_nodes()
            .await
            .map_err(|e| StorageError::Backend(e.to_string()))?;

        // Delete all shards for all chunks from all nodes (best effort)
        let mut delete_futures = Vec::new();
        for chunk_idx in 0..chunk_count {
            for shard_idx in 0..self.total_shards() {
                let is_parity = shard_idx >= self.data_shards;
                let chunk_id = ChunkId::new(object_id, chunk_idx, shard_idx, is_parity);
                let chunk_key = chunk_id.to_key();

                for node in &nodes {
                    let node = node.clone();
                    let key = chunk_key.clone();
                    delete_futures.push(async move {
                        if let Err(e) = node.delete_chunk(&key).await {
                            // Log but don't fail - best effort deletion
                            warn!(node_id = node.node_id(), chunk_key = key, error = ?e, "Failed to delete shard");
                        }
                    });
                }
            }
        }

        futures::future::join_all(delete_futures).await;

        // Delete metadata
        self.delete_metadata(object_id).await;

        debug!(object_id = %object_id, chunk_count, "Deleted object");
        Ok(())
    }

    async fn object_exists(&self, object_id: &ObjectId) -> StorageResult<bool> {
        // Check if metadata exists
        Ok(self.read_metadata(object_id).await.is_ok())
    }

    async fn object_size(&self, object_id: &ObjectId) -> StorageResult<u64> {
        // Read metadata to get original size
        let metadata = self.read_metadata(object_id).await?;
        Ok(metadata.original_size)
    }

    async fn put_part(
        &self,
        upload_id: &str,
        part_number: u32,
        data: Bytes,
    ) -> StorageResult<()> {
        // Use a deterministic part key based on upload_id and part_number
        let part_key = format!("part_{}_{}", upload_id, part_number);
        let nodes = self.select_nodes_for_write().await?;

        // Encode and store the part data
        let shards = self
            .codec
            .encode(&data)
            .map_err(|e| StorageError::Backend(e.to_string()))?;

        let mut write_futures = Vec::with_capacity(self.total_shards());
        for (shard_idx, (shard_data, node)) in shards.into_iter().zip(nodes.iter()).enumerate() {
            let is_parity = shard_idx >= self.data_shards;
            let key = format!("{}_{}_{}", part_key, shard_idx, if is_parity { "p" } else { "d" });
            let node = node.clone();
            let shard_bytes = Bytes::from(shard_data);

            write_futures.push(async move {
                node.put_chunk(&key, shard_idx as u32, is_parity, shard_bytes).await
            });
        }

        let results = futures::future::join_all(write_futures).await;
        let success_count = results.iter().filter(|r| r.is_ok()).count();

        let min_required = self.total_shards();
        if success_count < min_required {
            return Err(StorageError::Backend(format!(
                "Failed to write part shards: {} of {} required",
                success_count, min_required
            )));
        }

        Ok(())
    }

    async fn get_part(&self, upload_id: &str, part_number: u32) -> StorageResult<Bytes> {
        let part_key = format!("part_{}_{}", upload_id, part_number);
        let nodes = self
            .node_registry
            .get_healthy_nodes()
            .await
            .map_err(|e| StorageError::Backend(e.to_string()))?;

        // Try to read shards
        let mut shard_futures = FuturesUnordered::new();
        for shard_idx in 0..self.total_shards() {
            let is_parity = shard_idx >= self.data_shards;
            let key = format!("{}_{}_{}", part_key, shard_idx, if is_parity { "p" } else { "d" });
            let nodes = nodes.clone();
            let node_selector = self.node_selector.clone();

            shard_futures.push(async move {
                let preferred_id = node_selector
                    .select_for_read(&nodes, &key)
                    .await
                    .ok()
                    .map(|node| node.node_id().to_string());
                let mut readers: FuturesUnordered<BoxFuture<'static, Option<Bytes>>> =
                    FuturesUnordered::new();

                if let Some(preferred_id) = preferred_id.as_ref() {
                    if let Some(preferred) = nodes
                        .iter()
                        .find(|node| node.node_id() == preferred_id.as_str())
                        .cloned()
                    {
                        let key = key.clone();
                        readers.push(Box::pin(async move {
                            preferred
                                .get_chunk(&key, shard_idx as u32, is_parity)
                                .await
                                .ok()
                        }));
                    }
                }

                for node in &nodes {
                    if preferred_id
                        .as_ref()
                        .is_some_and(|preferred| preferred == node.node_id())
                    {
                        continue;
                    }
                    let node = node.clone();
                    let key = key.clone();
                    readers.push(Box::pin(async move {
                        node.get_chunk(&key, shard_idx as u32, is_parity).await.ok()
                    }));
                }

                while let Some(result) = readers.next().await {
                    if let Some(data) = result {
                        return (shard_idx, Some(data));
                    }
                }
                (shard_idx, None)
            });
        }

        let mut shard_results = vec![None; self.total_shards()];
        let mut available = 0usize;

        while let Some((shard_idx, shard)) = shard_futures.next().await {
            if shard.is_some() {
                available += 1;
            }
            shard_results[shard_idx] = shard;

            if available >= self.data_shards {
                break;
            }

            if available + shard_futures.len() < self.data_shards {
                break;
            }
        }

        if available < self.data_shards {
            return Err(StorageError::Backend(format!(
                "Part {}:{} not found (insufficient shards)",
                upload_id, part_number
            )));
        }

        // Get shard size from first available shard
        let shard_size = shard_results
            .iter()
            .find_map(|s| s.as_ref().map(|v| v.len()))
            .unwrap_or(0);
        let original_size = self.data_shards * shard_size;

        let data = self
            .codec
            .decode(
                shard_results
                    .into_iter()
                    .map(|shard| shard.map(|bytes| bytes.to_vec()))
                    .collect(),
                original_size,
            )
            .map_err(|e| StorageError::Backend(e.to_string()))?;

        Ok(Bytes::from(data))
    }

    async fn delete_part(&self, upload_id: &str, part_number: u32) -> StorageResult<()> {
        let part_key = format!("part_{}_{}", upload_id, part_number);
        let nodes = self
            .node_registry
            .get_all_nodes()
            .await
            .map_err(|e| StorageError::Backend(e.to_string()))?;

        let mut delete_futures = Vec::new();
        for shard_idx in 0..self.total_shards() {
            let is_parity = shard_idx >= self.data_shards;
            let key = format!("{}_{}_{}", part_key, shard_idx, if is_parity { "p" } else { "d" });

            for node in &nodes {
                let node = node.clone();
                let key = key.clone();
                delete_futures.push(async move {
                    let _ = node.delete_chunk(&key).await;
                });
            }
        }

        futures::future::join_all(delete_futures).await;
        Ok(())
    }

    async fn delete_upload_parts(&self, _upload_id: &str) -> StorageResult<()> {
        // Would need to track part numbers in metadata
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{ChunkConfig, RedundancyMode};
    use crate::node::{MockNodeClient, MockNodeRegistry, NodeError, NodeResult};
    use async_trait::async_trait;
    use dashmap::DashMap;
    use std::time::{Duration, Instant};
    use tokio::time::sleep;

    struct SlowReadNodeClient {
        node_id: String,
        endpoint: String,
        delay: Duration,
        chunks: DashMap<String, Vec<u8>>,
    }

    impl SlowReadNodeClient {
        fn new(node_id: impl Into<String>, endpoint: impl Into<String>, delay: Duration) -> Self {
            Self {
                node_id: node_id.into(),
                endpoint: endpoint.into(),
                delay,
                chunks: DashMap::new(),
            }
        }

        fn insert_chunk(&self, chunk_id: impl Into<String>, data: Vec<u8>) {
            self.chunks.insert(chunk_id.into(), data);
        }
    }

    #[async_trait]
    impl NodeClientTrait for SlowReadNodeClient {
        fn node_id(&self) -> &str {
            &self.node_id
        }

        fn endpoint(&self) -> &str {
            &self.endpoint
        }

        async fn is_healthy(&self) -> bool {
            true
        }

        async fn put_chunk(
            &self,
            chunk_id: &str,
            _shard_index: u32,
            _is_parity: bool,
            data: Bytes,
        ) -> NodeResult<()> {
            self.chunks.insert(chunk_id.to_string(), data.to_vec());
            Ok(())
        }

        async fn get_chunk(
            &self,
            chunk_id: &str,
            _shard_index: u32,
            _is_parity: bool,
        ) -> NodeResult<Bytes> {
            sleep(self.delay).await;
            self.chunks
                .get(chunk_id)
                .map(|value| Bytes::from(value.value().clone()))
                .ok_or_else(|| NodeError::NotFound(chunk_id.to_string()))
        }

        async fn delete_chunk(&self, chunk_id: &str) -> NodeResult<()> {
            self.chunks.remove(chunk_id);
            Ok(())
        }

        async fn chunk_exists(&self, chunk_id: &str) -> NodeResult<bool> {
            Ok(self.chunks.contains_key(chunk_id))
        }

        async fn chunk_size(&self, chunk_id: &str) -> NodeResult<Option<u64>> {
            Ok(self
                .chunks
                .get(chunk_id)
                .map(|value| value.value().len() as u64))
        }

        async fn ping(&self) -> NodeResult<Duration> {
            Ok(Duration::from_millis(1))
        }
    }

    struct FixedNodeRegistry {
        nodes: Vec<Arc<dyn NodeClientTrait>>,
    }

    #[async_trait]
    impl NodeRegistry for FixedNodeRegistry {
        async fn get_all_nodes(&self) -> NodeResult<Vec<Arc<dyn NodeClientTrait>>> {
            Ok(self.nodes.clone())
        }

        async fn get_healthy_nodes(&self) -> NodeResult<Vec<Arc<dyn NodeClientTrait>>> {
            Ok(self.nodes.clone())
        }

        async fn register_node(&self, _info: crate::node::NodeInfo) -> NodeResult<()> {
            Ok(())
        }

        async fn deregister_node(&self, _node_id: &str) -> NodeResult<()> {
            Ok(())
        }

        async fn update_health(&self, _node_id: &str, _healthy: bool) -> NodeResult<()> {
            Ok(())
        }

        async fn get_node(&self, node_id: &str) -> NodeResult<Option<Arc<dyn NodeClientTrait>>> {
            Ok(self
                .nodes
                .iter()
                .find(|node| node.node_id() == node_id)
                .cloned())
        }

        async fn node_count(&self) -> usize {
            self.nodes.len()
        }

        async fn healthy_node_count(&self) -> usize {
            self.nodes.len()
        }
    }

    fn create_ec_config(data_shards: usize, parity_shards: usize) -> DistributedConfig {
        DistributedConfig {
            redundancy: RedundancyMode::ErasureCoded {
                data_shards,
                parity_shards,
            },
            chunk: ChunkConfig::default(),
            ..Default::default()
        }
    }

    #[tokio::test]
    async fn test_ec_backend_creation() {
        let config = create_ec_config(4, 2);
        let registry = Arc::new(MockNodeRegistry::with_nodes(6));

        let backend = ErasureCodedBackend::new(config, registry).await.unwrap();

        assert_eq!(backend.total_shards(), 6);
        assert_eq!(backend.min_shards_for_read(), 4);
    }

    #[tokio::test]
    async fn test_ec_backend_put_get() {
        let config = create_ec_config(4, 2);
        let registry = Arc::new(MockNodeRegistry::with_nodes(6));

        let backend = ErasureCodedBackend::new(config, registry.clone()).await.unwrap();

        let object_id = ObjectId::new();
        let data = Bytes::from(vec![42u8; 1024]);

        backend.put_object(&object_id, data.clone()).await.unwrap();

        // Verify shards were written to nodes
        let nodes = registry.all_mock_nodes();
        let total_chunks: usize = nodes.iter().map(|n| n.chunk_count()).sum();
        assert!(total_chunks >= 4); // At least data shards should be stored

        // Get the object back
        let retrieved = backend.get_object(&object_id).await.unwrap();

        // The retrieved data might have padding, but should contain original data
        assert!(retrieved.len() >= data.len());
        assert_eq!(&retrieved[..data.len()], &data[..]);
    }

    #[tokio::test]
    async fn test_ec_backend_tolerates_failures() {
        let config = create_ec_config(4, 2);
        let registry = Arc::new(MockNodeRegistry::with_nodes(6));

        let backend = ErasureCodedBackend::new(config, registry.clone()).await.unwrap();

        let object_id = ObjectId::new();
        let data = Bytes::from(vec![42u8; 512]);

        backend.put_object(&object_id, data.clone()).await.unwrap();

        // Fail 2 nodes (within parity tolerance)
        let nodes = registry.all_mock_nodes();
        nodes[0].set_healthy(false);
        nodes[1].set_healthy(false);

        // Should still be able to read
        let retrieved = backend.get_object(&object_id).await.unwrap();
        assert!(retrieved.len() >= data.len());
        assert_eq!(&retrieved[..data.len()], &data[..]);
    }

    #[tokio::test]
    async fn test_ec_backend_delete() {
        let config = create_ec_config(4, 2);
        let registry = Arc::new(MockNodeRegistry::with_nodes(6));

        let backend = ErasureCodedBackend::new(config, registry.clone()).await.unwrap();

        let object_id = ObjectId::new();
        let data = Bytes::from(vec![42u8; 256]);

        backend.put_object(&object_id, data).await.unwrap();
        assert!(backend.object_exists(&object_id).await.unwrap());

        backend.delete_object(&object_id).await.unwrap();

        // After deletion, object should not exist
        // (All shards should be deleted)
        assert!(!backend.object_exists(&object_id).await.unwrap());
    }

    #[tokio::test]
    async fn test_ec_backend_not_enough_nodes() {
        let config = create_ec_config(4, 2);
        // Only 3 nodes, but need 6 for 4+2 EC
        let registry = Arc::new(MockNodeRegistry::with_nodes(3));

        let backend = ErasureCodedBackend::new(config, registry).await.unwrap();

        let object_id = ObjectId::new();
        let data = Bytes::from(vec![42u8; 256]);

        let result = backend.put_object(&object_id, data).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_ec_backend_multi_chunk() {
        // Create config with small chunk size to force multiple chunks
        let config = DistributedConfig {
            redundancy: RedundancyMode::ErasureCoded {
                data_shards: 4,
                parity_shards: 2,
            },
            chunk: ChunkConfig::new(1024), // 1 KB chunks
            ..Default::default()
        };
        let registry = Arc::new(MockNodeRegistry::with_nodes(6));

        let backend = ErasureCodedBackend::new(config, registry.clone()).await.unwrap();

        let object_id = ObjectId::new();
        // Create data larger than chunk size (3 KB = 3 chunks)
        let data: Vec<u8> = (0..3072).map(|i| (i % 256) as u8).collect();
        let data = Bytes::from(data);

        // Store the object
        backend.put_object(&object_id, data.clone()).await.unwrap();

        // Verify it exists
        assert!(backend.object_exists(&object_id).await.unwrap());

        // Get object size
        let size = backend.object_size(&object_id).await.unwrap();
        assert_eq!(size, 3072);

        // Retrieve the object
        let retrieved = backend.get_object(&object_id).await.unwrap();

        // Verify data integrity
        assert_eq!(retrieved.len(), data.len());
        assert_eq!(retrieved, data);

        // Delete the object
        backend.delete_object(&object_id).await.unwrap();

        // Verify it's deleted
        assert!(!backend.object_exists(&object_id).await.unwrap());
    }

    #[tokio::test]
    async fn test_ec_backend_multi_chunk_with_failures() {
        // Create config with small chunk size
        let config = DistributedConfig {
            redundancy: RedundancyMode::ErasureCoded {
                data_shards: 4,
                parity_shards: 2,
            },
            chunk: ChunkConfig::new(512), // 512 byte chunks
            ..Default::default()
        };
        let registry = Arc::new(MockNodeRegistry::with_nodes(6));

        let backend = ErasureCodedBackend::new(config, registry.clone()).await.unwrap();

        let object_id = ObjectId::new();
        // 2 KB = 4 chunks with 512 byte chunk size
        let data: Vec<u8> = (0..2048).map(|i| (i % 256) as u8).collect();
        let data = Bytes::from(data);

        // Store the object
        backend.put_object(&object_id, data.clone()).await.unwrap();

        // Fail 2 nodes (within parity tolerance)
        let nodes = registry.all_mock_nodes();
        nodes[0].set_healthy(false);
        nodes[1].set_healthy(false);

        // Should still be able to read
        let retrieved = backend.get_object(&object_id).await.unwrap();
        assert_eq!(retrieved.len(), data.len());
        assert_eq!(retrieved, data);
    }

    #[tokio::test]
    async fn test_ec_backend_read_returns_after_minimum_shards() {
        let config = create_ec_config(4, 2);
        let mut fast_nodes = Vec::new();
        for index in 0..4 {
            fast_nodes.push(Arc::new(MockNodeClient::new(
                format!("fast-{index}"),
                format!("http://fast-{index}:9002"),
            )));
        }
        let slow_a = Arc::new(SlowReadNodeClient::new(
            "slow-a",
            "http://slow-a:9002",
            Duration::from_millis(250),
        ));
        let slow_b = Arc::new(SlowReadNodeClient::new(
            "slow-b",
            "http://slow-b:9002",
            Duration::from_millis(250),
        ));

        let backend = ErasureCodedBackend::new(
            config,
            Arc::new(FixedNodeRegistry {
                nodes: vec![
                    fast_nodes[0].clone() as Arc<dyn NodeClientTrait>,
                    fast_nodes[1].clone() as Arc<dyn NodeClientTrait>,
                    fast_nodes[2].clone() as Arc<dyn NodeClientTrait>,
                    fast_nodes[3].clone() as Arc<dyn NodeClientTrait>,
                    slow_a.clone() as Arc<dyn NodeClientTrait>,
                    slow_b.clone() as Arc<dyn NodeClientTrait>,
                ],
            }),
        )
        .await
        .unwrap();

        let object_id = ObjectId::new();
        let data = Bytes::from(vec![5u8; 512]);
        let metadata = ObjectMetadata::new(data.len() as u64, 1, data.len());
        let meta_key = ObjectMetadata::metadata_key(&object_id);
        let shards = backend.codec.encode(&data).unwrap();

        for fast_node in &fast_nodes {
            fast_node
                .put_chunk(&meta_key, 0, false, Bytes::from(metadata.to_bytes()))
                .await
                .unwrap();
        }
        for slow_node in [&slow_a, &slow_b] {
            slow_node.insert_chunk(meta_key.clone(), metadata.to_bytes());
        }

        for (shard_idx, shard_data) in shards.into_iter().enumerate() {
            let is_parity = shard_idx >= backend.data_shards;
            let key = ChunkId::new(&object_id, 0, shard_idx, is_parity).to_key();
            if shard_idx < 4 {
                fast_nodes[shard_idx]
                    .put_chunk(
                        &key,
                        shard_idx as u32,
                        is_parity,
                        Bytes::from(shard_data),
                    )
                    .await
                    .unwrap();
            } else if shard_idx == 4 {
                slow_a.insert_chunk(key, shard_data);
            } else {
                slow_b.insert_chunk(key, shard_data);
            }
        }

        let started = Instant::now();
        let retrieved = backend.get_object(&object_id).await.unwrap();
        let elapsed = started.elapsed();

        assert!(elapsed < Duration::from_millis(200), "elapsed={elapsed:?}");
        assert_eq!(retrieved, data);
    }

    #[tokio::test]
    async fn test_ec_backend_get_part_returns_after_minimum_shards() {
        let config = create_ec_config(4, 2);
        let mut fast_nodes = Vec::new();
        for index in 0..4 {
            fast_nodes.push(Arc::new(MockNodeClient::new(
                format!("fast-{index}"),
                format!("http://fast-{index}:9002"),
            )));
        }
        let slow_a = Arc::new(SlowReadNodeClient::new(
            "slow-a",
            "http://slow-a:9002",
            Duration::from_millis(250),
        ));
        let slow_b = Arc::new(SlowReadNodeClient::new(
            "slow-b",
            "http://slow-b:9002",
            Duration::from_millis(250),
        ));

        let backend = ErasureCodedBackend::new(
            config,
            Arc::new(FixedNodeRegistry {
                nodes: vec![
                    fast_nodes[0].clone() as Arc<dyn NodeClientTrait>,
                    fast_nodes[1].clone() as Arc<dyn NodeClientTrait>,
                    fast_nodes[2].clone() as Arc<dyn NodeClientTrait>,
                    fast_nodes[3].clone() as Arc<dyn NodeClientTrait>,
                    slow_a.clone() as Arc<dyn NodeClientTrait>,
                    slow_b.clone() as Arc<dyn NodeClientTrait>,
                ],
            }),
        )
        .await
        .unwrap();

        let upload_id = "upload-latency";
        let part_number = 7;
        let data = Bytes::from(vec![9u8; 512]);
        let shards = backend.codec.encode(&data).unwrap();

        for (shard_idx, shard_data) in shards.into_iter().enumerate() {
            let is_parity = shard_idx >= backend.data_shards;
            let key = format!(
                "part_{}_{}_{}_{}",
                upload_id,
                part_number,
                shard_idx,
                if is_parity { "p" } else { "d" }
            );

            if shard_idx < 4 {
                fast_nodes[shard_idx]
                    .put_chunk(
                        &key,
                        shard_idx as u32,
                        is_parity,
                        Bytes::from(shard_data),
                    )
                    .await
                    .unwrap();
            } else if shard_idx == 4 {
                slow_a.insert_chunk(key, shard_data);
            } else {
                slow_b.insert_chunk(key, shard_data);
            }
        }

        let started = Instant::now();
        let retrieved = backend.get_part(upload_id, part_number).await.unwrap();
        let elapsed = started.elapsed();

        assert!(elapsed < Duration::from_millis(200), "elapsed={elapsed:?}");
        assert_eq!(retrieved, data);
    }
}
