// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::{Error, Result};
use async_trait::async_trait;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use rustfs_common::heal_channel::{HealOpts, HealScanMode};
use rustfs_madmin::heal_commands::HealResultItem;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{debug, error, warn};

use super::storage_api::owner::{EcstoreHealLifecycleExpiryContext, ecstore_load_admin_data_usage_from_backend_cached};
use super::storage_api::storage::{
    BucketInfo, BucketOperations, DiskSetSelector, HealOperations as _, ListOperations as _, ObjectIO as _,
    ObjectOperations as _, StorageAdminApi,
};
use super::{DiskStore, ECStore, HealDiskExt as _, StorageError, resume::ReplacementTargetIdentity};
pub use super::{HealObjectInfo, HealObjectOptions, HealPutObjReader};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct HealBucketUsageBaseline {
    pub objects_count: u64,
    pub bytes: u64,
}

pub struct HealLifecycleExpiryContext {
    inner: HealLifecycleExpiryContextInner,
}

enum HealLifecycleExpiryContextInner {
    Ecstore(EcstoreHealLifecycleExpiryContext),
    #[allow(
        dead_code,
        reason = "constructed by the #[cfg(test)] `test()` helper; the lib target cannot see test-only consumers (backlog#1823)"
    )]
    Test,
}

impl HealLifecycleExpiryContext {
    fn ecstore(inner: EcstoreHealLifecycleExpiryContext) -> Self {
        Self {
            inner: HealLifecycleExpiryContextInner::Ecstore(inner),
        }
    }

    #[cfg(test)]
    pub(crate) fn test() -> Self {
        Self {
            inner: HealLifecycleExpiryContextInner::Test,
        }
    }
}

const LOG_COMPONENT_HEAL: &str = "heal";
const LOG_SUBSYSTEM_STORAGE: &str = "storage";
const EVENT_HEAL_STORAGE_OBJECT_IO: &str = "heal_storage_object_io";
const EVENT_HEAL_STORAGE_OBJECT_READ_LIMIT: &str = "heal_storage_object_read_limit";
const EVENT_HEAL_STORAGE_ADMIN_OP: &str = "heal_storage_admin_op";
const EVENT_HEAL_STORAGE_REPAIR_OP: &str = "heal_storage_repair_op";

pub enum ReplacementResumeDisk {
    Fresh,
    Existing(DiskStore),
}

pub(crate) fn next_heal_listing_token(
    bucket: &str,
    prefix: &str,
    next_token: Option<String>,
    is_truncated: bool,
) -> Result<Option<String>> {
    if !is_truncated {
        return Ok(None);
    }

    match next_token {
        Some(token) => Ok(Some(token)),
        None => {
            // A version listing legitimately reports the final page as truncated
            // when the last object's versions land exactly on the page boundary
            // yet the backend has nothing further to yield. Treat a missing
            // continuation token as end-of-listing rather than a hard error so
            // the heal pass terminates cleanly instead of failing the bucket.
            warn!(
                target: "rustfs::heal::storage",
                event = EVENT_HEAL_STORAGE_ADMIN_OP,
                component = LOG_COMPONENT_HEAL,
                subsystem = LOG_SUBSYSTEM_STORAGE,
                operation = "next_heal_listing_token",
                bucket,
                prefix,
                state = "truncated_without_token",
                "Heal storage object listing truncated without continuation token; treating as end of listing"
            );
            Ok(None)
        }
    }
}

/// Opaque continuation token payload for heal version listing. Encodes the
/// `(marker, version_marker)` pair that `list_object_versions` needs to resume.
#[derive(Debug, Serialize, Deserialize)]
struct HealTokenPayload {
    /// object key marker
    #[serde(rename = "m")]
    m: Option<String>,
    /// version-id marker
    #[serde(rename = "v")]
    v: Option<String>,
}

const HEAL_TOKEN_PREFIX: &str = "v1:";

/// Encode a `(marker, version_marker)` pair into an opaque heal continuation
/// token. The token is `"v1:" + base64url_nopad(json)`.
///
/// Invariant: `list_object_versions` returns `NotImplemented` for
/// `(None, Some(_))`, so callers must never produce that pair. This is checked
/// with a `debug_assert!`.
pub(crate) fn encode_heal_token(marker: Option<&str>, version_marker: Option<&str>) -> String {
    debug_assert!(
        !(marker.is_none() && version_marker.is_some()),
        "encode_heal_token must never be called with (None, Some(_))"
    );

    let payload = HealTokenPayload {
        m: marker.map(str::to_string),
        v: version_marker.map(str::to_string),
    };
    // serde_json of a simple two-Option struct cannot fail; fall back to an
    // empty object rather than panicking if it somehow does.
    let json = serde_json::to_vec(&payload).unwrap_or_else(|_| b"{}".to_vec());
    format!("{HEAL_TOKEN_PREFIX}{}", URL_SAFE_NO_PAD.encode(json))
}

/// Decode an opaque heal continuation token back into `(marker, version_marker)`.
///
/// TOTAL function: an empty token, a missing `"v1:"` prefix, invalid base64, or
/// invalid JSON all decode to `(None, None)` (start from the beginning). A
/// decoded `(None, Some(_))` is coerced to `(None, None)` to preserve the
/// `list_object_versions` invariant.
pub(crate) fn decode_heal_token(token: &str) -> (Option<String>, Option<String>) {
    if token.is_empty() {
        return (None, None);
    }

    let Some(encoded) = token.strip_prefix(HEAL_TOKEN_PREFIX) else {
        warn!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_ADMIN_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "decode_heal_token",
            state = "missing_prefix",
            "Heal continuation token missing version prefix; restarting listing"
        );
        return (None, None);
    };

    let bytes = match URL_SAFE_NO_PAD.decode(encoded) {
        Ok(bytes) => bytes,
        Err(e) => {
            warn!(
                target: "rustfs::heal::storage",
                event = EVENT_HEAL_STORAGE_ADMIN_OP,
                component = LOG_COMPONENT_HEAL,
                subsystem = LOG_SUBSYSTEM_STORAGE,
                operation = "decode_heal_token",
                state = "bad_base64",
                error = %e,
                "Heal continuation token has invalid base64; restarting listing"
            );
            return (None, None);
        }
    };

    let payload: HealTokenPayload = match serde_json::from_slice(&bytes) {
        Ok(payload) => payload,
        Err(e) => {
            warn!(
                target: "rustfs::heal::storage",
                event = EVENT_HEAL_STORAGE_ADMIN_OP,
                component = LOG_COMPONENT_HEAL,
                subsystem = LOG_SUBSYSTEM_STORAGE,
                operation = "decode_heal_token",
                state = "bad_json",
                error = %e,
                "Heal continuation token has invalid payload; restarting listing"
            );
            return (None, None);
        }
    };

    // Preserve the list_object_versions invariant: (None, Some(_)) is illegal.
    if payload.m.is_none() && payload.v.is_some() {
        warn!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_ADMIN_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "decode_heal_token",
            state = "illegal_version_only_marker",
            "Heal continuation token had a version marker without an object marker; restarting listing"
        );
        return (None, None);
    }

    (payload.m, payload.v)
}

const DISK_WALK_TOKEN_PREFIX: &str = "dw1:";

/// Encode the disk-walk resume cursor (`next_forward` object key) into an opaque
/// continuation token: `"dw1:" + base64url_nopad(forward)`.
///
/// The `dw1:` namespace is DISJOINT from the B5 `v1:` token namespace so the two
/// enumerators can never misread each other's cursor: a `dw1:` token decodes to
/// `(None, None)` under the B5 decoder, and a `v1:` token decodes to `None` here.
pub(crate) fn encode_disk_walk_token(next_forward: &str) -> String {
    format!("{DISK_WALK_TOKEN_PREFIX}{}", URL_SAFE_NO_PAD.encode(next_forward.as_bytes()))
}

/// Decode a disk-walk continuation token back into the `next_forward` object key.
///
/// TOTAL function: an empty token, a missing `"dw1:"` prefix (including a foreign
/// B5 `v1:` token), invalid base64, or invalid UTF-8 all decode to `None` (start
/// the walk from the beginning). This makes a restart across an enumerator switch
/// idempotent rather than corrupting.
pub(crate) fn decode_disk_walk_token(token: &str) -> Option<String> {
    if token.is_empty() {
        return None;
    }

    let Some(encoded) = token.strip_prefix(DISK_WALK_TOKEN_PREFIX) else {
        warn!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_ADMIN_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "decode_disk_walk_token",
            state = "foreign_or_missing_prefix",
            "Disk-walk continuation token missing dw1 prefix; restarting walk"
        );
        return None;
    };

    let bytes = match URL_SAFE_NO_PAD.decode(encoded) {
        Ok(bytes) => bytes,
        Err(e) => {
            warn!(
                target: "rustfs::heal::storage",
                event = EVENT_HEAL_STORAGE_ADMIN_OP,
                component = LOG_COMPONENT_HEAL,
                subsystem = LOG_SUBSYSTEM_STORAGE,
                operation = "decode_disk_walk_token",
                state = "bad_base64",
                error = %e,
                "Disk-walk continuation token has invalid base64; restarting walk"
            );
            return None;
        }
    };

    match String::from_utf8(bytes) {
        Ok(forward) if !forward.is_empty() => Some(forward),
        Ok(_) => None,
        Err(e) => {
            warn!(
                target: "rustfs::heal::storage",
                event = EVENT_HEAL_STORAGE_ADMIN_OP,
                component = LOG_COMPONENT_HEAL,
                subsystem = LOG_SUBSYSTEM_STORAGE,
                operation = "decode_disk_walk_token",
                state = "bad_utf8",
                error = %e,
                "Disk-walk continuation token has invalid utf8; restarting walk"
            );
            None
        }
    }
}

/// A single object version to heal.
///
/// `is_delete_marker` is OBSERVABILITY-ONLY (metrics / logging / e2e
/// assertions); it MUST NOT gate healing logic. Whether the delete-marker path
/// or the data path is taken is decided internally in `ops/heal.rs` from
/// `latest_meta.deleted`. `version_id` is normalized (nil/absent UUID => `None`)
/// at the single construction point in `list_objects_for_heal_page`.
#[derive(Debug, Clone)]
pub struct HealListItem {
    /// object key
    pub name: String,
    /// normalized version id (`None` when the version is nil/absent)
    pub version_id: Option<String>,
    /// version modification time as Unix nanoseconds
    pub mod_time_unix_nanos: Option<i128>,
    /// object snapshot for lifecycle evaluation
    pub lifecycle_object_info: Option<HealObjectInfo>,
    /// whether this version is a delete marker (observability only)
    pub is_delete_marker: bool,
}

/// Heal storage layer interface
#[async_trait]
pub trait HealStorageAPI: Send + Sync {
    /// Get object meta
    ///
    /// Reserved for HS-01 MRF wiring (rustfs/backlog#1865): MRF intents
    /// currently execute through `heal_object`; keep this entry point for the
    /// metadata-corruption variant that must inspect metadata first.
    async fn get_object_meta(&self, bucket: &str, object: &str) -> Result<Option<HealObjectInfo>>;

    /// EC decode rebuild
    ///
    /// Reserved for HS-01 MRF wiring (rustfs/backlog#1865): urgent ECDecode
    /// requests currently execute through `heal_object`; keep the explicit
    /// rebuild-and-read path for the decode-failure fast variant.
    async fn ec_decode_rebuild(&self, bucket: &str, object: &str) -> Result<Vec<u8>>;

    /// Get bucket info
    async fn get_bucket_info(&self, bucket: &str) -> Result<Option<BucketInfo>>;

    /// Aggregate usage-cache baselines for the requested buckets.
    async fn erasure_set_usage_baseline(&self, _buckets: &[String]) -> Result<Option<HealBucketUsageBaseline>> {
        Ok(None)
    }

    /// Load per-bucket lifecycle expiry context for heal skips.
    async fn load_heal_lifecycle_expiry_context(&self, _bucket: &str) -> Result<Option<HealLifecycleExpiryContext>> {
        Ok(None)
    }

    /// Queue lifecycle expiry for a version that heal can skip.
    async fn enqueue_heal_lifecycle_expiry(
        &self,
        _context: &HealLifecycleExpiryContext,
        _bucket: &str,
        _object: &str,
        _version_id: Option<&str>,
        _object_info: Option<&HealObjectInfo>,
    ) -> Result<bool> {
        Ok(false)
    }

    /// Get all buckets
    async fn list_buckets(&self) -> Result<Vec<BucketInfo>>;

    /// Check object exists
    async fn object_exists(&self, bucket: &str, object: &str) -> Result<bool>;

    /// Heal object using ecstore
    async fn heal_object(
        &self,
        bucket: &str,
        object: &str,
        version_id: Option<&str>,
        opts: &HealOpts,
    ) -> Result<(HealResultItem, Option<Error>)>;

    /// Heal bucket using ecstore
    async fn heal_bucket(&self, bucket: &str, opts: &HealOpts) -> Result<HealResultItem>;

    /// Heal format using ecstore
    async fn heal_format(&self, dry_run: bool) -> Result<(HealResultItem, Option<Error>)>;

    /// Heal only the explicitly admitted replacement targets in one erasure set.
    ///
    /// The default is deliberately fail-closed so alternate storage
    /// implementations cannot accidentally fall back to the global format path.
    async fn heal_replacement_format(
        &self,
        _dry_run: bool,
        _pool_index: usize,
        _set_index: usize,
        _targets: &[String],
    ) -> Result<(HealResultItem, Option<Error>)> {
        Err(Error::other("target-scoped replacement format is unsupported"))
    }

    /// Recheck admitted replacement targets immediately before destructive work.
    async fn replacement_targets_ready(&self, _targets: &[String]) -> Result<bool> {
        Ok(false)
    }

    /// Read target-specific physical evidence for one replacement version.
    ///
    /// This is only used by automatic replacement healing after the normal
    /// transaction returns success. The conservative default prevents an
    /// alternate backend from turning an unverified replacement into a
    /// completed generation.
    async fn replacement_targets_have_version(
        &self,
        _bucket: &str,
        _object: &str,
        _version_id: Option<&str>,
        _opts: &HealOpts,
        _targets: &[String],
    ) -> Result<bool> {
        Ok(false)
    }

    /// List object versions for healing with pagination (returns one page and continuation token)
    /// Returns (versions, next_continuation_token, is_truncated). The continuation token is an
    /// opaque composite `(marker, version_marker)` value — see `encode_heal_token`/`decode_heal_token`.
    async fn list_objects_for_heal_page(
        &self,
        bucket: &str,
        prefix: &str,
        continuation_token: Option<&str>,
        include_lifecycle_object_info: bool,
    ) -> Result<(Vec<HealListItem>, Option<String>, bool)>;

    /// List versions for healing via a per-erasure-set DISK-WALK union enumerator
    /// (backlog#920). Unlike `list_objects_for_heal_page` (which reflects only the
    /// READ-QUORUM metadata view via `list_object_versions`), this surfaces every
    /// `(object, version)` present on ANY disk in the set identified by
    /// `set_disk_id`, so sub-quorum-but-reconstructable versions are healed.
    ///
    /// The continuation token uses the disjoint `"dw1:"` namespace. The DEFAULT
    /// implementation falls back to the read-quorum listing so mock/alternate
    /// storages keep compiling and behaving; `ECStoreHealStorage` overrides it
    /// with the real disk walk.
    async fn list_versions_for_heal_page_disk_walk(
        &self,
        _set_disk_id: &str,
        bucket: &str,
        prefix: &str,
        continuation_token: Option<&str>,
        include_lifecycle_object_info: bool,
    ) -> Result<(Vec<HealListItem>, Option<String>, bool)> {
        self.list_objects_for_heal_page(bucket, prefix, continuation_token, include_lifecycle_object_info)
            .await
    }

    /// Get disk for resume functionality.
    async fn get_disk_for_resume(&self, set_disk_id: &str) -> Result<DiskStore>;

    /// Get a healthy non-target disk for durable replacement state.
    async fn get_disk_for_resume_excluding(&self, _set_disk_id: &str, _excluded_targets: &[String]) -> Result<DiskStore> {
        Err(Error::other("target-excluding resume disk selection is unsupported"))
    }

    /// Reopen the exact surviving disk that owns an existing replacement
    /// intent. Falling back to another disk would create a second copy of the
    /// same generation and split its progress.
    async fn get_replacement_resume_disk(
        &self,
        _set_disk_id: &str,
        _task_id: &str,
        _excluded_targets: &[String],
    ) -> Result<ReplacementResumeDisk> {
        Err(Error::other("durable replacement resume selection is unsupported"))
    }

    /// Capture the mounted replacement instance before it is formatted.
    async fn replacement_target_identities(&self, _targets: &[String]) -> Result<Vec<ReplacementTargetIdentity>> {
        Err(Error::other("replacement target identity collection is unsupported"))
    }
}

/// ECStore Heal storage layer implementation
pub struct ECStoreHealStorage {
    ecstore: Arc<ECStore>,
}

impl ECStoreHealStorage {
    pub fn new(ecstore: Arc<ECStore>) -> Self {
        Self { ecstore }
    }

    /// Read back an object's bytes, capped to bound memory.
    ///
    /// Private support for the reserved `ec_decode_rebuild` (HS-01); not part
    /// of the storage trait surface.
    async fn get_object_data(&self, bucket: &str, object: &str) -> Result<Option<Vec<u8>>> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_OBJECT_IO,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "get_object_data",
            bucket,
            object,
            "Heal storage request started"
        );

        let reader = match (*self.ecstore)
            .get_object_reader(bucket, object, None, Default::default(), &Default::default())
            .await
        {
            Ok(reader) => reader,
            Err(e) => {
                error!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_OBJECT_IO,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "get_object_data",
                    bucket,
                    object,
                    result = "failed",
                    error = %e,
                    "Heal storage request failed"
                );
                return Err(Error::other(e));
            }
        };

        // WARNING: Returning Vec<u8> for large objects is dangerous. To avoid OOM, cap the read size.
        // If needed, refactor callers to stream instead of buffering entire object.
        const MAX_READ_BYTES: usize = 16 * 1024 * 1024; // 16 MiB cap
        let mut buf = Vec::with_capacity(1024 * 1024);
        use tokio::io::AsyncReadExt as _;
        let mut n_read: usize = 0;
        let mut stream = reader.stream;
        loop {
            // Read in chunks
            let mut chunk = vec![0u8; 1024 * 1024];
            match stream.read(&mut chunk).await {
                Ok(0) => break,
                Ok(n) => {
                    buf.extend_from_slice(&chunk[..n]);
                    n_read += n;
                    if n_read > MAX_READ_BYTES {
                        warn!(
                            target: "rustfs::heal::storage",
                            event = EVENT_HEAL_STORAGE_OBJECT_READ_LIMIT,
                            component = LOG_COMPONENT_HEAL,
                            subsystem = LOG_SUBSYSTEM_STORAGE,
                            bucket,
                            object,
                            max_read_bytes = MAX_READ_BYTES,
                            bytes_read = n_read,
                            "Heal storage aborted object read after reaching safety cap"
                        );
                        return Err(Error::other(format!(
                            "Object too large: {n_read} bytes (max: {MAX_READ_BYTES} bytes) for {bucket}/{object}"
                        )));
                    }
                }
                Err(e) => {
                    error!(
                        target: "rustfs::heal::storage",
                        event = EVENT_HEAL_STORAGE_OBJECT_IO,
                        component = LOG_COMPONENT_HEAL,
                        subsystem = LOG_SUBSYSTEM_STORAGE,
                        operation = "read_object_data",
                        bucket,
                        object,
                        result = "failed",
                        error = %e,
                        "Heal storage request failed"
                    );
                    return Err(Error::other(e));
                }
            }
        }
        Ok(Some(buf))
    }
}

fn is_transient_object_exists_message(message: &str) -> bool {
    let message = message.to_ascii_lowercase();

    [
        "failed to acquire read lock",
        "lock acquisition failed",
        "lock acquisition timeout",
        "quorum not reached",
        "deadline has elapsed",
        "timed out",
        "network error",
        "transport error",
        "connection refused",
    ]
    .iter()
    .any(|pattern| message.contains(pattern))
}

fn is_transient_object_exists_error(err: &StorageError) -> bool {
    if err.is_quorum_error() {
        return true;
    }

    match err {
        StorageError::Lock(lock_err) => lock_err.is_retryable() || is_transient_object_exists_message(&lock_err.to_string()),
        StorageError::Io(io_err) => is_transient_object_exists_message(&io_err.to_string()),
        StorageError::SlowDown | StorageError::OperationCanceled => true,
        _ => false,
    }
}

#[async_trait]
impl HealStorageAPI for ECStoreHealStorage {
    async fn get_object_meta(&self, bucket: &str, object: &str) -> Result<Option<HealObjectInfo>> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_OBJECT_IO,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "get_object_meta",
            bucket,
            object,
            "Heal storage request started"
        );

        match self.ecstore.get_object_info(bucket, object, &Default::default()).await {
            Ok(info) => Ok(Some(info)),
            Err(e) => {
                // Map ObjectNotFound to None to align with Option return type
                if matches!(e, StorageError::ObjectNotFound(_, _)) {
                    debug!(
                        target: "rustfs::heal::storage",
                        event = EVENT_HEAL_STORAGE_OBJECT_IO,
                        component = LOG_COMPONENT_HEAL,
                        subsystem = LOG_SUBSYSTEM_STORAGE,
                        operation = "get_object_meta",
                        bucket,
                        object,
                        result = "not_found",
                        "Heal storage object metadata missing"
                    );
                    Ok(None)
                } else {
                    error!(
                        target: "rustfs::heal::storage",
                        event = EVENT_HEAL_STORAGE_OBJECT_IO,
                        component = LOG_COMPONENT_HEAL,
                        subsystem = LOG_SUBSYSTEM_STORAGE,
                        operation = "get_object_meta",
                        bucket,
                        object,
                        result = "failed",
                        error = %e,
                        "Heal storage request failed"
                    );
                    Err(Error::other(e))
                }
            }
        }
    }

    async fn ec_decode_rebuild(&self, bucket: &str, object: &str) -> Result<Vec<u8>> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_REPAIR_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "ec_decode_rebuild",
            bucket,
            object,
            state = "started",
            "Heal storage repair started"
        );

        // Use ecstore's heal_object to rebuild the object
        let heal_opts = HealOpts {
            recursive: false,
            dry_run: false,
            remove: false,
            recreate: true,
            scan_mode: HealScanMode::Deep,
            update_parity: true,
            no_lock: false,
            pool: None,
            set: None,
        };

        match self.heal_object(bucket, object, None, &heal_opts).await {
            Ok((_result, error)) => {
                if error.is_some() {
                    return Err(Error::TaskExecutionFailed {
                        message: format!("Heal failed: {error:?}"),
                    });
                }

                // After healing, try to read the object data
                match self.get_object_data(bucket, object).await? {
                    Some(data) => {
                        debug!(
                            target: "rustfs::heal::storage",
                            event = EVENT_HEAL_STORAGE_REPAIR_OP,
                            component = LOG_COMPONENT_HEAL,
                            subsystem = LOG_SUBSYSTEM_STORAGE,
                            operation = "ec_decode_rebuild",
                            bucket,
                            object,
                            bytes = data.len(),
                            state = "ok",
                            "Heal storage EC decode rebuild completed"
                        );
                        Ok(data)
                    }
                    None => {
                        error!(
                            target: "rustfs::heal::storage",
                            event = EVENT_HEAL_STORAGE_REPAIR_OP,
                            component = LOG_COMPONENT_HEAL,
                            subsystem = LOG_SUBSYSTEM_STORAGE,
                            operation = "ec_decode_rebuild",
                            bucket,
                            object,
                            state = "missing_after_heal",
                            "Heal storage repair failed"
                        );
                        Err(Error::TaskExecutionFailed {
                            message: format!("Object not found after heal: {bucket}/{object}"),
                        })
                    }
                }
            }
            Err(e) => {
                error!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_REPAIR_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "ec_decode_rebuild",
                    bucket,
                    object,
                    state = "failed",
                    error = %e,
                    "Heal storage repair failed"
                );
                Err(e)
            }
        }
    }

    async fn get_bucket_info(&self, bucket: &str) -> Result<Option<BucketInfo>> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_ADMIN_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "get_bucket_info",
            bucket,
            state = "started",
            "Heal storage admin operation started"
        );

        match self.ecstore.get_bucket_info(bucket, &Default::default()).await {
            Ok(info) => Ok(Some(info)),
            Err(e) => {
                error!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_ADMIN_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "get_bucket_info",
                    bucket,
                    result = "failed",
                    error = %e,
                    "Heal storage admin operation failed"
                );
                Err(Error::other(e))
            }
        }
    }

    async fn erasure_set_usage_baseline(&self, buckets: &[String]) -> Result<Option<HealBucketUsageBaseline>> {
        if buckets.is_empty() {
            return Ok(None);
        }

        let info = match ecstore_load_admin_data_usage_from_backend_cached(self.ecstore.clone()).await {
            Ok(info) if info.is_complete_bucket_usage_snapshot() => info,
            Ok(_) | Err(_) => return Ok(None),
        };

        let mut baseline = HealBucketUsageBaseline::default();
        for bucket in buckets {
            if let Some(usage) = info.buckets_usage.get(bucket) {
                baseline.objects_count = baseline.objects_count.saturating_add(usage.objects_count);
                baseline.bytes = baseline.bytes.saturating_add(usage.size);
            }
        }

        Ok(Some(baseline))
    }

    async fn load_heal_lifecycle_expiry_context(&self, bucket: &str) -> Result<Option<HealLifecycleExpiryContext>> {
        match self.ecstore.load_heal_lifecycle_expiry_context(bucket).await {
            Ok(Some(context)) => Ok(Some(HealLifecycleExpiryContext::ecstore(context))),
            Ok(None) => Ok(None),
            Err(err) => {
                debug!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_ADMIN_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "load_heal_lifecycle_expiry_context",
                    bucket,
                    result = "failed",
                    error = %err,
                    "Heal storage lifecycle expiry context load failed"
                );
                Ok(None)
            }
        }
    }

    async fn enqueue_heal_lifecycle_expiry(
        &self,
        context: &HealLifecycleExpiryContext,
        bucket: &str,
        object: &str,
        version_id: Option<&str>,
        object_info: Option<&HealObjectInfo>,
    ) -> Result<bool> {
        let context = match &context.inner {
            HealLifecycleExpiryContextInner::Ecstore(context) => context,
            HealLifecycleExpiryContextInner::Test => return Ok(false),
        };
        match self
            .ecstore
            .enqueue_heal_lifecycle_expiry(context, bucket, object, version_id, object_info)
            .await
        {
            Ok(queued) => Ok(queued),
            Err(err) => {
                debug!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_ADMIN_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "enqueue_heal_lifecycle_expiry",
                    bucket,
                    object,
                    version_id = ?version_id,
                    result = "failed",
                    error = %err,
                    "Heal storage lifecycle expiry check failed"
                );
                Ok(false)
            }
        }
    }

    async fn list_buckets(&self) -> Result<Vec<BucketInfo>> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_ADMIN_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "list_buckets",
            state = "started",
            "Heal storage admin operation started"
        );

        match self.ecstore.list_bucket(&Default::default()).await {
            Ok(buckets) => Ok(buckets),
            Err(e) => {
                error!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_ADMIN_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "list_buckets",
                    result = "failed",
                    error = %e,
                    "Heal storage admin operation failed"
                );
                Err(Error::other(e))
            }
        }
    }

    async fn object_exists(&self, bucket: &str, object: &str) -> Result<bool> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_OBJECT_IO,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "object_exists",
            bucket,
            object,
            "Heal storage request started"
        );

        // Existence checks are best-effort for background heal scheduling, so avoid
        // acquiring an extra namespace read lock here.
        let opts = HealObjectOptions {
            no_lock: true,
            ..Default::default()
        };

        match self.ecstore.get_object_info(bucket, object, &opts).await {
            Ok(_) => Ok(true), // Object exists
            Err(e) => {
                if matches!(e, StorageError::ObjectNotFound(_, _)) {
                    debug!(
                        target: "rustfs::heal::storage",
                        event = EVENT_HEAL_STORAGE_OBJECT_IO,
                        component = LOG_COMPONENT_HEAL,
                        subsystem = LOG_SUBSYSTEM_STORAGE,
                        operation = "object_exists",
                        bucket,
                        object,
                        result = "not_found",
                        "Heal storage object absence confirmed"
                    );
                    Ok(false)
                } else if is_transient_object_exists_error(&e) {
                    warn!(
                        target: "rustfs::heal::storage",
                        event = EVENT_HEAL_STORAGE_OBJECT_IO,
                        component = LOG_COMPONENT_HEAL,
                        subsystem = LOG_SUBSYSTEM_STORAGE,
                        operation = "object_exists",
                        bucket,
                        object,
                        result = "transient_skip",
                        error = %e,
                        "Heal storage request skipped due to transient error"
                    );
                    Err(Error::transient_skip(format!(
                        "Skipped object existence check for {bucket}/{object}: {e}"
                    )))
                } else {
                    error!(
                        target: "rustfs::heal::storage",
                        event = EVENT_HEAL_STORAGE_OBJECT_IO,
                        component = LOG_COMPONENT_HEAL,
                        subsystem = LOG_SUBSYSTEM_STORAGE,
                        operation = "object_exists",
                        bucket,
                        object,
                        result = "failed",
                        error = %e,
                        "Heal storage request failed"
                    );
                    Err(Error::other(e))
                }
            }
        }
    }

    async fn heal_object(
        &self,
        bucket: &str,
        object: &str,
        version_id: Option<&str>,
        opts: &HealOpts,
    ) -> Result<(HealResultItem, Option<Error>)> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_REPAIR_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "heal_object",
            bucket,
            object,
            version_id = ?version_id,
            scan_mode = %opts.scan_mode.as_str(),
            dry_run = opts.dry_run,
            state = "started",
            "Heal storage repair started"
        );

        let version_id_str = version_id.unwrap_or("");

        match self.ecstore.heal_object(bucket, object, version_id_str, opts).await {
            Ok((result, ecstore_error)) => {
                let error = ecstore_error.map(Error::Storage);
                debug!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_REPAIR_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "heal_object",
                    bucket,
                    object,
                    version_id = ?version_id,
                    drives_after = result.after.drives.len(),
                    has_error = error.is_some(),
                    result = "ok",
                    "Heal storage object repair completed"
                );
                Ok((result, error))
            }
            Err(e) => {
                error!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_REPAIR_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "heal_object",
                    bucket,
                    object,
                    version_id = ?version_id,
                    result = "failed",
                    error = %e,
                    "Heal storage repair failed"
                );
                Err(Error::Storage(e))
            }
        }
    }

    async fn heal_bucket(&self, bucket: &str, opts: &HealOpts) -> Result<HealResultItem> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_REPAIR_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "heal_bucket",
            bucket,
            dry_run = opts.dry_run,
            recursive = opts.recursive,
            state = "started",
            "Heal storage repair started"
        );

        match self.ecstore.heal_bucket(bucket, opts).await {
            Ok(result) => {
                debug!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_REPAIR_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "heal_bucket",
                    bucket,
                    drives_after = result.after.drives.len(),
                    result = "ok",
                    "Heal storage bucket repair completed"
                );
                Ok(result)
            }
            Err(e) => {
                error!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_REPAIR_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "heal_bucket",
                    bucket,
                    result = "failed",
                    error = %e,
                    "Heal storage repair failed"
                );
                Err(Error::Storage(e))
            }
        }
    }

    async fn heal_format(&self, dry_run: bool) -> Result<(HealResultItem, Option<Error>)> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_REPAIR_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "heal_format",
            dry_run,
            state = "started",
            "Heal storage repair started"
        );

        match self.ecstore.heal_format(dry_run).await {
            Ok((result, ecstore_error)) => {
                let error = ecstore_error.map(Error::Storage);
                debug!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_REPAIR_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "heal_format",
                    drives_after = result.after.drives.len(),
                    has_error = error.is_some(),
                    result = "ok",
                    "Heal storage format repair completed"
                );
                Ok((result, error))
            }
            Err(e) => {
                error!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_REPAIR_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "heal_format",
                    result = "failed",
                    error = %e,
                    "Heal storage repair failed"
                );
                Err(Error::Storage(e))
            }
        }
    }

    async fn heal_replacement_format(
        &self,
        dry_run: bool,
        pool_index: usize,
        set_index: usize,
        targets: &[String],
    ) -> Result<(HealResultItem, Option<Error>)> {
        self.ecstore
            .heal_replacement_format(dry_run, pool_index, set_index, targets)
            .await
            .map(|(result, error)| (result, error.map(Error::Storage)))
            .map_err(Error::Storage)
    }

    async fn replacement_targets_ready(&self, targets: &[String]) -> Result<bool> {
        Ok(super::replacement_readiness::auto_replacement_targets_ready(targets).await)
    }

    async fn replacement_targets_have_version(
        &self,
        bucket: &str,
        object: &str,
        version_id: Option<&str>,
        opts: &HealOpts,
        targets: &[String],
    ) -> Result<bool> {
        let pool_index = opts
            .pool
            .ok_or_else(|| Error::other("replacement target readback is missing pool scope"))?;
        let set_index = opts
            .set
            .ok_or_else(|| Error::other("replacement target readback is missing set scope"))?;
        self.ecstore
            .replacement_targets_have_version(bucket, object, version_id.unwrap_or(""), pool_index, set_index, targets)
            .await
            .map_err(Error::Storage)
    }

    async fn list_objects_for_heal_page(
        &self,
        bucket: &str,
        prefix: &str,
        continuation_token: Option<&str>,
        include_lifecycle_object_info: bool,
    ) -> Result<(Vec<HealListItem>, Option<String>, bool)> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_ADMIN_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "list_objects_for_heal_page",
            bucket,
            prefix,
            continuation_token = ?continuation_token,
            state = "started",
            "Heal storage admin operation started"
        );

        const MAX_KEYS: i32 = 1000;
        // Decode the opaque composite token into the (marker, version_marker)
        // pair that list_object_versions consumes. Malformed tokens restart the
        // listing from the beginning (decode_heal_token is total).
        let (marker, version_marker) = decode_heal_token(continuation_token.unwrap_or(""));

        // Enumerate EVERY version (not just the latest) so old versions and
        // delete-marker-latest objects are healed too.
        let list_info = match self
            .ecstore
            .clone()
            .list_object_versions(bucket, prefix, marker, version_marker, None, MAX_KEYS)
            .await
        {
            Ok(info) => info,
            Err(e) => {
                error!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_ADMIN_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "list_objects_for_heal_page",
                    bucket,
                    prefix,
                    result = "failed",
                    error = %e,
                    "Heal storage admin operation failed"
                );
                return Err(Error::other(e));
            }
        };

        // Collect versions from this page. version_id is normalized to Option<String>
        // here at the single construction point: nil/absent UUID => None.
        let page_objects: Vec<HealListItem> = list_info
            .objects
            .into_iter()
            .map(|mut obj| {
                obj.version_id = obj.version_id.filter(|u| !u.is_nil());
                let version_id = obj.version_id.map(|u| u.to_string());
                let mod_time_unix_nanos = obj.mod_time.map(|mod_time| mod_time.unix_timestamp_nanos());
                let is_delete_marker = obj.delete_marker;
                if include_lifecycle_object_info {
                    HealListItem {
                        name: obj.name.clone(),
                        version_id,
                        mod_time_unix_nanos,
                        lifecycle_object_info: Some(obj),
                        is_delete_marker,
                    }
                } else {
                    HealListItem {
                        name: obj.name,
                        version_id,
                        mod_time_unix_nanos,
                        lifecycle_object_info: None,
                        is_delete_marker,
                    }
                }
            })
            .collect();
        let page_count = page_objects.len();

        let next_token = if list_info.is_truncated {
            Some(encode_heal_token(
                list_info.next_marker.as_deref(),
                list_info.next_version_idmarker.as_deref(),
            ))
        } else {
            None
        };

        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_ADMIN_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "list_objects_for_heal_page",
            bucket,
            prefix,
            version_count = page_count,
            is_truncated = list_info.is_truncated,
            state = "page_loaded",
            "Heal storage version listing page loaded"
        );

        Ok((page_objects, next_token, list_info.is_truncated))
    }

    async fn list_versions_for_heal_page_disk_walk(
        &self,
        set_disk_id: &str,
        bucket: &str,
        prefix: &str,
        continuation_token: Option<&str>,
        include_lifecycle_object_info: bool,
    ) -> Result<(Vec<HealListItem>, Option<String>, bool)> {
        // Per-page bounds for the disk-walk union enumerator. Objects are atomic
        // (never split across pages), so version_budget only bounds how many
        // versions accumulate before the page is cut at the next object boundary.
        const BATCH_OBJECTS: usize = 1000;
        const VERSION_BUDGET: usize = 10_000;

        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_ADMIN_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "list_versions_for_heal_page_disk_walk",
            set_disk_id,
            bucket,
            prefix,
            continuation_token = ?continuation_token,
            state = "started",
            "Heal storage disk-walk union enumeration started"
        );

        let (pool_idx, set_idx) = crate::heal::utils::parse_set_disk_id(set_disk_id)?;
        // Decode the dw1: cursor into the forward_to object key. Malformed/foreign
        // tokens restart the walk from the beginning (decode_disk_walk_token is total).
        let forward_to = decode_disk_walk_token(continuation_token.unwrap_or(""));

        let (versions, next_forward, is_truncated) = self
            .ecstore
            .heal_walk_versions_page(
                pool_idx,
                set_idx,
                bucket,
                prefix,
                forward_to.as_deref(),
                BATCH_OBJECTS,
                VERSION_BUDGET,
                include_lifecycle_object_info,
            )
            .await
            .map_err(|e| {
                error!(
                    target: "rustfs::heal::storage",
                    event = EVENT_HEAL_STORAGE_ADMIN_OP,
                    component = LOG_COMPONENT_HEAL,
                    subsystem = LOG_SUBSYSTEM_STORAGE,
                    operation = "list_versions_for_heal_page_disk_walk",
                    set_disk_id,
                    bucket,
                    prefix,
                    result = "failed",
                    error = %e,
                    "Heal storage disk-walk union enumeration failed"
                );
                Error::other(e)
            })?;

        let page_objects: Vec<HealListItem> = versions
            .into_iter()
            .map(|v| HealListItem {
                name: v.name,
                version_id: v.version_id,
                mod_time_unix_nanos: v.mod_time_unix_nanos,
                lifecycle_object_info: v.lifecycle_object_info,
                is_delete_marker: v.is_delete_marker,
            })
            .collect();
        let page_count = page_objects.len();

        let next_token = if is_truncated {
            next_forward.map(|fw| encode_disk_walk_token(&fw))
        } else {
            None
        };

        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_ADMIN_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "list_versions_for_heal_page_disk_walk",
            set_disk_id,
            bucket,
            prefix,
            version_count = page_count,
            is_truncated,
            state = "page_loaded",
            "Heal storage disk-walk union enumeration page loaded"
        );

        Ok((page_objects, next_token, is_truncated))
    }

    async fn get_disk_for_resume(&self, set_disk_id: &str) -> Result<DiskStore> {
        self.get_disk_for_resume_excluding(set_disk_id, &[]).await
    }

    async fn get_disk_for_resume_excluding(&self, set_disk_id: &str, excluded_targets: &[String]) -> Result<DiskStore> {
        debug!(
            target: "rustfs::heal::storage",
            event = EVENT_HEAL_STORAGE_ADMIN_OP,
            component = LOG_COMPONENT_HEAL,
            subsystem = LOG_SUBSYSTEM_STORAGE,
            operation = "get_disk_for_resume",
            set_disk_id,
            state = "started",
            "Heal storage admin operation started"
        );

        // Parse set_disk_id to extract pool and set indices
        let (pool_idx, set_idx) = crate::heal::utils::parse_set_disk_id(set_disk_id)?;

        // Get the first available disk from the set
        let disks = StorageAdminApi::disk_set_inventory(self.ecstore.as_ref(), DiskSetSelector::new(pool_idx, set_idx))
            .await
            .map_err(|e| Error::TaskExecutionFailed {
                message: format!("Failed to get disks for pool {pool_idx} set {set_idx}: {e}"),
            })?;

        // The replacement target is unformatted before repair and must never
        // host the intent that authorizes its own formatting.
        for disk_store in disks.into_iter().flatten() {
            if !disk_store.endpoint().is_local {
                continue;
            }
            if excluded_targets.contains(&disk_store.endpoint().to_string()) {
                continue;
            }
            if !matches!(disk_store.get_disk_id().await, Ok(Some(id)) if !id.is_nil()) {
                continue;
            }
            debug!(
                target: "rustfs::heal::storage",
                event = EVENT_HEAL_STORAGE_ADMIN_OP,
                component = LOG_COMPONENT_HEAL,
                subsystem = LOG_SUBSYSTEM_STORAGE,
                operation = "get_disk_for_resume",
                set_disk_id,
                result = "ok",
                disk = ?disk_store,
                "Heal storage resume disk resolved"
            );
            return Ok(disk_store);
        }

        Err(Error::TaskExecutionFailed {
            message: format!("No available disk found for set_disk_id: {set_disk_id}"),
        })
    }

    async fn get_replacement_resume_disk(
        &self,
        set_disk_id: &str,
        task_id: &str,
        excluded_targets: &[String],
    ) -> Result<ReplacementResumeDisk> {
        let (pool_idx, set_idx) = crate::heal::utils::parse_set_disk_id(set_disk_id)?;
        let disks = StorageAdminApi::disk_set_inventory(self.ecstore.as_ref(), DiskSetSelector::new(pool_idx, set_idx))
            .await
            .map_err(|e| Error::TaskExecutionFailed {
                message: format!("Failed to get disks for pool {pool_idx} set {set_idx}: {e}"),
            })?;
        let mut existing = None;
        for disk_store in disks.into_iter().flatten() {
            if !disk_store.endpoint().is_local || excluded_targets.contains(&disk_store.endpoint().to_string()) {
                continue;
            }
            if !matches!(disk_store.get_disk_id().await, Ok(Some(id)) if !id.is_nil()) {
                continue;
            }
            if super::resume::ResumeManager::has_replacement_intent(&disk_store, task_id).await
                && existing.replace(disk_store).is_some()
            {
                return Err(Error::TaskExecutionFailed {
                    message: format!("Replacement resume intent is duplicated for set_disk_id: {set_disk_id}"),
                });
            }
        }
        Ok(existing.map_or(ReplacementResumeDisk::Fresh, ReplacementResumeDisk::Existing))
    }

    async fn replacement_target_identities(&self, targets: &[String]) -> Result<Vec<ReplacementTargetIdentity>> {
        super::replacement_readiness::auto_replacement_target_identities(targets)
            .await
            .ok_or_else(|| Error::other("replacement target is not a stable mounted disk"))
    }
}

#[cfg(test)]
mod tests {
    use super::super::StorageError;
    use super::{
        decode_disk_walk_token, decode_heal_token, encode_disk_walk_token, encode_heal_token, is_transient_object_exists_error,
        is_transient_object_exists_message, next_heal_listing_token,
    };
    use base64::Engine as _;

    #[test]
    fn next_heal_listing_token_returns_none_for_complete_page() {
        assert_eq!(
            next_heal_listing_token("bucket", "prefix", None, false).expect("complete page should not fail"),
            None
        );
    }

    #[test]
    fn next_heal_listing_token_returns_token_for_truncated_page() {
        assert_eq!(
            next_heal_listing_token("bucket", "prefix", Some("token-1".to_string()), true)
                .expect("truncated page with token should continue"),
            Some("token-1".to_string())
        );
    }

    #[test]
    fn next_heal_listing_token_treats_truncated_page_without_token_as_end() {
        // A version listing can report the final page as truncated with no
        // continuation token; that must terminate the scan cleanly, not error.
        assert_eq!(
            next_heal_listing_token("bucket", "prefix", None, true).expect("truncated without token ends listing"),
            None
        );
    }

    #[test]
    fn test_heal_token_roundtrip() {
        let token = encode_heal_token(Some("obj/key"), Some("v-123"));
        assert!(token.starts_with("v1:"));
        assert_eq!(decode_heal_token(&token), (Some("obj/key".to_string()), Some("v-123".to_string())));

        // marker only (no version marker) round-trips.
        let token = encode_heal_token(Some("obj/key"), None);
        assert_eq!(decode_heal_token(&token), (Some("obj/key".to_string()), None));

        // (None, None) round-trips.
        let token = encode_heal_token(None, None);
        assert_eq!(decode_heal_token(&token), (None, None));
    }

    #[test]
    fn test_heal_token_malformed_resets_to_start() {
        // empty, wrong prefix, bad base64, and bad json all reset to (None, None).
        assert_eq!(decode_heal_token(""), (None, None));
        assert_eq!(decode_heal_token("no-prefix-here"), (None, None));
        assert_eq!(decode_heal_token("v1:!!!not-base64!!!"), (None, None));
        // valid base64 of non-JSON bytes.
        let bad_json = format!("v1:{}", base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"not json"));
        assert_eq!(decode_heal_token(&bad_json), (None, None));
        // a raw v2-style list_objects_v2 token (no "v1:" prefix) resets cleanly.
        assert_eq!(decode_heal_token("some-opaque-legacy-token"), (None, None));
    }

    #[test]
    fn test_heal_token_none_and_marker_only() {
        // A decoded payload must NEVER yield (None, Some(_)) because
        // list_object_versions returns NotImplemented for that pairing.
        // Craft a token whose JSON encodes (None, Some) directly and confirm coercion.
        let json = br#"{"m":null,"v":"orphan-version"}"#;
        let token = format!("v1:{}", base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json));
        assert_eq!(decode_heal_token(&token), (None, None), "version-only marker must coerce to (None, None)");
    }

    #[test]
    fn disk_walk_cursor_round_trip_and_foreign_token_restarts() {
        // Round-trip: a real object key survives encode -> decode.
        let token = encode_disk_walk_token("some/deep/object.bin");
        assert!(token.starts_with("dw1:"));
        assert_eq!(decode_disk_walk_token(&token), Some("some/deep/object.bin".to_string()));

        // Empty / garbage / missing-prefix all restart the walk (None).
        assert_eq!(decode_disk_walk_token(""), None);
        assert_eq!(decode_disk_walk_token("dw1:!!!not-base64!!!"), None);
        assert_eq!(decode_disk_walk_token("no-prefix-here"), None);

        // CROSS-DECODER ISOLATION (both directions): a dw1 token must not be
        // misread as a B5 (marker, version_marker) pair, and a v1 token must not
        // be misread as a disk-walk forward cursor.
        let dw = encode_disk_walk_token("obj/key");
        assert_eq!(
            decode_heal_token(&dw),
            (None, None),
            "a dw1: token must decode to (None, None) under the B5 decoder"
        );

        let v1 = encode_heal_token(Some("obj/key"), Some("v-123"));
        assert!(v1.starts_with("v1:"));
        assert_eq!(
            decode_disk_walk_token(&v1),
            None,
            "a v1: token must decode to None under the disk-walk decoder"
        );
    }

    #[test]
    fn transient_object_exists_message_matches_lock_quorum_failures() {
        assert!(is_transient_object_exists_message(
            "Failed to acquire read lock: ns_loc: read lock acquisition failed on bucket/object: Quorum not reached: required 2, achieved 0"
        ));
        assert!(is_transient_object_exists_message("deadline has elapsed"));
    }

    #[test]
    fn transient_object_exists_error_matches_quorum_variants() {
        assert!(is_transient_object_exists_error(&StorageError::ErasureReadQuorum));
        assert!(is_transient_object_exists_error(&StorageError::InsufficientReadQuorum(
            "bucket".to_string(),
            "object".to_string(),
        )));
    }

    #[test]
    fn transient_object_exists_error_does_not_treat_not_found_as_transient() {
        assert!(!is_transient_object_exists_error(&StorageError::ObjectNotFound(
            "bucket".to_string(),
            "object".to_string(),
        )));
    }
}
