
mod retry;
mod schedule;
mod status;
mod executable;

pub use retry::{Retry, RetryStrategy};
pub use schedule::JobSchedule;
pub use status::JobStatus;
pub use executable::Executable;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use uuid::Uuid;

use crate::{error::Error};

/// A background job with execution context and data
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "postgres", derive(sqlx::FromRow))]
pub struct Job {
    /// Unique identifier for this job
    #[serde(default)]
    pub id: Uuid,

    /// Type/category of the job
    pub job_type: String,

    /// Payload data associated with the job
    #[cfg_attr(feature = "postgres", sqlx(json))]
    pub payload: serde_json::Value,

    /// How and when this job should be scheduled
    #[cfg_attr(feature = "postgres", sqlx(json))]
    pub job_schedule: JobSchedule,

    /// Current execution status
    #[cfg_attr(feature = "postgres", sqlx(try_from = "i32"))]
    pub status: JobStatus,

    /// Retry configuration
    #[cfg_attr(feature = "postgres", sqlx(json))]
    pub retry: Retry,

    /// Current retry attempt number
    #[serde(default)]
    pub attempts: i32,

    /// Maximum number of retry attempts
    #[serde(default = "default_max_attempts")]
    pub max_attempts: i32,

    /// When the job should be executed
    pub scheduled_at: DateTime<Utc>,

    /// Time until which this job is locked (for processing)
    pub locked_until: Option<DateTime<Utc>>,

    /// Last error message if job failed
    pub error: Option<String>,

    /// Timestamp when the job was created
    pub created_at: DateTime<Utc>,

    /// Timestamp when the job was last updated
    pub updated_at: DateTime<Utc>,

    /// Timestamp when the job started running
    pub started_at: Option<DateTime<Utc>>,

    /// Timestamp when the job completed
    pub completed_at: Option<DateTime<Utc>>,
}

fn default_max_attempts() -> i32 {
    3
}

impl Job {
    pub fn new<M: Serialize>(job: M) -> Result<Job, Error> {
        let now = Utc::now();
        Ok(Self {
            id: Uuid::new_v4(),
            job_type: std::any::type_name::<M>().to_string(),
            payload: serde_json::to_value(job)?,
            job_schedule: JobSchedule::default(),
            status: JobStatus::default(),
            retry: Retry::default(),
            attempts: 0,
            max_attempts: default_max_attempts(),
            scheduled_at: now,
            locked_until: None,
            error: None,
            created_at: now,
            updated_at: now,
            started_at: None,
            completed_at: None,
        })
    }

    /// Get Job Id
    pub fn id(&self) -> &Uuid {
        &self.id
    }

    /// Custom job id
    pub fn set_id(mut self, id: Uuid) -> Self {
        self.id = id;
        self
    }
    /// Set Job Schedule
    pub fn set_job_schedule(mut self, job_schedule: JobSchedule) -> Self {
        // Update scheduled_at based on the schedule type
        if let Some(next_run) = job_schedule.next_run_time() {
            self.scheduled_at = next_run;
        }
        self.job_schedule = job_schedule;
        self
    }

    /// Set Job Schedule (builder pattern)
    pub fn job_schedule(self, job_schedule: JobSchedule) -> Self {
        self.set_job_schedule(job_schedule)
    }

    /// Schedule job to run at specific time
    ///
    /// # Example
    ///
    /// ```ignore
    /// job.schedule_at(current_time() + Duration::seconds(2));
    /// ```
    pub fn schedule_at(self, schedule_at: DateTime<Utc>) -> Self {
        self.job_schedule(JobSchedule::once_at(schedule_at))
    }

    /// Schedule job run by Cron Pattern
    ///
    /// # Example
    ///
    /// ```ignore
    /// job.cron("0 0 * * * *"); // Every hour
    /// ```
    pub fn cron(self, cron_expression: &str) -> Result<Self, Error> {
        let job_schedule = JobSchedule::from_cron(cron_expression)?;
        Ok(self.job_schedule(job_schedule))
    }

    /// Set Retry logic for job
    ///
    /// # Example
    ///
    /// ```ignore
    /// job.set_retry(Retry::default())
    /// ```
    pub fn set_retry(mut self, retry: Retry) -> Self {
        self.retry = retry;
        self
    }

    /// Set Retry logic for job (builder pattern)
    pub fn retry(self, retry: Retry) -> Self {
        self.set_retry(retry)
    }
}

impl Job {
    /// Check if this job should be retried after a failure
    pub fn should_retry(&self) -> bool {
        self.retry.is_enabled() && self.attempts < self.max_attempts
    }

    /// Calculate when this job should next be retried after a failure
    pub fn next_retry_at(&self) -> Option<DateTime<Utc>> {
        if self.retry.is_enabled() {
            Some(self.retry.next_retry_time(self.attempts))
        } else {
            None
        }
    }

    /// Get the next scheduled execution time
    pub fn next_execution(&self) -> Option<DateTime<Utc>> {
        self.job_schedule.next_run_time()
    }

    /// Get the next recurrence time after job completion
    pub fn next_recurrence(&self) -> Option<DateTime<Utc>> {
        self.job_schedule.next_recurrence()
    }

    /// Execute the job by deserializing the payload and running it
    pub async fn execute<M>(&mut self) -> Result<<M as Executable>::Output, Error>
    where
        M: Executable + for<'de> Deserialize<'de> + Serialize + Clone + Sync + Send + 'static,
    {
        // Deserialize the payload
        let mut payload: M = serde_json::from_value(self.payload.clone())?;

        payload.pre_execute().await;
        let output = payload.execute().await;
        let output = payload.post_execute(output).await;

        // Update the payload in case it was modified
        self.payload = serde_json::to_value(&payload)?;

        Ok(output)
    }
}

/// Trait for background job types
pub trait BackgroundJob: Executable + Clone + Serialize {
    /// The queue name for this job type
    fn queue_name() -> &'static str;

    /// Convert this job into a Job instance
    fn job(self) -> Result<Job, Error>;
}

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

    #[derive(Debug, Serialize, Deserialize, Clone)]
    struct TestPayload {
        value: String,
    }

    #[test]
    fn test_job_new_creates_valid_job() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };

        // Act
        let job = Job::new(payload).unwrap();

        // Assert
        assert_eq!(job.status, JobStatus::Queued);
        assert_eq!(job.attempts, 0);
        assert_eq!(job.max_attempts, 3);
        assert!(job.error.is_none());
        assert!(job.locked_until.is_none());
        assert!(job.started_at.is_none());
        assert!(job.completed_at.is_none());
    }

    #[test]
    fn test_job_set_id() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let custom_id = Uuid::new_v4();

        // Act
        let job = Job::new(payload).unwrap().set_id(custom_id);

        // Assert
        assert_eq!(job.id, custom_id);
    }

    #[test]
    fn test_job_schedule_at_updates_scheduled_time() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let future_time = Utc::now() + Duration::hours(5);

        // Act
        let job = Job::new(payload).unwrap().schedule_at(future_time);

        // Assert
        assert_eq!(job.scheduled_at, future_time);
        assert_eq!(job.job_schedule, JobSchedule::OnceAt(future_time));
    }

    #[test]
    fn test_job_cron_sets_schedule() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let cron_pattern = "0 0 * * * *";

        // Act
        let result = Job::new(payload).unwrap().cron(cron_pattern);

        // Assert
        assert!(result.is_ok());
        let job = result.unwrap();
        match job.job_schedule {
            JobSchedule::Cron { pattern } => assert_eq!(pattern, cron_pattern),
            _ => panic!("Expected Cron schedule"),
        }
    }

    #[test]
    fn test_job_set_retry() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let retry = Retry::fixed(Duration::seconds(30));

        // Act
        let job = Job::new(payload).unwrap().set_retry(retry.clone());

        // Assert
        assert!(job.retry.is_enabled());
    }

    #[test]
    fn test_should_retry_when_enabled_and_under_max_attempts() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let retry = Retry::fixed(Duration::seconds(10));
        let mut job = Job::new(payload).unwrap().set_retry(retry);
        job.attempts = 1;
        job.max_attempts = 3;

        // Act
        let should_retry = job.should_retry();

        // Assert
        assert!(should_retry);
    }

    #[test]
    fn test_should_not_retry_when_at_max_attempts() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let retry = Retry::fixed(Duration::seconds(10));
        let mut job = Job::new(payload).unwrap().set_retry(retry);
        job.attempts = 3;
        job.max_attempts = 3;

        // Act
        let should_retry = job.should_retry();

        // Assert
        assert!(!should_retry);
    }

    #[test]
    fn test_should_not_retry_when_retry_disabled() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let mut job = Job::new(payload).unwrap();
        job.attempts = 1;
        job.max_attempts = 3;

        // Act
        let should_retry = job.should_retry();

        // Assert
        assert!(!should_retry);
    }

    #[test]
    fn test_next_retry_at_returns_time_when_enabled() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let retry = Retry::fixed(Duration::seconds(30));
        let job = Job::new(payload).unwrap().set_retry(retry);

        // Act
        let next_retry = job.next_retry_at();

        // Assert
        assert!(next_retry.is_some());
    }

    #[test]
    fn test_next_retry_at_returns_none_when_disabled() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let job = Job::new(payload).unwrap();

        // Act
        let next_retry = job.next_retry_at();

        // Assert
        assert!(next_retry.is_none());
    }

    #[test]
    fn test_next_execution_for_immediate_schedule() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let job = Job::new(payload).unwrap();

        // Act
        let next_execution = job.next_execution();

        // Assert
        assert!(next_execution.is_some());
    }

    #[test]
    fn test_next_execution_for_cron_schedule() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };
        let job = Job::new(payload).unwrap().cron("0 0 * * * *").unwrap();

        // Act
        let next_execution = job.next_execution();

        // Assert
        assert!(next_execution.is_some());
    }

    #[test]
    fn test_default_max_attempts_is_three() {
        // Arrange
        let payload = TestPayload {
            value: "test".to_string(),
        };

        // Act
        let job = Job::new(payload).unwrap();

        // Assert
        assert_eq!(job.max_attempts, 3);
    }
}