// Copyright 2023 Greptime 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.

mod basic;
mod candidate_select;
mod concurrent;
mod config;
mod err_handle;
mod full_list;
mod integration;
mod misc;

use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use common_catalog::consts::MITO_ENGINE;
use common_meta::datanode::{RegionManifestInfo, RegionStat};
use common_meta::key::table_repart::TableRepartValue;
use common_meta::key::table_route::PhysicalTableRouteValue;
use common_meta::peer::Peer;
use common_meta::rpc::ddl::EventContext;
use common_meta::rpc::router::{Region, RegionRoute};
use common_telemetry::debug;
use ordered_float::OrderedFloat;
use store_api::region_engine::RegionRole;
use store_api::storage::{FileRefsManifest, GcReport, RegionId};
use table::metadata::TableId;
use tokio::sync::mpsc::Sender;

use crate::error::Result;
use crate::gc::Region2Peers;
use crate::gc::candidate::GcCandidate;
use crate::gc::ctx::SchedulerCtx;
use crate::gc::options::GcSchedulerOptions;
use crate::gc::scheduler::{Event, GcScheduler};

pub const TEST_REGION_SIZE_200MB: u64 = 200_000_000;

/// Helper function to create an empty GcReport for the given region IDs
pub fn new_empty_report_with(region_ids: impl IntoIterator<Item = RegionId>) -> GcReport {
    let mut deleted_files = HashMap::new();
    let mut processed_regions = HashSet::new();
    for region_id in region_ids {
        deleted_files.insert(region_id, vec![]);
        processed_regions.insert(region_id);
    }
    GcReport {
        deleted_files,
        deleted_indexes: HashMap::new(),
        need_retry_regions: HashSet::new(),
        processed_regions,
    }
}

#[allow(clippy::type_complexity)]
#[derive(Debug, Default)]
pub struct MockSchedulerCtx {
    pub table_to_region_stats: Arc<Mutex<Option<HashMap<TableId, Vec<RegionStat>>>>>,
    pub table_reparts: Arc<Mutex<HashMap<TableId, TableRepartValue>>>,
    pub table_routes: Arc<Mutex<HashMap<TableId, (TableId, PhysicalTableRouteValue)>>>,
    pub file_refs: Arc<Mutex<Option<FileRefsManifest>>>,
    pub gc_reports: Arc<Mutex<HashMap<RegionId, GcReport>>>,
    pub candidates: Arc<Mutex<Option<HashMap<TableId, Vec<GcCandidate>>>>>,
    pub get_table_to_region_stats_calls: Arc<Mutex<usize>>,
    pub gc_regions_calls: Arc<Mutex<usize>>,
    pub gc_event_contexts: Arc<Mutex<Vec<EventContext>>>,
    // Error injection fields for testing
    pub get_table_to_region_stats_error: Arc<Mutex<Option<crate::error::Error>>>,
    pub get_table_route_error: Arc<Mutex<Option<crate::error::Error>>>,
    pub gc_regions_error: Arc<Mutex<Option<crate::error::Error>>>,
    // Retry testing fields
    pub gc_regions_retry_count: Arc<Mutex<HashMap<RegionId, usize>>>,
    pub gc_regions_error_sequence: Arc<Mutex<Vec<crate::error::Error>>>,
    pub gc_regions_success_after_retries: Arc<Mutex<HashMap<RegionId, usize>>>,
    // Per-region error injection
    pub gc_regions_per_region_errors: Arc<Mutex<HashMap<RegionId, crate::error::Error>>>,
}

impl MockSchedulerCtx {
    pub fn with_table_routes(
        self,
        table_routes: HashMap<TableId, (TableId, Vec<(RegionId, Peer)>)>,
    ) -> Self {
        *self.table_routes.lock().unwrap() = table_routes
            .into_iter()
            .map(|(k, (phy_id, region2peer))| {
                let phy = PhysicalTableRouteValue::new(
                    region2peer
                        .into_iter()
                        .map(|(region_id, peer)| RegionRoute {
                            region: Region::new_test(region_id),
                            leader_peer: Some(peer),
                            ..Default::default()
                        })
                        .collect(),
                );

                (k, (phy_id, phy))
            })
            .collect();
        self
    }

    #[allow(dead_code)]
    pub fn with_table_reparts(self, table_reparts: HashMap<TableId, TableRepartValue>) -> Self {
        *self.table_reparts.lock().unwrap() = table_reparts;
        self
    }

    /// Set an error to be returned by `get_table_to_region_stats`
    #[allow(dead_code)]
    pub fn with_get_table_to_region_stats_error(self, error: crate::error::Error) -> Self {
        *self.get_table_to_region_stats_error.lock().unwrap() = Some(error);
        self
    }

    /// Set an error to be returned by `get_table_route`
    pub fn set_table_route_error(&self, error: crate::error::Error) {
        *self.get_table_route_error.lock().unwrap() = Some(error);
    }

    /// Set an error to be returned by `gc_regions`
    pub fn with_gc_regions_error(self, error: crate::error::Error) -> Self {
        *self.gc_regions_error.lock().unwrap() = Some(error);
        self
    }

    /// Clear per-region errors
    #[allow(unused)]
    pub fn clear_gc_regions_per_region_errors(&self) {
        self.gc_regions_per_region_errors.lock().unwrap().clear();
    }
}

#[async_trait::async_trait]
impl SchedulerCtx for MockSchedulerCtx {
    async fn get_table_to_region_stats(&self) -> Result<HashMap<TableId, Vec<RegionStat>>> {
        *self.get_table_to_region_stats_calls.lock().unwrap() += 1;

        // Check if we should return an injected error
        if let Some(error) = self.get_table_to_region_stats_error.lock().unwrap().take() {
            return Err(error);
        }

        Ok(self
            .table_to_region_stats
            .lock()
            .unwrap()
            .clone()
            .unwrap_or_default())
    }

    async fn get_table_reparts(&self) -> Result<Vec<(TableId, TableRepartValue)>> {
        Ok(self
            .table_reparts
            .lock()
            .unwrap()
            .iter()
            .map(|(table_id, value)| (*table_id, value.clone()))
            .collect())
    }

    async fn get_table_route(
        &self,
        table_id: TableId,
    ) -> Result<(TableId, PhysicalTableRouteValue)> {
        // Check if we should return an injected error
        if let Some(error) = self.get_table_route_error.lock().unwrap().take() {
            return Err(error);
        }

        Ok(self
            .table_routes
            .lock()
            .unwrap()
            .get(&table_id)
            .cloned()
            .unwrap_or_else(|| (table_id, PhysicalTableRouteValue::default())))
    }

    async fn batch_get_table_route(
        &self,
        table_ids: &[TableId],
    ) -> Result<HashMap<TableId, PhysicalTableRouteValue>> {
        let mut result = HashMap::new();
        for &table_id in table_ids {
            let route = self
                .table_routes
                .lock()
                .unwrap()
                .get(&table_id)
                .cloned()
                .unwrap_or_else(|| (table_id, PhysicalTableRouteValue::default()));
            result.insert(table_id, route.1);
        }
        Ok(result)
    }

    async fn gc_regions(
        &self,
        region_ids: &[RegionId],
        _full_file_listing: bool,
        _timeout: Duration,
        _region_routes_override: Region2Peers,
        event_context: EventContext,
    ) -> Result<GcReport> {
        *self.gc_regions_calls.lock().unwrap() += 1;
        self.gc_event_contexts.lock().unwrap().push(event_context);

        // Check per-region error injection first (for any region)
        for &region_id in region_ids {
            if let Some(error) = self
                .gc_regions_per_region_errors
                .lock()
                .unwrap()
                .remove(&region_id)
            {
                *self
                    .gc_regions_retry_count
                    .lock()
                    .unwrap()
                    .entry(region_id)
                    .or_insert(0) += 1;
                return Err(error);
            }
        }

        // Check if we should return an injected error
        if let Some(error) = self.gc_regions_error.lock().unwrap().take() {
            for region_id in region_ids {
                *self
                    .gc_regions_retry_count
                    .lock()
                    .unwrap()
                    .entry(*region_id)
                    .or_insert(0) += 1;
            }
            return Err(error);
        }

        // Handle error sequence for retry testing
        {
            let mut error_sequence = self.gc_regions_error_sequence.lock().unwrap();
            if !error_sequence.is_empty() {
                let error = error_sequence.remove(0);
                for region_id in region_ids {
                    *self
                        .gc_regions_retry_count
                        .lock()
                        .unwrap()
                        .entry(*region_id)
                        .or_insert(0) += 1;
                }
                return Err(error);
            }
        }

        // Build the final report by processing each region individually
        let mut final_report = GcReport::default();
        let gc_reports = self.gc_reports.lock().unwrap();
        let success_after_retries = self.gc_regions_success_after_retries.lock().unwrap();

        for &region_id in region_ids {
            // Get current retry count for this region
            let retry_count = self
                .gc_regions_retry_count
                .lock()
                .unwrap()
                .get(&region_id)
                .copied()
                .unwrap_or(0);

            // Check if this region should succeed or need retry
            if let Some(&required_retries) = success_after_retries.get(&region_id) {
                if retry_count < required_retries {
                    debug!(
                        "Region {} needs retry (attempt {}/{})",
                        region_id,
                        retry_count + 1,
                        required_retries
                    );
                    // This region needs more retries - add to need_retry_regions
                    final_report.need_retry_regions.insert(region_id);
                    // Track the retry attempt
                    let mut retry_count_map = self.gc_regions_retry_count.lock().unwrap();
                    *retry_count_map.entry(region_id).or_insert(0) += 1;
                } else {
                    debug!(
                        "Region {} has completed retries - succeeding now",
                        region_id
                    );
                    // This region has completed all required retries - succeed
                    if let Some(report) = gc_reports.get(&region_id) {
                        final_report.merge(report.clone());
                    }
                    // Track the success attempt
                    let mut retry_count_map = self.gc_regions_retry_count.lock().unwrap();
                    *retry_count_map.entry(region_id).or_insert(0) += 1;
                }
            } else {
                // No retry requirement - check if we have a GC report for this region
                if let Some(report) = gc_reports.get(&region_id) {
                    // We have a GC report - succeed immediately
                    final_report.merge(report.clone());
                    // Track the success attempt
                    let mut retry_count_map = self.gc_regions_retry_count.lock().unwrap();
                    *retry_count_map.entry(region_id).or_insert(0) += 1;
                } else {
                    // No GC report available - this region should be marked for retry
                    final_report.need_retry_regions.insert(region_id);
                    // Track the attempt
                    let mut retry_count_map = self.gc_regions_retry_count.lock().unwrap();
                    *retry_count_map.entry(region_id).or_insert(0) += 1;
                }
            }
        }

        // Return the report with need_retry_regions populated - let the caller handle retry logic
        Ok(final_report)
    }

    #[cfg(feature = "enterprise")]
    async fn list_dropped_tables(&self) -> Result<Vec<common_meta::key::DroppedTableName>> {
        Ok(vec![])
    }

    #[cfg(feature = "enterprise")]
    async fn purge_dropped_table(&self, _table_id: TableId) -> Result<()> {
        Ok(())
    }

    #[cfg(feature = "enterprise")]
    fn try_reserve_purge(
        &self,
        table_id: TableId,
        max_in_flight: usize,
    ) -> Option<crate::gc::ctx::PurgeReservation> {
        crate::gc::ctx::PurgeReservation::try_new(
            Arc::new(std::sync::Mutex::new(HashSet::new())),
            table_id,
            max_in_flight,
        )
    }
}

pub struct TestEnv {
    pub scheduler: GcScheduler,
    pub ctx: Arc<MockSchedulerCtx>,
    #[allow(dead_code)]
    tx: Sender<Event>,
}

#[allow(unused)]
impl TestEnv {
    pub fn new() -> Self {
        let ctx = Arc::new(MockSchedulerCtx::default());
        let (tx, rx) = GcScheduler::channel();
        let config = GcSchedulerOptions::default();

        let scheduler = GcScheduler {
            ctx: ctx.clone(),
            runtime_switch_manager: crate::gc::scheduler::new_test_runtime_switch_manager(),
            receiver: rx,
            config,
            region_gc_tracker: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
            last_tracker_cleanup: Arc::new(tokio::sync::Mutex::new(Instant::now())),
        };

        Self { scheduler, ctx, tx }
    }

    pub fn with_candidates(self, candidates: HashMap<TableId, Vec<GcCandidate>>) -> Self {
        *self.ctx.candidates.lock().unwrap() = Some(candidates);
        self
    }

    #[allow(dead_code)]
    pub async fn run_scheduler(mut self) {
        self.scheduler.run().await;
    }

    #[allow(dead_code)]
    pub async fn tick(&self) {
        self.tx.send(Event::Tick).await.unwrap();
    }
}

/// Helper function to create a mock GC candidate that will pass the GC threshold
fn new_candidate(region_id: RegionId, score: f64) -> GcCandidate {
    // will pass threshold for gc
    let region_stat = mock_region_stat(region_id, RegionRole::Leader, 10_000, 10);

    GcCandidate {
        region_id,
        score: OrderedFloat(score),
        region_stat,
    }
}

/// Helper function to create a mock GC candidate
fn mock_candidate(region_id: RegionId) -> GcCandidate {
    let region_stat = mock_region_stat(region_id, RegionRole::Leader, TEST_REGION_SIZE_200MB, 10);
    GcCandidate {
        region_id,
        score: ordered_float::OrderedFloat(1.0),
        region_stat,
    }
}

/// Helper function to create a mock RegionStat
fn mock_region_stat(
    id: RegionId,
    role: RegionRole,
    approximate_bytes: u64,
    sst_num: u64,
) -> RegionStat {
    RegionStat {
        id,
        role,
        approximate_bytes,
        sst_num,
        region_manifest: RegionManifestInfo::Mito {
            manifest_version: 0,
            flushed_entry_id: 0,
            file_removed_cnt: 0,
        },
        rcus: 0,
        wcus: 0,
        engine: MITO_ENGINE.to_string(),
        num_rows: 0,
        memtable_size: 0,
        manifest_size: 0,
        sst_size: 0,
        index_size: 0,
        data_topic_latest_entry_id: 0,
        metadata_topic_latest_entry_id: 0,
        written_bytes: 0,
        query_cpu_time: 0,
        query_scanned_bytes: 0,
    }
}
