//! Persisted background job queue and scheduler helpers.

use std::sync::Arc;
use std::time::Duration;

use serde_json::json;
use tauri::Runtime;

use crate::research::errors::Result;
use crate::research::repository::ResearchRepository;
use crate::research::types::{JobKind, JobStatus, PipelineJob};
use crate::research::util::{generate_id, now_rfc3339};

#[derive(Clone)]
pub struct ResearchPipeline {
    repository: Arc<ResearchRepository>,
}

impl ResearchPipeline {
    pub fn new(repository: Arc<ResearchRepository>) -> Self {
        Self { repository }
    }

    pub async fn enqueue_capture_jobs(
        &self,
        workspace_id: &str,
        note_id: &str,
        revision: u32,
        refresh_source_id: Option<String>,
    ) -> Result<Vec<PipelineJob>> {
        let mut jobs = vec![
            new_job(
                workspace_id,
                note_id,
                JobKind::EnrichNote,
                json!({ "noteId": note_id, "expectedRevision": revision }),
            ),
            new_job(
                workspace_id,
                note_id,
                JobKind::InferLinks,
                json!({ "workspaceId": workspace_id, "noteId": note_id, "expectedRevision": revision }),
            ),
            new_job(
                workspace_id,
                note_id,
                JobKind::EvaluateDuplicates,
                json!({ "workspaceId": workspace_id, "noteId": note_id, "expectedRevision": revision }),
            ),
            new_job(
                workspace_id,
                note_id,
                JobKind::EvaluateGhosts,
                json!({ "workspaceId": workspace_id }),
            ),
        ];
        if let Some(source_id) = refresh_source_id {
            jobs.push(new_job(
                workspace_id,
                &source_id,
                JobKind::RefreshSourceMetadata,
                json!({ "sourceId": source_id }),
            ));
        }

        self.repository.enqueue_jobs(jobs).await
    }

    pub async fn mark_completed(&self, mut job: PipelineJob) -> Result<PipelineJob> {
        job.status = JobStatus::Completed;
        job.last_error = None;
        job.next_attempt_at = None;
        job.updated_at = now_rfc3339();
        self.repository.save_job(job).await
    }

    pub async fn mark_skipped(&self, mut job: PipelineJob, reason: &str) -> Result<PipelineJob> {
        job.status = JobStatus::Skipped;
        job.last_error = Some(reason.to_string());
        job.next_attempt_at = None;
        job.updated_at = now_rfc3339();
        self.repository.save_job(job).await
    }

    pub async fn mark_failed(&self, mut job: PipelineJob, error: &str) -> Result<PipelineJob> {
        job.status = JobStatus::Failed;
        job.last_error = Some(error.to_string());
        job.next_attempt_at = Some(next_retry_timestamp(job.attempt_count));
        job.updated_at = now_rfc3339();
        self.repository.save_job(job).await
    }

    pub async fn claim_due_jobs(&self, limit: usize) -> Result<Vec<PipelineJob>> {
        self.repository.claim_due_jobs(limit).await
    }
}

pub fn spawn_research_scheduler<R: Runtime + 'static>(
    service: Arc<crate::research::service::ResearchService<R>>,
) {
    tauri::async_runtime::spawn(async move {
        loop {
            if let Err(e) = service.process_due_jobs().await {
                eprintln!("research scheduler tick failed: {e}");
            }
            tokio::time::sleep(Duration::from_secs(3)).await;
        }
    });
}

#[must_use]
fn new_job(
    workspace_id: &str,
    entity_id: &str,
    job_kind: JobKind,
    payload_json: serde_json::Value,
) -> PipelineJob {
    let now = now_rfc3339();
    PipelineJob {
        id: generate_id("job"),
        workspace_id: workspace_id.to_string(),
        entity_id: entity_id.to_string(),
        job_kind,
        status: JobStatus::Queued,
        attempt_count: 0,
        max_attempts: 3,
        next_attempt_at: None,
        last_error: None,
        payload_json,
        created_at: now.clone(),
        updated_at: now,
    }
}

fn next_retry_timestamp(attempt_number: u32) -> String {
    let seconds = match attempt_number {
        0 | 1 => 30,
        2 => 5 * 60,
        _ => 30 * 60,
    };
    (chrono::Utc::now() + chrono::Duration::seconds(i64::from(seconds))).to_rfc3339()
}
