use std::sync::Arc;

use chrono::{DateTime, Utc};
use surrealdb::engine::remote::ws::Client;
use surrealdb::Surreal;
use tracing::debug;

use crate::error::{Result, WorkflowError};
use crate::schedule::{ScheduleRun, ScheduledWorkflow};

/// Persists `ScheduledWorkflow` and `ScheduleRun` records to SurrealDB.
///
/// Follows the same injection pattern as `SurrealWorkflowStore` — receives the
/// shared DB connection; does not create its own. Tables are defined by
/// migration 010.
pub struct ScheduleStore {
    db: Arc<Surreal<Client>>,
}

impl ScheduleStore {
    pub fn new(db: Arc<Surreal<Client>>) -> Self {
        Self { db }
    }

    /// Idempotent upsert for a TOML-seeded schedule.
    ///
    /// - First call: creates the record with full initial state (including
    ///   computed `next_fire_at`).
    /// - Subsequent calls: merges only static config fields (`cron_expression`,
    ///   `allow_concurrent`, `catch_up`, `initial_context`), leaving runtime
    ///   state (`last_fired_at`, `next_fire_at`, `runs_count`) intact.
    pub async fn upsert(&self, s: &ScheduledWorkflow) -> Result<()> {
        let existing: Option<serde_json::Value> = self
            .db
            .select(("scheduled_workflows", &*s.id))
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("check schedule {}: {e}", s.id)))?;

        if existing.is_none() {
            let json = serde_json::to_value(s).map_err(|e| {
                WorkflowError::DatabaseError(format!("serialize schedule {}: {e}", s.id))
            })?;
            let _: Option<serde_json::Value> = self
                .db
                .create(("scheduled_workflows", &*s.id))
                .content(json)
                .await
                .map_err(|e| {
                    WorkflowError::DatabaseError(format!("create schedule {}: {e}", s.id))
                })?;
            debug!(schedule_id = %s.id, template = %s.template_name, "Schedule created");
        } else {
            let _: Option<serde_json::Value> = self
                .db
                .update(("scheduled_workflows", &*s.id))
                .merge(serde_json::json!({
                    "cron_expression":  s.cron_expression,
                    "allow_concurrent": s.allow_concurrent,
                    "catch_up":         s.catch_up,
                    "initial_context":  s.initial_context,
                    "timezone":         s.timezone,
                    "updated_at":       Utc::now(),
                }))
                .await
                .map_err(|e| {
                    WorkflowError::DatabaseError(format!("merge schedule {}: {e}", s.id))
                })?;
            debug!(schedule_id = %s.id, template = %s.template_name, "Schedule config merged");
        }

        Ok(())
    }

    /// Load all enabled schedules. Filters in Rust to avoid complex SurrealQL
    /// against serde-tagged enums (same rationale as `SurrealWorkflowStore`).
    pub async fn load_enabled(&self) -> Result<Vec<ScheduledWorkflow>> {
        let mut response = self
            .db
            .query("SELECT * FROM scheduled_workflows")
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("load_enabled query: {e}")))?;

        let raw: Vec<serde_json::Value> = response
            .take(0)
            .map_err(|e| WorkflowError::DatabaseError(format!("load_enabled take: {e}")))?;

        let schedules: Vec<ScheduledWorkflow> = raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect();

        Ok(schedules.into_iter().filter(|s| s.enabled).collect())
    }

    /// Atomically advance runtime state after one or more fires.
    ///
    /// Uses `runs_count = runs_count + 1` in SurrealQL to avoid a read-modify-
    /// write race when multiple scheduler instances run (future distributed
    /// deployments).
    pub async fn update_after_fire(
        &self,
        id: &str,
        fired_at: DateTime<Utc>,
        next_fire_at: Option<DateTime<Utc>>,
    ) -> Result<()> {
        self.db
            .query(
                "UPDATE type::thing('scheduled_workflows', $id) SET last_fired_at = $fired_at, \
                 next_fire_at  = $next_fire_at, runs_count    = runs_count + 1, updated_at    = \
                 time::now()",
            )
            .bind(("id", id.to_string()))
            .bind(("fired_at", fired_at))
            .bind(("next_fire_at", next_fire_at))
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("update_after_fire {id}: {e}")))?;

        debug!(schedule_id = %id, "Schedule runtime state updated");
        Ok(())
    }

    /// Load a single schedule by its ID.
    pub async fn load_one(&self, id: &str) -> Result<Option<ScheduledWorkflow>> {
        let raw: Option<serde_json::Value> = self
            .db
            .select(("scheduled_workflows", id))
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("load_one schedule {id}: {e}")))?;

        raw.map(|v| {
            serde_json::from_value(v).map_err(|e| {
                WorkflowError::DatabaseError(format!("deserialize schedule {id}: {e}"))
            })
        })
        .transpose()
    }

    /// Load all schedules (enabled and disabled).
    pub async fn load_all(&self) -> Result<Vec<ScheduledWorkflow>> {
        let mut response = self
            .db
            .query("SELECT * FROM scheduled_workflows")
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("load_all query: {e}")))?;

        let raw: Vec<serde_json::Value> = response
            .take(0)
            .map_err(|e| WorkflowError::DatabaseError(format!("load_all take: {e}")))?;

        Ok(raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect())
    }

    /// Full-replace upsert for `PUT` semantics.
    ///
    /// Replaces all config fields and recomputes `next_fire_at`. Preserves
    /// `last_fired_at` and `runs_count` from the existing record so PUT
    /// doesn't erase operational history.
    pub async fn full_upsert(&self, s: &ScheduledWorkflow) -> Result<()> {
        let existing: Option<serde_json::Value> = self
            .db
            .select(("scheduled_workflows", &*s.id))
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("check schedule {}: {e}", s.id)))?;

        if existing.is_none() {
            let json = serde_json::to_value(s).map_err(|e| {
                WorkflowError::DatabaseError(format!("serialize schedule {}: {e}", s.id))
            })?;
            let _: Option<serde_json::Value> = self
                .db
                .create(("scheduled_workflows", &*s.id))
                .content(json)
                .await
                .map_err(|e| {
                    WorkflowError::DatabaseError(format!("create schedule {}: {e}", s.id))
                })?;
        } else {
            // Replace all config fields but preserve operational counters.
            let _: Option<serde_json::Value> = self
                .db
                .update(("scheduled_workflows", &*s.id))
                .merge(serde_json::json!({
                    "template_name":    s.template_name,
                    "cron_expression":  s.cron_expression,
                    "initial_context":  s.initial_context,
                    "enabled":          s.enabled,
                    "allow_concurrent": s.allow_concurrent,
                    "catch_up":         s.catch_up,
                    "timezone":         s.timezone,
                    "next_fire_at":     s.next_fire_at,
                    "updated_at":       Utc::now(),
                }))
                .await
                .map_err(|e| {
                    WorkflowError::DatabaseError(format!("full_upsert schedule {}: {e}", s.id))
                })?;
        }
        debug!(schedule_id = %s.id, "Schedule full-upserted");
        Ok(())
    }

    /// Partial update: only touches the fields provided (PATCH semantics).
    ///
    /// If `cron_expression` changes, the caller must compute and pass the new
    /// `next_fire_at`.
    pub async fn patch(
        &self,
        id: &str,
        patch: serde_json::Value,
    ) -> Result<Option<ScheduledWorkflow>> {
        let _: Option<serde_json::Value> = self
            .db
            .update(("scheduled_workflows", id))
            .merge(patch)
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("patch schedule {id}: {e}")))?;

        self.load_one(id).await
    }

    /// Delete a schedule definition permanently.
    pub async fn delete(&self, id: &str) -> Result<()> {
        let _: Option<serde_json::Value> = self
            .db
            .delete(("scheduled_workflows", id))
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("delete schedule {id}: {e}")))?;

        debug!(schedule_id = %id, "Schedule deleted");
        Ok(())
    }

    /// Load the run history for a specific schedule, ordered by fired_at desc.
    pub async fn load_runs(&self, schedule_id: &str) -> Result<Vec<ScheduleRun>> {
        let mut response = self
            .db
            .query(
                "SELECT * FROM schedule_runs WHERE schedule_id = $schedule_id ORDER BY fired_at \
                 DESC LIMIT 100",
            )
            .bind(("schedule_id", schedule_id.to_string()))
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("load_runs query: {e}")))?;

        let raw: Vec<serde_json::Value> = response
            .take(0)
            .map_err(|e| WorkflowError::DatabaseError(format!("load_runs take: {e}")))?;

        Ok(raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect())
    }

    /// Attempt to acquire an exclusive fire-lock for distributed deployments.
    ///
    /// Uses a conditional SurrealDB UPDATE: succeeds only when the record has
    /// no current lock holder, or the existing lock has expired (> 120 s old).
    /// Returns `true` if the lock was acquired by this call, `false` if another
    /// instance already holds it.
    ///
    /// The 120-second TTL means a crashed instance releases its lock
    /// automatically on the next scheduler tick after that window.
    pub async fn try_acquire_fire_lock(
        &self,
        id: &str,
        instance_id: &str,
        now: &chrono::DateTime<Utc>,
    ) -> Result<bool> {
        let expiry = *now - chrono::Duration::seconds(120);
        let mut resp = self
            .db
            .query(
                "UPDATE type::thing('scheduled_workflows', $id) SET locked_by = $instance_id, \
                 locked_at = $now WHERE locked_by IS NONE OR locked_at < $expiry",
            )
            .bind(("id", id.to_string()))
            .bind(("instance_id", instance_id.to_string()))
            .bind(("now", *now))
            .bind(("expiry", expiry))
            .await
            .map_err(|e| {
                WorkflowError::DatabaseError(format!("try_acquire_fire_lock {id}: {e}"))
            })?;

        let records: Vec<serde_json::Value> = resp.take(0).map_err(|e| {
            WorkflowError::DatabaseError(format!("try_acquire_fire_lock take {id}: {e}"))
        })?;

        Ok(!records.is_empty())
    }

    /// Release the fire-lock held by `instance_id`.
    ///
    /// The WHERE guard ensures an instance can only release its own lock,
    /// preventing a slow instance from accidentally clearing another's lock
    /// after its TTL expired and a second instance re-acquired it.
    pub async fn release_fire_lock(&self, id: &str, instance_id: &str) -> Result<()> {
        self.db
            .query(
                "UPDATE type::thing('scheduled_workflows', $id) SET locked_by = NONE, locked_at = \
                 NONE WHERE locked_by = $instance_id",
            )
            .bind(("id", id.to_string()))
            .bind(("instance_id", instance_id.to_string()))
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("release_fire_lock {id}: {e}")))?;

        debug!(schedule_id = %id, instance_id = %instance_id, "Fire lock released");
        Ok(())
    }

    /// Append a run record to the immutable audit log.
    pub async fn record_run(&self, run: &ScheduleRun) -> Result<()> {
        let json = serde_json::to_value(run)
            .map_err(|e| WorkflowError::DatabaseError(format!("serialize run {}: {e}", run.id)))?;

        let _: Option<serde_json::Value> = self
            .db
            .create(("schedule_runs", &*run.id))
            .content(json)
            .await
            .map_err(|e| WorkflowError::DatabaseError(format!("record_run {}: {e}", run.id)))?;

        debug!(
            run_id    = %run.id,
            schedule  = %run.schedule_id,
            status    = ?run.status,
            "Schedule run recorded"
        );
        Ok(())
    }
}

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

    #[test]
    fn test_schedule_serialization() {
        let now = Utc::now();
        let s = ScheduledWorkflow {
            id: "nightly_analysis".to_string(),
            template_name: "nightly_analysis".to_string(),
            cron_expression: "0 2 * * *".to_string(),
            initial_context: serde_json::json!({"env": "prod"}),
            enabled: true,
            allow_concurrent: false,
            catch_up: false,
            timezone: None,
            last_fired_at: Some(now),
            next_fire_at: Some(now + chrono::Duration::hours(24)),
            runs_count: 42,
            created_at: now,
            updated_at: now,
        };

        let json = serde_json::to_string(&s).expect("serialize");
        let decoded: ScheduledWorkflow = serde_json::from_str(&json).expect("deserialize");

        assert_eq!(decoded.id, s.id);
        assert_eq!(decoded.template_name, s.template_name);
        assert_eq!(decoded.cron_expression, s.cron_expression);
        assert_eq!(decoded.runs_count, 42);
        assert_eq!(decoded.allow_concurrent, false);
        assert_eq!(decoded.catch_up, false);

        let run = ScheduleRun {
            id: uuid::Uuid::new_v4().to_string(),
            schedule_id: s.id.clone(),
            workflow_instance_id: Some("wf-abc".to_string()),
            fired_at: now,
            status: RunStatus::Fired,
            notes: None,
        };
        let run_json = serde_json::to_string(&run).expect("serialize run");
        let decoded_run: ScheduleRun = serde_json::from_str(&run_json).expect("deserialize run");
        assert_eq!(decoded_run.status, RunStatus::Fired);
    }
}
