use crate::metadata::MetadataStore;
use async_trait::async_trait;
use lightningstor_distributed::{RepairQueue, ReplicatedBackend, ReplicatedRepairTask};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::task::JoinHandle;
use tokio::time::sleep;
use tracing::{debug, warn};

const REPAIR_SCAN_LIMIT: u32 = 256;
const REPAIR_BACKOFF_BASE_MILLIS: u64 = 1_000;
const REPAIR_BACKOFF_MAX_MILLIS: u64 = 60_000;
const ORPHAN_REPAIR_DROP_ATTEMPTS: u32 = 8;

pub struct MetadataRepairQueue {
    metadata: Arc<MetadataStore>,
}

impl MetadataRepairQueue {
    pub fn new(metadata: Arc<MetadataStore>) -> Self {
        Self { metadata }
    }
}

#[async_trait]
impl RepairQueue for MetadataRepairQueue {
    async fn enqueue_repair(&self, task: ReplicatedRepairTask) {
        if let Err(error) = self.metadata.save_replicated_repair_task(&task).await {
            warn!(
                task_id = task.id,
                chunk_key = task.key,
                error = %error,
                "failed to persist replicated repair task"
            );
        }
    }
}

pub fn spawn_replicated_repair_worker(
    metadata: Arc<MetadataStore>,
    backend: Arc<ReplicatedBackend>,
    interval: Duration,
) -> JoinHandle<()> {
    tokio::spawn(async move {
        loop {
            if let Err(error) = process_replicated_repair_queue(&metadata, &backend).await {
                if replicated_repair_queue_transiently_unready(&error) {
                    debug!(error = %error, "replicated repair queue pass deferred until metadata becomes ready");
                } else {
                    warn!(error = %error, "replicated repair queue pass failed");
                }
            }
            sleep(interval).await;
        }
    })
}

async fn process_replicated_repair_queue(
    metadata: &MetadataStore,
    backend: &ReplicatedBackend,
) -> Result<(), lightningstor_types::Error> {
    let now = unix_time_millis();
    let tasks = metadata
        .list_replicated_repair_tasks(REPAIR_SCAN_LIMIT)
        .await?;
    for mut task in tasks {
        if !task.is_due(now) {
            continue;
        }
        match backend.repair_chunk(&task).await {
            Ok(()) => {
                metadata.delete_replicated_repair_task(&task.id).await?;
                debug!(
                    task_id = task.id,
                    chunk_key = task.key,
                    "repaired replicated chunk"
                );
            }
            Err(error) => {
                if task.attempt_count >= ORPHAN_REPAIR_DROP_ATTEMPTS {
                    match backend.chunk_exists_anywhere(&task.key).await {
                        Ok(false) => {
                            warn!(
                                task_id = task.id,
                                chunk_key = task.key,
                                attempts = task.attempt_count,
                                "dropping orphan replicated repair task with no remaining source replica"
                            );
                            metadata.delete_replicated_repair_task(&task.id).await?;
                            continue;
                        }
                        Ok(true) => {}
                        Err(probe_error) => {
                            warn!(
                                task_id = task.id,
                                chunk_key = task.key,
                                error = %probe_error,
                                "failed to probe global chunk existence while evaluating orphan repair task"
                            );
                        }
                    }
                }
                let backoff = repair_backoff_millis(task.attempt_count);
                task.schedule_retry(error.to_string(), backoff);
                metadata.save_replicated_repair_task(&task).await?;
                warn!(
                    task_id = task.id,
                    chunk_key = task.key,
                    attempts = task.attempt_count,
                    backoff_millis = backoff,
                    error = %error,
                    "replicated chunk repair failed"
                );
            }
        }
    }
    Ok(())
}

fn unix_time_millis() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64
}

fn repair_backoff_millis(attempt_count: u32) -> u64 {
    let exponent = attempt_count.min(6);
    let multiplier = 1u64 << exponent;
    (REPAIR_BACKOFF_BASE_MILLIS.saturating_mul(multiplier)).min(REPAIR_BACKOFF_MAX_MILLIS)
}

fn replicated_repair_queue_transiently_unready(error: &lightningstor_types::Error) -> bool {
    let rendered = error.to_string().to_ascii_lowercase();
    let transient = rendered.contains("region not found")
        || rendered.contains("status: notfound")
        || rendered.contains("metadata backend not ready")
        || rendered.contains("notleader");
    if transient {
        return true;
    }

    match error {
        lightningstor_types::Error::StorageError(message)
        | lightningstor_types::Error::Internal(message) => {
            let message = message.to_ascii_lowercase();
            message.contains("region not found")
                || message.contains("status: notfound")
                || message.contains("metadata backend not ready")
                || message.contains("notleader")
        }
        _ => false,
    }
}

#[cfg(test)]
mod tests {
    use super::replicated_repair_queue_transiently_unready;

    #[test]
    fn treats_region_not_found_as_transient_startup_state() {
        let error = lightningstor_types::Error::StorageError(
            "FlareDB scan failed: status: NotFound, message: \"region not found\"".to_string(),
        );
        assert!(replicated_repair_queue_transiently_unready(&error));
    }

    #[test]
    fn treats_wrapped_storage_error_rendering_as_transient_startup_state() {
        let error = lightningstor_types::Error::StorageError(
            "FlareDB scan failed: status: NotFound, message: \"region not found\", details: [], metadata: MetadataMap { headers: {} }".to_string(),
        );
        assert!(replicated_repair_queue_transiently_unready(&error));
    }

    #[test]
    fn keeps_real_repair_failures_as_warnings() {
        let error =
            lightningstor_types::Error::StorageError("replication checksum mismatch".to_string());
        assert!(!replicated_repair_queue_transiently_unready(&error));
    }
}
