// 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.

//! `BucketOperations` for `SetDisks`.
//!
//! P4 of the SetDisks God-Object split (tracking backlog#815, issue #819).
//! Relocated verbatim from `set_disk/mod.rs`; the contract stays implemented
//! `for SetDisks`, so its associated-type bounds are unchanged and runtime
//! behavior is the same.

use super::super::{
    BUCKET_OP_IGNORED_ERRS, BucketInfo, BucketOperations, BucketOptions, DeleteBucketOptions, DiskError, Error, HashMap,
    MakeBucketOptions, Result, SetDisks, is_reserved_or_invalid_bucket, join_all, reduce_write_quorum_errs,
};
use crate::api::bucket::metadata_sys;
use crate::disk::DiskAPI;

impl SetDisks {
    pub(crate) async fn list_bucket_for_scanner(&self, _opts: &BucketOptions) -> Result<(Vec<BucketInfo>, bool)> {
        let disks = self.disk_inventory().await;
        let write_quorum = (disks.len() / 2) + 1;

        let mut futures = Vec::with_capacity(disks.len());
        for disk in disks {
            futures.push(async move {
                match disk {
                    Some(disk) => disk.list_volumes().await,
                    None => Err(DiskError::DiskNotFound),
                }
            });
        }

        let results = join_all(futures).await;
        let mut topology_complete = results.iter().all(|result| result.is_ok());
        let mut infos = Vec::with_capacity(results.len());
        let mut errs = Vec::with_capacity(results.len());
        for result in results {
            match result {
                Ok(volumes) => {
                    infos.push(Some(volumes));
                    errs.push(None);
                }
                Err(err) => {
                    infos.push(None);
                    errs.push(Some(err));
                }
            }
        }

        if let Some(err) = reduce_write_quorum_errs(&errs, BUCKET_OP_IGNORED_ERRS, write_quorum) {
            return Err(err.into());
        }

        let mut counts: HashMap<String, (usize, BucketInfo)> = HashMap::new();
        for volumes in infos.into_iter().flatten() {
            for volume in volumes {
                if is_reserved_or_invalid_bucket(&volume.name, false) {
                    continue;
                }

                let entry = counts.entry(volume.name.clone()).or_insert((
                    0,
                    BucketInfo {
                        name: volume.name.clone(),
                        created: volume.created,
                        ..Default::default()
                    },
                ));
                entry.0 += 1;
            }
        }

        topology_complete &= counts.values().all(|(count, _)| *count >= write_quorum);
        let mut buckets = counts
            .into_values()
            .filter_map(|(count, bucket)| (count >= write_quorum).then_some(bucket))
            .collect::<Vec<_>>();
        buckets.sort_by(|left, right| left.name.cmp(&right.name));
        Ok((buckets, topology_complete))
    }
}

#[async_trait::async_trait]
impl BucketOperations for SetDisks {
    type Error = Error;

    #[tracing::instrument(skip(self))]
    async fn make_bucket(&self, bucket: &str, opts: &MakeBucketOptions) -> Result<()> {
        let disks = self.disk_inventory().await;
        let write_quorum = (disks.len() / 2) + 1;
        let force_create = opts.force_create;

        let mut futures = Vec::with_capacity(disks.len());
        for disk in disks {
            let bucket = bucket.to_string();
            futures.push(async move {
                match disk {
                    Some(disk) => match disk.make_volume(&bucket).await {
                        Ok(()) => Ok(()),
                        Err(err) if force_create && matches!(err, DiskError::VolumeExists) => Ok(()),
                        Err(err) => Err(err),
                    },
                    None => Err(DiskError::DiskNotFound),
                }
            });
        }

        let results = join_all(futures).await;
        let errs = results
            .into_iter()
            .map(|result| result.err())
            .collect::<Vec<Option<DiskError>>>();

        if let Some(err) = reduce_write_quorum_errs(&errs, BUCKET_OP_IGNORED_ERRS, write_quorum) {
            return Err(err.into());
        }

        Ok(())
    }

    #[tracing::instrument(skip(self))]
    async fn get_bucket_info(&self, bucket: &str, _opts: &BucketOptions) -> Result<BucketInfo> {
        let disks = self.disk_inventory().await;
        let write_quorum = (disks.len() / 2) + 1;

        let mut futures = Vec::with_capacity(disks.len());
        for disk in disks {
            let bucket = bucket.to_string();
            futures.push(async move {
                match disk {
                    Some(disk) => disk.stat_volume(&bucket).await,
                    None => Err(DiskError::DiskNotFound),
                }
            });
        }

        let results = join_all(futures).await;
        let mut infos = Vec::with_capacity(results.len());
        let mut errs = Vec::with_capacity(results.len());
        for result in results {
            match result {
                Ok(info) => {
                    infos.push(Some(info));
                    errs.push(None);
                }
                Err(err) => {
                    infos.push(None);
                    errs.push(Some(err));
                }
            }
        }

        if let Some(err) = reduce_write_quorum_errs(&errs, BUCKET_OP_IGNORED_ERRS, write_quorum) {
            return Err(err.into());
        }

        let mut versioning = false;
        let mut object_locking = false;
        if let Ok(sys) = metadata_sys::get(bucket).await {
            versioning = sys.versioning();
            object_locking = sys.object_locking();
        }

        infos
            .into_iter()
            .flatten()
            .next()
            .map(|info| BucketInfo {
                name: info.name,
                created: info.created,
                versioning,
                object_locking,
                ..Default::default()
            })
            .ok_or(Error::VolumeNotFound)
    }

    #[tracing::instrument(skip(self))]
    async fn list_bucket(&self, opts: &BucketOptions) -> Result<Vec<BucketInfo>> {
        Ok(self.list_bucket_for_scanner(opts).await?.0)
    }

    #[tracing::instrument(skip(self))]
    async fn delete_bucket(&self, bucket: &str, opts: &DeleteBucketOptions) -> Result<()> {
        let disks = self.disk_inventory().await;
        let write_quorum = (disks.len() / 2) + 1;

        let mut futures = Vec::with_capacity(disks.len());
        for disk in disks.iter().cloned() {
            let bucket = bucket.to_string();
            let force = opts.force;
            futures.push(async move {
                match disk {
                    // Non-force refuses a non-empty bucket (VolumeNotEmpty); only
                    // an explicit force delete removes recursively (backlog#799 B1).
                    Some(disk) => disk.delete_volume(&bucket, force).await,
                    None => Err(DiskError::DiskNotFound),
                }
            });
        }

        let results = join_all(futures).await;
        let mut errs = Vec::with_capacity(results.len());
        let mut recreate = false;
        for result in results {
            match result {
                Ok(()) => errs.push(None),
                Err(err) => {
                    if matches!(err, DiskError::VolumeNotEmpty) {
                        recreate = true;
                    }
                    errs.push(Some(err));
                }
            }
        }

        if recreate {
            for (index, err) in errs.iter().enumerate() {
                if err.is_none()
                    && let Some(Some(disk)) = disks.get(index)
                {
                    let _ = disk.make_volume(bucket).await;
                }
            }
            return Err(Error::VolumeNotEmpty);
        }

        if let Some(err) = reduce_write_quorum_errs(&errs, BUCKET_OP_IGNORED_ERRS, write_quorum) {
            return Err(err.into());
        }

        Ok(())
    }
}
