// 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::cluster::rpc::{
    ScannerBucketListing, TonicInterceptor, gen_tonic_signature_interceptor, node_service_time_out_client,
};
use crate::data_usage::{DATA_USAGE_CACHE_NAME, DATA_USAGE_ROOT, load_data_usage_from_backend_cached};
use crate::error::{Error, Result};
use crate::{
    disk::endpoint::{Endpoint, EndpointType},
    layout::endpoints::EndpointServerPools,
    runtime::sources as runtime_sources,
};

use crate::data_usage::load_data_usage_cache;
use crate::storage_api_contracts::admin::StorageAdminApi;
use crate::storage_api_contracts::bucket::BucketOptions;
use rustfs_heal_contracts::heal_channel::DriveState;
use rustfs_madmin::{
    BackendDisks, Disk, ErasureSetInfo, ITEM_INITIALIZING, ITEM_OFFLINE, ITEM_ONLINE, ITEM_UNKNOWN, InfoMessage, MemStats,
    ServerProperties,
};
use rustfs_protos::{
    models::{PingBody, PingBodyBuilder},
    proto_gen::node_service::{PingRequest, PingResponse},
};
use std::{
    collections::{HashMap, HashSet},
    time::Duration,
};
use time::OffsetDateTime;
use tokio::time::timeout;
use tonic::Request;
use tracing::warn;

use shadow_rs::shadow;

shadow!(build);

const SERVER_PING_TIMEOUT: Duration = Duration::from_secs(1);
const DATA_USAGE_UNAVAILABLE_ERROR: &str = "data usage snapshot unavailable";

fn apply_data_usage_result(
    result: Result<rustfs_data_usage::DataUsageInfo>,
    buckets: &mut rustfs_madmin::Buckets,
    objects: &mut rustfs_madmin::Objects,
    versions: &mut rustfs_madmin::Versions,
    delete_markers: &mut rustfs_madmin::DeleteMarkers,
    usage: &mut rustfs_madmin::Usage,
) {
    match result {
        Ok(info) if info.is_complete_bucket_usage_snapshot() => {
            buckets.count = info.buckets_count;
            objects.count = info.objects_total_count;
            versions.count = info.versions_total_count;
            delete_markers.count = info.delete_markers_total_count;
            usage.size = info.objects_total_size;
        }
        Ok(_) | Err(_) => {
            buckets.error = Some(DATA_USAGE_UNAVAILABLE_ERROR.to_string());
            objects.error = Some(DATA_USAGE_UNAVAILABLE_ERROR.to_string());
            versions.error = Some(DATA_USAGE_UNAVAILABLE_ERROR.to_string());
            delete_markers.error = Some(DATA_USAGE_UNAVAILABLE_ERROR.to_string());
            usage.error = Some(DATA_USAGE_UNAVAILABLE_ERROR.to_string());
        }
    }
}

fn apply_bucket_namespace_count(result: Result<ScannerBucketListing>, buckets: &mut rustfs_madmin::Buckets) {
    if let Ok(listing) = result
        && listing.topology_complete
    {
        let count = listing.buckets.iter().filter(|bucket| !bucket.name.starts_with('.')).count();
        let Ok(count) = u64::try_from(count) else {
            return;
        };
        buckets.count = count;
        buckets.error = None;
    }
}

// pub const ITEM_OFFLINE: &str = "offline";
// pub const ITEM_INITIALIZING: &str = "initializing";
// pub const ITEM_ONLINE: &str = "online";

// #[derive(Debug, Default, Serialize, Deserialize)]
// pub struct MemStats {
//     alloc: u64,
//     total_alloc: u64,
//     mallocs: u64,
//     frees: u64,
//     heap_alloc: u64,
// }

// #[derive(Debug, Default, Serialize, Deserialize)]
// pub struct ServerProperties {
//     pub state: String,
//     pub endpoint: String,
//     pub scheme: String,
//     pub uptime: u64,
//     pub version: String,
//     pub commit_id: String,
//     pub network: HashMap<String, String>,
//     pub disks: Vec<madmin::Disk>,
//     pub pool_number: i32,
//     pub pool_numbers: Vec<i32>,
//     pub mem_stats: MemStats,
//     pub max_procs: u64,
//     pub num_cpu: u64,
//     pub runtime_version: String,
//     pub rustfs_env_vars: HashMap<String, String>,
// }

async fn is_server_resolvable(endpoint: &Endpoint) -> Result<()> {
    let addr = format!(
        "{}://{}:{}",
        endpoint.url.scheme(),
        endpoint.url.host_str().expect("URL should have host"),
        // `Url::port()` is None when the URL uses the scheme's default port
        // (e.g. http on 80 / https on 443); fall back to the scheme default.
        endpoint.url.port_or_known_default().expect("URL should have port")
    );

    let ping_task = async {
        let mut fbb = flatbuffers::FlatBufferBuilder::new();
        let payload = fbb.create_vector(b"hello world");

        let mut builder = PingBodyBuilder::new(&mut fbb);
        builder.add_payload(payload);
        let root = builder.finish();
        fbb.finish(root, None);

        let finished_data = fbb.finished_data();

        let decoded_payload = flatbuffers::root::<PingBody>(finished_data);
        assert!(decoded_payload.is_ok());

        let mut client = node_service_time_out_client(&addr, TonicInterceptor::Signature(gen_tonic_signature_interceptor()))
            .await
            .map_err(|err| Error::RemoteClientUnavailable(err.to_string()))?;

        let request = Request::new(PingRequest {
            version: 1,
            body: bytes::Bytes::copy_from_slice(finished_data),
        });

        let response: PingResponse = client.ping(request).await?.into_inner();

        let ping_response_body = flatbuffers::root::<PingBody>(&response.body);
        if let Err(e) = ping_response_body {
            eprintln!("{e}");
        } else {
            println!("ping_resp:body(flatbuffer): {ping_response_body:?}");
        }

        Ok(())
    };

    timeout(SERVER_PING_TIMEOUT, ping_task)
        .await
        .map_err(|_| Error::other("server ping timeout"))?
}

pub async fn get_local_server_property() -> ServerProperties {
    let addr = runtime_sources::local_node_name().await;
    let mut pool_numbers = HashSet::new();
    let mut network = HashMap::new();
    let (mem_stats, max_procs, num_cpu) = collect_runtime_server_stats();

    let endpoints = match runtime_sources::endpoint_pools() {
        Some(eps) => eps,
        None => {
            return ServerProperties {
                state: ITEM_INITIALIZING.to_string(),
                endpoint: addr,
                uptime: runtime_sources::boot_uptime_secs(),
                version: get_commit_id(),
                mem_stats,
                max_procs,
                num_cpu,
                ..Default::default()
            };
        }
    };
    for ep in endpoints.as_ref().iter() {
        for endpoint in ep.endpoints.as_ref().iter() {
            let node_name = match endpoint.url.host_str() {
                Some(s) => s.to_string(),
                None => addr.clone(),
            };
            if endpoint.is_local {
                pool_numbers.insert(endpoint.pool_idx + 1);
                network.insert(node_name, ITEM_ONLINE.to_string());
                continue;
            }
            if let std::collections::hash_map::Entry::Vacant(e) = network.entry(node_name) {
                if is_server_resolvable(endpoint).await.is_err() {
                    e.insert(ITEM_OFFLINE.to_string());
                } else {
                    e.insert(ITEM_ONLINE.to_string());
                }
            }
        }
    }

    let mut props = ServerProperties {
        endpoint: addr,
        uptime: runtime_sources::boot_uptime_secs(),
        network,
        version: get_commit_id(),
        mem_stats,
        max_procs,
        num_cpu,
        ..Default::default()
    };

    for pool_num in pool_numbers.iter() {
        props.pool_numbers.push(*pool_num);
    }
    props.pool_numbers.sort();
    props.pool_number = if props.pool_numbers.len() == 1 {
        props.pool_numbers[0]
    } else {
        i32::MAX
    };

    // let mut sensitive = HashSet::new();
    // sensitive.insert(rustfs_config::ENV_RUSTFS_ACCESS_KEY.to_string());
    // sensitive.insert(rustfs_config::ENV_RUSTFS_SECRET_KEY.to_string());
    if let Some(store) = runtime_sources::object_store_handle() {
        let storage_info = StorageAdminApi::local_storage_info(store.as_ref()).await;
        props.state = ITEM_ONLINE.to_string();
        props.disks = storage_info.disks;
    } else {
        props.state = ITEM_INITIALIZING.to_string();
    };

    props
}

fn collect_runtime_server_stats() -> (MemStats, u64, u64) {
    let num_cpu = u64::try_from(num_cpus::get()).unwrap_or(u64::MAX);
    let max_procs = std::thread::available_parallelism()
        .map(|parallelism| u64::try_from(parallelism.get()).unwrap_or(u64::MAX))
        .unwrap_or(num_cpu.max(1));

    (rustfs_madmin::health::collect_mem_stats(), max_procs, num_cpu)
}

pub async fn get_server_info(get_pools: bool) -> InfoMessage {
    let nowt: OffsetDateTime = OffsetDateTime::now_utc();

    warn!("get_server_info start {:?}", nowt);

    let local = get_local_server_property().await;

    let after1 = OffsetDateTime::now_utc();

    warn!("get_local_server_property end {:?}", after1 - nowt);

    let mut servers = {
        if let Some(sys) = runtime_sources::notification_sys() {
            sys.server_info().await
        } else {
            vec![]
        }
    };

    let after2 = OffsetDateTime::now_utc();

    warn!("server_info end {:?}", after2 - after1);
    servers.push(local);

    let mut buckets = rustfs_madmin::Buckets::default();
    let mut objects = rustfs_madmin::Objects::default();
    let mut versions = rustfs_madmin::Versions::default();
    let mut delete_markers = rustfs_madmin::DeleteMarkers::default();
    let mut usage = rustfs_madmin::Usage::default();
    let mut mode = ITEM_INITIALIZING;
    let mut backend = rustfs_madmin::ErasureBackend::default();
    let mut pools: HashMap<i32, HashMap<i32, ErasureSetInfo>> = HashMap::new();

    if let Some(store) = runtime_sources::object_store_handle() {
        mode = ITEM_ONLINE;
        apply_data_usage_result(
            load_data_usage_from_backend_cached(store.clone()).await,
            &mut buckets,
            &mut objects,
            &mut versions,
            &mut delete_markers,
            &mut usage,
        );
        if buckets.error.is_some() {
            apply_bucket_namespace_count(
                store
                    .list_bucket_for_scanner(&BucketOptions {
                        cached: true,
                        no_metadata: true,
                        ..Default::default()
                    })
                    .await,
                &mut buckets,
            );
        }

        let after3 = OffsetDateTime::now_utc();

        warn!("load_data_usage_from_backend end {:?}", after3 - after2);

        let backend_info = StorageAdminApi::backend_info(store.as_ref()).await;

        let after4 = OffsetDateTime::now_utc();

        warn!("backend_info end {:?}", after4 - after3);
        if let Some(endpoints) = runtime_sources::endpoint_pools() {
            let (added, report) = reconcile_servers_with_endpoint_topology(&mut servers, &endpoints);
            if added > 0 || !report.is_complete() {
                warn!(
                    event = "admin_v3_info_topology_incomplete",
                    synthesized_servers = added,
                    expected_drives = report.expected_drives,
                    observed_drives = report.observed_drives,
                    missing_drives = report.missing_drive_ids.len(),
                    duplicate_drives = report.duplicate_drive_ids.len(),
                    "admin v3 server_info reconciled endpoint topology before computing backend counters"
                );
            }
        }

        let mut all_disks: Vec<Disk> = Vec::new();
        for server in servers.iter() {
            all_disks.extend(server.disks.clone());
        }
        let (online_disks, offline_disks, unknown_disks) = get_online_offline_disks_stats(&all_disks);

        let after5 = OffsetDateTime::now_utc();

        warn!("get_online_offline_disks_stats end {:?}", after5 - after4);
        backend = rustfs_madmin::ErasureBackend {
            backend_type: rustfs_madmin::BackendType::ErasureType,
            online_disks: online_disks.sum(),
            offline_disks: offline_disks.sum(),
            unknown_disks: unknown_disks.sum(),
            standard_sc_parity: backend_info.standard_sc_parity,
            rr_sc_parity: backend_info.rr_sc_parity,
            total_sets: backend_info.total_sets,
            drives_per_set: backend_info.drives_per_set,
        };
        if get_pools {
            pools = get_pools_info(&all_disks).await.unwrap_or_default();
            let after6 = OffsetDateTime::now_utc();

            warn!("get_pools_info end {:?}", after6 - after5);
        }
    }

    let services = rustfs_madmin::Services::default();

    InfoMessage {
        mode: Some(mode.to_string()),
        domain: None,
        region: None,
        sqs_arn: None,
        deployment_id: runtime_sources::deployment_id(),
        buckets: Some(buckets),
        objects: Some(objects),
        versions: Some(versions),
        delete_markers: Some(delete_markers),
        usage: Some(usage),
        backend: Some(backend),
        services: Some(services),
        servers: Some(servers),
        pools: Some(pools),
    }
}

/// Classify every drive into online / offline / unknown buckets.
///
/// `unknown` holds drives synthesized for a member whose properties RPC could
/// not be answered this cycle but which is not confirmed offline. Keeping them
/// out of the `offline` bucket means a transient probe miss no longer inflates
/// the offline count for a healthy member, while `online + offline + unknown`
/// still sums to the pool's total drive count (rustfs/backlog#1049).
fn get_online_offline_disks_stats(disks_info: &[Disk]) -> (BackendDisks, BackendDisks, BackendDisks) {
    let mut online_disks: HashMap<String, usize> = HashMap::new();
    let mut offline_disks: HashMap<String, usize> = HashMap::new();
    let mut unknown_disks: HashMap<String, usize> = HashMap::new();

    for disk in disks_info {
        let ep = &disk.endpoint;
        offline_disks.entry(ep.clone()).or_insert(0);
        online_disks.entry(ep.clone()).or_insert(0);
        unknown_disks.entry(ep.clone()).or_insert(0);
    }

    for disk in disks_info {
        let ep = &disk.endpoint;
        let state = &disk.state;
        if *state == ITEM_UNKNOWN {
            *unknown_disks.get_mut(ep).expect("endpoint should be in disk map") += 1;
            continue;
        }
        if *state != DriveState::Ok.to_string() && *state != DriveState::Unformatted.to_string() {
            *offline_disks.get_mut(ep).expect("endpoint should be in disk map") += 1;
            continue;
        }
        *online_disks.get_mut(ep).expect("endpoint should be in disk map") += 1;
    }

    let mut root_disk_count = 0;
    for di in disks_info {
        if di.root_disk {
            root_disk_count += 1;
        }
    }

    // When every non-offline, non-unknown drive is a root mount, leave the
    // online tally as-is instead of demoting all of them (matches the prior
    // behavior; the unknown bucket is simply carried through untouched).
    if disks_info.len() == (root_disk_count + offline_disks.values().sum::<usize>() + unknown_disks.values().sum::<usize>()) {
        return (BackendDisks(online_disks), BackendDisks(offline_disks), BackendDisks(unknown_disks));
    }

    for disk in disks_info {
        let ep = &disk.endpoint;
        if disk.root_disk {
            *offline_disks.get_mut(ep).expect("endpoint should be in disk map") += 1;
            *online_disks.get_mut(ep).expect("endpoint should be in disk map") -= 1;
        }
    }

    (BackendDisks(online_disks), BackendDisks(offline_disks), BackendDisks(unknown_disks))
}

#[derive(Debug, Default, PartialEq, Eq)]
struct TopologyCompletenessReport {
    expected_drives: usize,
    observed_drives: usize,
    missing_drive_ids: Vec<TopologyDriveKey>,
    duplicate_drive_ids: Vec<TopologyDriveKey>,
}

impl TopologyCompletenessReport {
    fn is_complete(&self) -> bool {
        self.expected_drives == self.observed_drives && self.missing_drive_ids.is_empty() && self.duplicate_drive_ids.is_empty()
    }
}

#[derive(Debug)]
struct TopologyMember {
    display_endpoint: String,
    disks: Vec<Disk>,
    drive_keys: Vec<TopologyDriveKey>,
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct TopologyDriveKey {
    pool_index: i32,
    set_index: i32,
    disk_index: i32,
    member_index: usize,
}

#[derive(Debug, Default)]
struct EndpointTopology {
    members: Vec<TopologyMember>,
    aliases: HashMap<String, usize>,
    expected_drive_ids: HashSet<TopologyDriveKey>,
}

impl EndpointTopology {
    fn from_endpoint_pools(endpoints: &EndpointServerPools) -> Self {
        let mut topology = Self::default();
        let mut by_host_port = HashMap::new();
        let mut display_aliases: HashMap<String, Option<usize>> = HashMap::new();

        for pool in endpoints.as_ref() {
            for ep in pool.endpoints.as_ref() {
                if ep.get_type() != EndpointType::Url {
                    continue;
                }
                let host_port = ep.host_port();
                if host_port.is_empty() {
                    continue;
                }

                let member_index = match by_host_port.get(&host_port).copied() {
                    Some(index) => index,
                    None => {
                        let display_endpoint = ep.url.host_str().map(str::to_owned).unwrap_or_else(|| host_port.clone());
                        let index = topology.members.len();
                        topology.members.push(TopologyMember {
                            display_endpoint,
                            disks: Vec::new(),
                            drive_keys: Vec::new(),
                        });
                        by_host_port.insert(host_port.clone(), index);
                        index
                    }
                };

                topology.aliases.entry(host_port.clone()).or_insert(member_index);
                topology.aliases.entry(ep.to_string()).or_insert(member_index);
                if let Some(display_endpoint) = ep.url.host_str() {
                    match display_aliases.entry(display_endpoint.to_owned()) {
                        std::collections::hash_map::Entry::Vacant(entry) => {
                            entry.insert(Some(member_index));
                        }
                        std::collections::hash_map::Entry::Occupied(mut entry) => {
                            if entry.get().is_some_and(|index| index != member_index) {
                                entry.insert(None);
                            }
                        }
                    }
                }

                let drive_key = TopologyDriveKey {
                    pool_index: ep.pool_idx,
                    set_index: ep.set_idx,
                    disk_index: ep.disk_idx,
                    member_index,
                };
                topology.expected_drive_ids.insert(drive_key.clone());
                topology.members[member_index].drive_keys.push(drive_key);
                topology.members[member_index].disks.push(Disk {
                    endpoint: ep.to_string(),
                    state: ITEM_UNKNOWN.to_string(),
                    pool_index: ep.pool_idx,
                    set_index: ep.set_idx,
                    disk_index: ep.disk_idx,
                    ..Default::default()
                });
            }
        }

        for (display_endpoint, member_index) in display_aliases {
            if let Some(member_index) = member_index {
                topology.aliases.entry(display_endpoint).or_insert(member_index);
            }
        }

        topology
    }

    fn is_empty(&self) -> bool {
        self.members.is_empty()
    }

    fn observe_servers(&self, servers: &[ServerProperties]) -> (Vec<bool>, HashMap<TopologyDriveKey, usize>) {
        let mut observed_members = vec![false; self.members.len()];
        let mut observed_drive_counts = HashMap::with_capacity(self.expected_drive_ids.len());

        for server in servers {
            if let Some(member_index) = self.member_index_from_endpoint(&server.endpoint) {
                observed_members[member_index] = true;
            }

            for disk in &server.disks {
                if let Some(member_index) = self.member_index_from_endpoint(&disk.endpoint) {
                    observed_members[member_index] = true;
                    let drive_key = TopologyDriveKey {
                        pool_index: disk.pool_index,
                        set_index: disk.set_index,
                        disk_index: disk.disk_index,
                        member_index,
                    };
                    if self.expected_drive_ids.contains(&drive_key) {
                        *observed_drive_counts.entry(drive_key).or_insert(0) += 1;
                    }
                }
            }
        }

        (observed_members, observed_drive_counts)
    }

    fn member_index_from_endpoint(&self, endpoint: &str) -> Option<usize> {
        if let Some(index) = self.aliases.get(endpoint) {
            return Some(*index);
        }

        Endpoint::try_from(endpoint)
            .ok()
            .and_then(|ep| self.aliases.get(&ep.host_port()).copied())
    }

    fn report_from_counts(&self, observed_drive_counts: HashMap<TopologyDriveKey, usize>) -> TopologyCompletenessReport {
        let mut missing_drive_ids = Vec::new();
        let mut duplicate_drive_ids = Vec::new();
        for id in &self.expected_drive_ids {
            match observed_drive_counts.get(id).copied().unwrap_or(0) {
                0 => missing_drive_ids.push(id.clone()),
                1 => {}
                _ => duplicate_drive_ids.push(id.clone()),
            }
        }
        missing_drive_ids.sort();
        duplicate_drive_ids.sort();

        TopologyCompletenessReport {
            expected_drives: self.expected_drive_ids.len(),
            observed_drives: observed_drive_counts.values().sum(),
            missing_drive_ids,
            duplicate_drive_ids,
        }
    }
}

fn reconcile_servers_with_endpoint_topology(
    servers: &mut Vec<ServerProperties>,
    endpoints: &EndpointServerPools,
) -> (usize, TopologyCompletenessReport) {
    let topology = EndpointTopology::from_endpoint_pools(endpoints);
    if topology.is_empty() {
        return (0, TopologyCompletenessReport::default());
    }

    let (observed_members, mut observed_drive_counts) = topology.observe_servers(servers);
    let mut missing: Vec<_> = topology
        .members
        .iter()
        .enumerate()
        .filter(|(index, _)| !observed_members[*index])
        .map(|(_, member)| {
            for drive_key in &member.drive_keys {
                *observed_drive_counts.entry(drive_key.clone()).or_insert(0) += 1;
            }
            ServerProperties {
                endpoint: member.display_endpoint.clone(),
                state: ITEM_UNKNOWN.to_string(),
                disks: member.disks.clone(),
                ..Default::default()
            }
        })
        .collect();
    missing.sort_by(|a, b| a.endpoint.cmp(&b.endpoint));

    let added = missing.len();
    servers.extend(missing);
    let report = topology.report_from_counts(observed_drive_counts);

    (added, report)
}

#[allow(dead_code, reason = "exercised by this file's topology tests (backlog#1823)")]
fn server_topology_completeness_report(
    servers: &[ServerProperties],
    endpoints: &EndpointServerPools,
) -> TopologyCompletenessReport {
    let topology = EndpointTopology::from_endpoint_pools(endpoints);
    if topology.is_empty() {
        return TopologyCompletenessReport::default();
    }
    let (_, observed_drive_counts) = topology.observe_servers(servers);
    topology.report_from_counts(observed_drive_counts)
}

async fn get_pools_info(all_disks: &[Disk]) -> Result<HashMap<i32, HashMap<i32, ErasureSetInfo>>> {
    let Some(store) = runtime_sources::object_store_handle() else {
        return Err(Error::other("ServerNotInitialized"));
    };

    let mut pools_info: HashMap<i32, HashMap<i32, ErasureSetInfo>> = HashMap::new();
    for d in all_disks {
        let pool_info = pools_info.entry(d.pool_index).or_default();
        let erasure_set = pool_info.entry(d.set_index).or_default();

        if erasure_set.id == 0 {
            erasure_set.id = d.set_index;
            match load_data_usage_cache(
                store.pools[d.pool_index as usize].disk_set[d.set_index as usize].as_ref(),
                DATA_USAGE_CACHE_NAME,
            )
            .await
            {
                Ok(cache) => apply_erasure_set_usage(&cache, erasure_set),
                Err(_) => erasure_set.usage_error = Some(DATA_USAGE_UNAVAILABLE_ERROR.to_string()),
            }
        }

        erasure_set.raw_capacity += d.total_space;
        erasure_set.raw_usage += d.used_space;
        if d.healing {
            erasure_set.heal_disks = 1;
        }
    }
    Ok(pools_info)
}

fn apply_erasure_set_usage(cache: &rustfs_data_usage::DataUsageCache, erasure_set: &mut ErasureSetInfo) {
    let data_usage_info = cache.dui(DATA_USAGE_ROOT, &[]);
    if !data_usage_info.is_complete_bucket_usage_snapshot() {
        erasure_set.usage_error = Some(DATA_USAGE_UNAVAILABLE_ERROR.to_string());
        return;
    }

    erasure_set.objects_count = data_usage_info.objects_total_count;
    erasure_set.versions_count = data_usage_info.versions_total_count;
    erasure_set.delete_markers_count = data_usage_info.delete_markers_total_count;
    erasure_set.usage = data_usage_info.objects_total_size;
    erasure_set.usage_error = None;
}

#[allow(clippy::const_is_empty)]
pub fn get_commit_id() -> String {
    let ver = if !build::TAG.is_empty() {
        build::TAG.to_string()
    } else if !build::SHORT_COMMIT.is_empty() {
        build::SHORT_COMMIT.to_string()
    } else {
        build::PKG_VERSION.to_string()
    };

    format!("{}@{}", build::COMMIT_DATE_3339, ver)
}

#[cfg(test)]
mod tests {
    use serial_test::serial;

    use crate::layout::{
        endpoint::Endpoint,
        endpoints::{EndpointServerPools, Endpoints, PoolEndpoints},
    };
    use crate::runtime::sources as runtime_sources;
    use crate::storage_api_contracts::bucket::BucketInfo;
    use rustfs_madmin::{Disk, ITEM_OFFLINE, ITEM_ONLINE, ITEM_UNKNOWN, ServerProperties};

    use super::{
        DATA_USAGE_ROOT, DATA_USAGE_UNAVAILABLE_ERROR, apply_bucket_namespace_count, apply_data_usage_result,
        apply_erasure_set_usage, get_local_server_property, get_online_offline_disks_stats, get_server_info,
        reconcile_servers_with_endpoint_topology, server_topology_completeness_report,
    };

    fn disk_with_state(endpoint: &str, state: &str) -> Disk {
        Disk {
            endpoint: endpoint.to_string(),
            state: state.to_string(),
            ..Default::default()
        }
    }

    fn topology_endpoint(host: &str, pool_index: usize, set_index: usize, disk_index: usize) -> Endpoint {
        topology_endpoint_url(format!("http://{host}:9000/data{disk_index}").as_str(), pool_index, set_index, disk_index)
    }

    fn topology_endpoint_url(url: &str, pool_index: usize, set_index: usize, disk_index: usize) -> Endpoint {
        let mut endpoint = Endpoint::try_from(url).expect("URL endpoint should parse");
        endpoint.set_pool_index(pool_index);
        endpoint.set_set_index(set_index);
        endpoint.set_disk_index(disk_index);
        endpoint
    }

    fn topology_with_hosts(hosts: &[&str]) -> EndpointServerPools {
        let endpoints: Vec<Endpoint> = hosts
            .iter()
            .enumerate()
            .map(|(disk_index, host)| topology_endpoint(host, 0, 0, disk_index))
            .collect();
        EndpointServerPools::from(vec![PoolEndpoints {
            legacy: false,
            set_count: 1,
            drives_per_set: hosts.len(),
            endpoints: Endpoints::from(endpoints),
            cmd_line: String::new(),
            platform: String::new(),
        }])
    }

    fn server_with_disk(host: &str, disk_index: i32, state: &str) -> ServerProperties {
        ServerProperties {
            endpoint: host.to_string(),
            state: ITEM_ONLINE.to_string(),
            disks: vec![Disk {
                endpoint: format!("http://{host}:9000/data{disk_index}"),
                state: state.to_string(),
                pool_index: 0,
                set_index: 0,
                disk_index,
                ..Default::default()
            }],
            ..Default::default()
        }
    }

    #[test]
    fn disk_stats_split_unknown_into_its_own_bucket() {
        // A member whose properties RPC could not be answered contributes
        // drives tagged `unknown`. They must land in the unknown bucket, not
        // inflate `offline`, and the three buckets must still account for every
        // drive so the summary stays balanced (rustfs/backlog#1049).
        //
        // A live drive reports the DriveState string "ok"; only "ok"/"unformatted"
        // count as online.
        let disks = vec![
            disk_with_state("http://n1:9000/data", "ok"),
            disk_with_state("http://n2:9000/data", "ok"),
            disk_with_state("http://n3:9000/data", ITEM_OFFLINE),
            disk_with_state("http://n4:9000/data", ITEM_UNKNOWN),
        ];

        let (online, offline, unknown) = get_online_offline_disks_stats(&disks);

        assert_eq!(online.sum(), 2, "the two healthy drives are online");
        assert_eq!(offline.sum(), 1, "only the confirmed-offline drive is offline");
        assert_eq!(unknown.sum(), 1, "the unreachable member's drive is unknown, not offline");
        assert_eq!(
            online.sum() + offline.sum() + unknown.sum(),
            disks.len(),
            "online + offline + unknown must equal the total drive count"
        );
    }

    #[test]
    fn topology_reconcile_synthesizes_missing_member_as_unknown() {
        let endpoints = topology_with_hosts(&["rustfs-1", "rustfs-2", "rustfs-3", "rustfs-4"]);
        let mut servers = vec![
            server_with_disk("rustfs-1", 0, "ok"),
            server_with_disk("rustfs-2", 1, "ok"),
            server_with_disk("rustfs-3", 2, "ok"),
        ];

        let (added, report) = reconcile_servers_with_endpoint_topology(&mut servers, &endpoints);
        let (_, _, unknown) =
            get_online_offline_disks_stats(&servers.iter().flat_map(|server| server.disks.clone()).collect::<Vec<_>>());

        assert_eq!(added, 1, "the missing fourth topology member must be synthesized");
        assert!(report.is_complete(), "synthesized drives should complete the topology report");
        assert_eq!(servers.len(), 4, "v3 server list must preserve topology membership length");
        let synthesized = servers
            .iter()
            .find(|server| server.endpoint == "rustfs-4")
            .expect("missing member should be present");
        assert_eq!(synthesized.state, ITEM_UNKNOWN);
        assert_eq!(synthesized.disks.len(), 1);
        assert_eq!(synthesized.disks[0].endpoint, "http://rustfs-4:9000/data3");
        assert_eq!(synthesized.disks[0].disk_index, 3);
        assert_eq!(unknown.sum(), 1, "the synthesized drive must land in unknownDisks");
    }

    #[test]
    fn topology_reconcile_does_not_duplicate_existing_synthesized_rows() {
        let endpoints = topology_with_hosts(&["rustfs-1", "rustfs-2"]);
        let mut servers = vec![
            server_with_disk("rustfs-1", 0, "ok"),
            server_with_disk("rustfs-2", 1, ITEM_UNKNOWN),
        ];
        servers[1].state = ITEM_UNKNOWN.to_string();

        let (added, report) = reconcile_servers_with_endpoint_topology(&mut servers, &endpoints);

        assert_eq!(
            added, 0,
            "an existing unknown/degraded/offline row with topology drives already represents the member"
        );
        assert!(report.is_complete(), "existing synthesized rows should already complete the topology");
        assert_eq!(servers.len(), 2);
    }

    #[test]
    fn topology_reconcile_does_not_over_match_ambiguous_host_only_aliases() {
        let endpoints = EndpointServerPools::from(vec![PoolEndpoints {
            legacy: false,
            set_count: 1,
            drives_per_set: 2,
            endpoints: Endpoints::from(vec![
                topology_endpoint_url("http://rustfs-1:9000/data0", 0, 0, 0),
                topology_endpoint_url("http://rustfs-1:9001/data1", 0, 0, 1),
            ]),
            cmd_line: String::new(),
            platform: String::new(),
        }]);
        let mut servers = vec![server_with_disk("rustfs-1", 0, "ok")];

        let (added, report) = reconcile_servers_with_endpoint_topology(&mut servers, &endpoints);

        assert_eq!(added, 1, "host-only aliases are ambiguous when one host has multiple topology ports");
        assert!(report.is_complete());
        assert!(
            servers
                .iter()
                .any(|server| server.disks.iter().any(|disk| disk.endpoint == "http://rustfs-1:9001/data1")),
            "the second host:port member should be synthesized from exact topology"
        );
    }

    #[test]
    fn topology_report_detects_duplicate_drive_identity_with_balanced_total() {
        let endpoints = topology_with_hosts(&["rustfs-1", "rustfs-2"]);
        let servers = vec![server_with_disk("rustfs-1", 0, "ok"), server_with_disk("rustfs-1", 0, "ok")];

        let report = server_topology_completeness_report(&servers, &endpoints);

        assert_eq!(report.expected_drives, 2);
        assert_eq!(report.observed_drives, 2, "a plain total-count check would look balanced");
        assert_eq!(report.missing_drive_ids.len(), 1, "rustfs-2's drive identity is absent");
        assert_eq!(report.duplicate_drive_ids.len(), 1, "rustfs-1's drive identity is duplicated");
        assert!(!report.is_complete());
    }

    #[test]
    fn data_usage_errors_are_sanitized_in_server_info() {
        let mut buckets = rustfs_madmin::Buckets::default();
        let mut objects = rustfs_madmin::Objects::default();
        let mut versions = rustfs_madmin::Versions::default();
        let mut delete_markers = rustfs_madmin::DeleteMarkers::default();
        let mut usage = rustfs_madmin::Usage::default();

        apply_data_usage_result(
            Err(crate::error::Error::other("sensitive disk path")),
            &mut buckets,
            &mut objects,
            &mut versions,
            &mut delete_markers,
            &mut usage,
        );

        assert_eq!(buckets.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
        assert_eq!(objects.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
        assert_eq!(versions.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
        assert_eq!(delete_markers.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
        assert_eq!(usage.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
    }

    #[test]
    fn data_usage_counts_are_mapped_into_server_info() {
        let mut buckets = rustfs_madmin::Buckets::default();
        let mut objects = rustfs_madmin::Objects::default();
        let mut versions = rustfs_madmin::Versions::default();
        let mut delete_markers = rustfs_madmin::DeleteMarkers::default();
        let mut usage = rustfs_madmin::Usage::default();
        let info = rustfs_data_usage::DataUsageInfo {
            last_update: Some(std::time::SystemTime::UNIX_EPOCH),
            buckets_count: 2,
            objects_total_count: 3,
            versions_total_count: 4,
            delete_markers_total_count: 5,
            objects_total_size: 6,
            buckets_usage: [
                ("bucket-a".to_string(), rustfs_data_usage::BucketUsageInfo::default()),
                ("bucket-b".to_string(), rustfs_data_usage::BucketUsageInfo::default()),
            ]
            .into_iter()
            .collect(),
            usage_snapshot_complete: true,
            ..Default::default()
        };

        apply_data_usage_result(Ok(info), &mut buckets, &mut objects, &mut versions, &mut delete_markers, &mut usage);

        assert_eq!(buckets.count, 2);
        assert_eq!(objects.count, 3);
        assert_eq!(versions.count, 4);
        assert_eq!(delete_markers.count, 5);
        assert_eq!(usage.size, 6);
    }

    #[test]
    fn incomplete_data_usage_is_unavailable_in_server_info() {
        let mut buckets = rustfs_madmin::Buckets::default();
        let mut objects = rustfs_madmin::Objects::default();
        let mut versions = rustfs_madmin::Versions::default();
        let mut delete_markers = rustfs_madmin::DeleteMarkers::default();
        let mut usage = rustfs_madmin::Usage::default();
        let info = rustfs_data_usage::DataUsageInfo {
            buckets_count: 1,
            objects_total_count: 100,
            objects_total_size: 1024,
            ..Default::default()
        };

        apply_data_usage_result(Ok(info), &mut buckets, &mut objects, &mut versions, &mut delete_markers, &mut usage);

        assert_eq!(buckets.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
        assert_eq!(objects.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
        assert_eq!(versions.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
        assert_eq!(delete_markers.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
        assert_eq!(usage.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
    }

    #[test]
    fn live_bucket_namespace_count_survives_unavailable_data_usage() {
        let mut buckets = rustfs_madmin::Buckets {
            count: 0,
            error: Some(DATA_USAGE_UNAVAILABLE_ERROR.to_string()),
        };

        apply_bucket_namespace_count(
            Ok(crate::cluster::rpc::ScannerBucketListing {
                buckets: vec![
                    BucketInfo {
                        name: "bucket-a".to_string(),
                        ..Default::default()
                    },
                    BucketInfo {
                        name: ".rustfs.sys".to_string(),
                        ..Default::default()
                    },
                    BucketInfo {
                        name: "bucket-b".to_string(),
                        ..Default::default()
                    },
                ],
                set_buckets: Vec::new(),
                topology_complete: true,
            }),
            &mut buckets,
        );

        assert_eq!(buckets.count, 2);
        assert_eq!(buckets.error, None);
    }

    #[test]
    fn incomplete_bucket_namespace_lookup_preserves_usage_state() {
        let mut buckets = rustfs_madmin::Buckets {
            count: 7,
            error: Some(DATA_USAGE_UNAVAILABLE_ERROR.to_string()),
        };

        apply_bucket_namespace_count(
            Ok(crate::cluster::rpc::ScannerBucketListing {
                buckets: vec![BucketInfo {
                    name: "bucket-a".to_string(),
                    ..Default::default()
                }],
                set_buckets: Vec::new(),
                topology_complete: false,
            }),
            &mut buckets,
        );

        assert_eq!(buckets.count, 7);
        assert_eq!(buckets.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
    }

    #[test]
    fn failed_bucket_namespace_lookup_preserves_usage_state() {
        let mut buckets = rustfs_madmin::Buckets {
            count: 7,
            error: Some(DATA_USAGE_UNAVAILABLE_ERROR.to_string()),
        };

        apply_bucket_namespace_count(Err(crate::error::Error::DiskNotFound), &mut buckets);

        assert_eq!(buckets.count, 7);
        assert_eq!(buckets.error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
    }

    #[test]
    fn incomplete_erasure_set_cache_is_not_reported_as_zero() {
        let mut cache = rustfs_data_usage::DataUsageCache::default();
        cache.info.name = DATA_USAGE_ROOT.to_string();
        cache.replace(DATA_USAGE_ROOT, "", rustfs_data_usage::DataUsageEntry::default());
        let mut set = rustfs_madmin::ErasureSetInfo::default();

        apply_erasure_set_usage(&cache, &mut set);

        assert_eq!(set.usage_error.as_deref(), Some(DATA_USAGE_UNAVAILABLE_ERROR));
        assert_eq!(set.objects_count, 0);
        assert_eq!(set.usage, 0);
    }

    #[test]
    fn complete_erasure_set_cache_is_reported() {
        let mut cache = rustfs_data_usage::DataUsageCache::default();
        cache.info.name = DATA_USAGE_ROOT.to_string();
        cache.info.last_update = Some(std::time::SystemTime::UNIX_EPOCH);
        cache.info.snapshot_complete = true;
        cache.replace(
            DATA_USAGE_ROOT,
            "",
            rustfs_data_usage::DataUsageEntry {
                size: 512,
                objects: 3,
                versions: 4,
                delete_markers: 1,
                ..Default::default()
            },
        );
        let mut set = rustfs_madmin::ErasureSetInfo::default();

        apply_erasure_set_usage(&cache, &mut set);

        assert_eq!(set.objects_count, 3);
        assert_eq!(set.versions_count, 4);
        assert_eq!(set.delete_markers_count, 1);
        assert_eq!(set.usage, 512);
        assert!(set.usage_error.is_none());
    }

    #[serial]
    #[tokio::test]
    async fn server_info_includes_global_deployment_id() {
        let expected_deployment_id = runtime_sources::deployment_id();
        let info = get_server_info(false).await;

        assert_eq!(info.deployment_id, expected_deployment_id);
    }

    #[serial]
    #[tokio::test]
    async fn local_server_property_includes_runtime_stats_without_endpoint_pools() {
        let props = get_local_server_property().await;

        assert!(props.num_cpu > 0);
        assert!(props.max_procs > 0);
        assert!(
            props.mem_stats.alloc > 0 || props.mem_stats.total_alloc > 0 || props.mem_stats.heap_alloc > 0,
            "memory stats should not remain fixed placeholders"
        );
    }
}
