use chrono::{Duration, Utc};
use std::sync::Arc;
use uuid::Uuid;

use crate::{
    datastore::Datastore,
    error::{Error, JobResult},
    job::{Job, JobStatus},
};

/// Configuration for the job queue
#[derive(Debug, Clone)]
pub struct JobQueueConfig {
    /// How long a job remains locked after being claimed
    pub duration: Duration,
}

impl Default for JobQueueConfig {
    fn default() -> Self {
        Self {
            duration: Duration::minutes(5),
        }
    }
}

/// Job queue that manages job lifecycle
#[derive(Clone)]
pub struct JobQueue {
    datastore: Arc<dyn Datastore>,
    config: JobQueueConfig,
}

impl JobQueue {
    /// Create a new job queue
    pub fn new(datastore: Arc<dyn Datastore>, config: JobQueueConfig) -> Self {
        Self { datastore, config }
    }

    /// Create a new job queue with default configuration
    pub fn with_datastore(datastore: Arc<dyn Datastore>) -> Self {
        Self::new(datastore, JobQueueConfig::default())
    }

    /// Get the lock duration for jobs
    pub fn duration(&self) -> Duration {
        self.config.duration
    }

    /// Get a job by ID
    pub async fn get_job(&self, job_id: Uuid) -> JobResult<Option<Job>> {
        self.datastore.get_job(job_id).await
    }

    /// Enqueue a job for execution
    pub async fn enqueue(&self, job: Job) -> JobResult<Uuid> {
        self.datastore.insert_job(job).await
    }

    /// Claim the next available job for processing
    pub async fn claim_next_job(&self) -> JobResult<Option<Job>> {
        let locked_until = Utc::now() + self.config.duration;

        self.datastore.claim_next_job(locked_until).await
    }

    /// Handle job completion
    pub async fn complete_job(&self, mut job: Job) -> JobResult<()> {
        // Reschedule the job if it's recurring, otherwise delete it
        if let Some(next_execution) = job.next_recurrence() {
            job.status = JobStatus::Queued;
            job.scheduled_at = next_execution;
            job.locked_until = None;
            job.attempts = 0;
            job.error = None;
            job.completed_at = None;

            self.datastore.update_job(job).await
        } else {
            self.datastore.delete_job(*job.id()).await
        }
    }

    /// Handle a job failure
    pub async fn fail_job(&self, mut job: Job, error: &str) -> JobResult<()> {
        job.attempts = job.attempts + 1;
        job.error = Some(error.to_string());
        job.locked_until = None;

        if job.attempts >= job.max_attempts {
            job.status = JobStatus::Failed;
            job.completed_at = Some(Utc::now());

            self.datastore.update_job(job).await?;
        } else if let Some(next_run) = job.next_retry_at() {
            job.status = JobStatus::Queued;
            job.scheduled_at = next_run;
            job.locked_until = None;

            self.datastore.update_job(job).await?;
        }

        Ok(())
    }

    /// Cancel a pending or failed job
    pub async fn cancel_job(&self, mut job: Job) -> JobResult<()> {
        if !matches!(job.status, JobStatus::Queued | JobStatus::Failed) {
            return Err(Error::InvalidStateTransition);
        }

        job.status = JobStatus::Canceled;
        job.completed_at = Some(Utc::now());
        job.locked_until = None;

        self.datastore.update_job(job).await
    }

    /// Create a worker for this queue
    pub fn worker(&self) -> JobWorker {
        JobWorker::new(Arc::new(self.clone()))
    }

    /// Run the worker loop for this queue
    pub async fn start_worker(&self) -> JobResult<()> {
        self.worker().run().await
    }
}

mod worker;
pub use worker::JobWorker;

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

    #[test]
    fn test_job_queue_config_default() {
        // Arrange & Act
        let config = JobQueueConfig::default();

        // Assert
        assert_eq!(config.duration, Duration::minutes(5));
    }

    #[test]
    fn test_job_queue_config_custom() {
        // Arrange
        let custom_duration = Duration::minutes(10);

        // Act
        let config = JobQueueConfig {
            duration: custom_duration,
        };

        // Assert
        assert_eq!(config.duration, custom_duration);
    }
}