use std::sync::Arc;

use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use chrono::Utc;
use ontoref_derive::onto_api;
use serde::{Deserialize, Serialize};
use tracing::{error, info, warn};
use vapora_shared::VaporaError;
use vapora_workflow_engine::{
    compute_next_fire_after_tz, compute_next_fire_at_tz, validate_cron_expression,
    validate_timezone, RunStatus, ScheduleRun, ScheduleStore, ScheduledWorkflow,
};

use crate::api::error::ApiError;
use crate::api::state::AppState;

// ─── Response types ──────────────────────────────────────────────────────────

#[derive(Debug, Serialize)]
pub struct ScheduleResponse {
    pub id: String,
    pub template_name: String,
    pub cron_expression: String,
    pub initial_context: serde_json::Value,
    pub enabled: bool,
    pub allow_concurrent: bool,
    pub catch_up: bool,
    pub timezone: Option<String>,
    pub last_fired_at: Option<String>,
    pub next_fire_at: Option<String>,
    pub runs_count: u64,
    pub created_at: String,
    pub updated_at: String,
}

impl From<ScheduledWorkflow> for ScheduleResponse {
    fn from(s: ScheduledWorkflow) -> Self {
        Self {
            id: s.id,
            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,
            last_fired_at: s.last_fired_at.map(|t| t.to_rfc3339()),
            next_fire_at: s.next_fire_at.map(|t| t.to_rfc3339()),
            runs_count: s.runs_count,
            created_at: s.created_at.to_rfc3339(),
            updated_at: s.updated_at.to_rfc3339(),
        }
    }
}

#[derive(Debug, Serialize)]
pub struct ScheduleRunResponse {
    pub id: String,
    pub schedule_id: String,
    pub workflow_instance_id: Option<String>,
    pub fired_at: String,
    pub status: String,
    pub notes: Option<String>,
}

impl From<ScheduleRun> for ScheduleRunResponse {
    fn from(r: ScheduleRun) -> Self {
        Self {
            id: r.id,
            schedule_id: r.schedule_id,
            workflow_instance_id: r.workflow_instance_id,
            fired_at: r.fired_at.to_rfc3339(),
            status: match r.status {
                RunStatus::Fired => "fired".to_string(),
                RunStatus::Skipped => "skipped".to_string(),
                RunStatus::Failed => "failed".to_string(),
            },
            notes: r.notes,
        }
    }
}

#[derive(Debug, Serialize)]
pub struct ScheduleListResponse {
    pub schedules: Vec<ScheduleResponse>,
    pub total: usize,
}

#[derive(Debug, Serialize)]
pub struct RunListResponse {
    pub runs: Vec<ScheduleRunResponse>,
    pub total: usize,
}

#[derive(Debug, Serialize)]
pub struct MessageResponse {
    pub success: bool,
    pub message: String,
}

// ─── Request types
// ────────────────────────────────────────────────────────────

/// Body for `PUT /api/v1/schedules/:id` — full replacement.
#[derive(Debug, Deserialize)]
pub struct PutScheduleRequest {
    pub template_name: String,
    pub cron_expression: String,
    #[serde(default)]
    pub initial_context: Option<serde_json::Value>,
    #[serde(default)]
    pub enabled: Option<bool>,
    #[serde(default)]
    pub allow_concurrent: bool,
    #[serde(default)]
    pub catch_up: bool,
    /// IANA timezone for cron evaluation (e.g. `"America/New_York"`). UTC when
    /// absent.
    #[serde(default)]
    pub timezone: Option<String>,
}

/// Body for `PATCH /api/v1/schedules/:id` — partial update.
#[derive(Debug, Deserialize)]
pub struct PatchScheduleRequest {
    pub enabled: Option<bool>,
    pub cron_expression: Option<String>,
    pub allow_concurrent: Option<bool>,
    pub catch_up: Option<bool>,
    pub initial_context: Option<serde_json::Value>,
    /// Update the timezone. Pass `null` explicitly to clear it (revert to UTC).
    #[serde(default)]
    pub timezone: Option<String>,
}

// ─── Helper
// ───────────────────────────────────────────────────────────────────

fn require_store(state: &AppState) -> Result<Arc<ScheduleStore>, ApiError> {
    state.schedule_store.clone().ok_or_else(|| {
        ApiError(VaporaError::InternalError(
            "Schedule store not available".to_string(),
        ))
    })
}

fn validate_cron(expr: &str) -> Result<(), ApiError> {
    validate_cron_expression(expr).map_err(|e| {
        ApiError(VaporaError::InvalidInput(format!(
            "Invalid cron expression '{}': {}",
            expr, e
        )))
    })
}

fn validate_tz(tz: &str) -> Result<(), ApiError> {
    validate_timezone(tz).map_err(|e| ApiError(VaporaError::InvalidInput(e)))
}

// ─── Handlers
// ─────────────────────────────────────────────────────────────────

/// `GET /api/v1/schedules` — list all schedules.
#[onto_api(
    method = "GET",
    path = "/api/v1/schedules",
    description = "Returns all scheduled workflows with their cron expressions and next fire \
                   times.",
    auth = "bearer",
    actors = "developer, agent, ci, admin",
    tags = "schedules, read"
)]
pub async fn list_schedules(
    State(state): State<AppState>,
) -> Result<Json<ScheduleListResponse>, ApiError> {
    let store = require_store(&state)?;
    let schedules = store.load_all().await.map_err(|e| {
        error!("list_schedules: {e}");
        ApiError(VaporaError::InternalError(e.to_string()))
    })?;
    let total = schedules.len();
    Ok(Json(ScheduleListResponse {
        total,
        schedules: schedules.into_iter().map(ScheduleResponse::from).collect(),
    }))
}

/// `GET /api/v1/schedules/:id` — get a single schedule.
#[onto_api(
    method = "GET",
    path = "/api/v1/schedules/:id",
    description = "Returns a single scheduled workflow by its identifier.",
    auth = "bearer",
    actors = "developer, agent, ci, admin",
    params = "id:string:required:Schedule identifier",
    tags = "schedules, read"
)]
pub async fn get_schedule(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<Json<ScheduleResponse>, ApiError> {
    let store = require_store(&state)?;
    let schedule = store
        .load_one(&id)
        .await
        .map_err(|e| {
            error!(schedule_id = %id, "get_schedule: {e}");
            ApiError(VaporaError::InternalError(e.to_string()))
        })?
        .ok_or_else(|| ApiError(VaporaError::NotFound(format!("Schedule {} not found", id))))?;

    Ok(Json(ScheduleResponse::from(schedule)))
}

/// `PUT /api/v1/schedules/:id` — create or fully replace a schedule.
///
/// Preserves `last_fired_at` and `runs_count` from the existing record.
#[onto_api(
    method = "PUT",
    path = "/api/v1/schedules/:id",
    description = "Creates or fully replaces a scheduled workflow, validating cron and timezone.",
    auth = "bearer",
    actors = "developer, admin",
    params = "id:string:required:Schedule identifier",
    tags = "schedules, write"
)]
pub async fn put_schedule(
    State(state): State<AppState>,
    Path(id): Path<String>,
    Json(req): Json<PutScheduleRequest>,
) -> Result<(StatusCode, Json<ScheduleResponse>), ApiError> {
    let store = require_store(&state)?;

    validate_cron(&req.cron_expression)?;
    if let Some(ref tz) = req.timezone {
        validate_tz(tz)?;
    }
    let tz = req.timezone.as_deref();
    let next_fire_at = compute_next_fire_at_tz(&req.cron_expression, tz);

    let now = Utc::now();
    let s = ScheduledWorkflow {
        id: id.clone(),
        template_name: req.template_name.clone(),
        cron_expression: req.cron_expression.clone(),
        initial_context: req
            .initial_context
            .unwrap_or(serde_json::Value::Object(Default::default())),
        enabled: req.enabled.unwrap_or(true),
        allow_concurrent: req.allow_concurrent,
        catch_up: req.catch_up,
        timezone: req.timezone.clone(),
        last_fired_at: None,
        next_fire_at,
        runs_count: 0,
        created_at: now,
        updated_at: now,
    };

    store.full_upsert(&s).await.map_err(|e| {
        error!(schedule_id = %id, "put_schedule: {e}");
        ApiError(VaporaError::InternalError(e.to_string()))
    })?;

    let updated = store
        .load_one(&id)
        .await
        .map_err(|e| ApiError(VaporaError::InternalError(e.to_string())))?
        .ok_or_else(|| {
            ApiError(VaporaError::InternalError(
                "Schedule vanished after upsert".into(),
            ))
        })?;

    info!(schedule_id = %id, template = %req.template_name, "Schedule PUT");
    Ok((StatusCode::OK, Json(ScheduleResponse::from(updated))))
}

/// `PATCH /api/v1/schedules/:id` — partial update.
///
/// Only the provided fields are changed. If `cron_expression` is updated,
/// `next_fire_at` is recomputed automatically.
#[onto_api(
    method = "PATCH",
    path = "/api/v1/schedules/:id",
    description = "Partially updates a schedule; recomputes next_fire_at if cron_expression \
                   changes.",
    auth = "bearer",
    actors = "developer, admin",
    params = "id:string:required:Schedule identifier",
    tags = "schedules, write"
)]
pub async fn patch_schedule(
    State(state): State<AppState>,
    Path(id): Path<String>,
    Json(req): Json<PatchScheduleRequest>,
) -> Result<Json<ScheduleResponse>, ApiError> {
    let store = require_store(&state)?;

    // Ensure the schedule exists first.
    store
        .load_one(&id)
        .await
        .map_err(|e| ApiError(VaporaError::InternalError(e.to_string())))?
        .ok_or_else(|| ApiError(VaporaError::NotFound(format!("Schedule {} not found", id))))?;

    let mut patch = serde_json::Map::new();

    if let Some(enabled) = req.enabled {
        patch.insert("enabled".into(), serde_json::json!(enabled));
    }
    if let Some(allow_concurrent) = req.allow_concurrent {
        patch.insert(
            "allow_concurrent".into(),
            serde_json::json!(allow_concurrent),
        );
    }
    if let Some(catch_up) = req.catch_up {
        patch.insert("catch_up".into(), serde_json::json!(catch_up));
    }
    if let Some(ctx) = req.initial_context {
        patch.insert("initial_context".into(), ctx);
    }
    if let Some(ref tz) = req.timezone {
        validate_tz(tz)?;
        patch.insert("timezone".into(), serde_json::json!(tz));
    }
    if let Some(ref cron) = req.cron_expression {
        validate_cron(cron)?;
        // Recompute next_fire_at using the new cron and whatever timezone is
        // already in the patch (or falls back to None → UTC).
        let tz = req.timezone.as_deref();
        let next_fire_at = compute_next_fire_at_tz(cron, tz);
        patch.insert("cron_expression".into(), serde_json::json!(cron));
        patch.insert("next_fire_at".into(), serde_json::json!(next_fire_at));
    }
    patch.insert("updated_at".into(), serde_json::json!(Utc::now()));

    let updated = store
        .patch(&id, serde_json::Value::Object(patch))
        .await
        .map_err(|e| {
            error!(schedule_id = %id, "patch_schedule: {e}");
            ApiError(VaporaError::InternalError(e.to_string()))
        })?
        .ok_or_else(|| {
            ApiError(VaporaError::NotFound(format!(
                "Schedule {} not found after patch",
                id
            )))
        })?;

    info!(schedule_id = %id, "Schedule PATCHed");
    Ok(Json(ScheduleResponse::from(updated)))
}

/// `DELETE /api/v1/schedules/:id` — permanently remove a schedule.
#[onto_api(
    method = "DELETE",
    path = "/api/v1/schedules/:id",
    description = "Permanently removes a scheduled workflow and its run history.",
    auth = "bearer",
    actors = "developer, admin",
    params = "id:string:required:Schedule identifier",
    tags = "schedules, write"
)]
pub async fn delete_schedule(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<(StatusCode, Json<MessageResponse>), ApiError> {
    let store = require_store(&state)?;

    // Verify existence before delete.
    store
        .load_one(&id)
        .await
        .map_err(|e| ApiError(VaporaError::InternalError(e.to_string())))?
        .ok_or_else(|| ApiError(VaporaError::NotFound(format!("Schedule {} not found", id))))?;

    store.delete(&id).await.map_err(|e| {
        error!(schedule_id = %id, "delete_schedule: {e}");
        ApiError(VaporaError::InternalError(e.to_string()))
    })?;

    info!(schedule_id = %id, "Schedule deleted");
    Ok((
        StatusCode::OK,
        Json(MessageResponse {
            success: true,
            message: format!("Schedule {} deleted", id),
        }),
    ))
}

/// `GET /api/v1/schedules/:id/runs` — execution history (last 100, desc).
#[onto_api(
    method = "GET",
    path = "/api/v1/schedules/:id/runs",
    description = "Returns the execution run history for a scheduled workflow (last 100, \
                   descending).",
    auth = "bearer",
    actors = "developer, agent, ci, admin",
    params = "id:string:required:Schedule identifier",
    tags = "schedules, read"
)]
pub async fn list_schedule_runs(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<Json<RunListResponse>, ApiError> {
    let store = require_store(&state)?;

    // Ensure schedule exists.
    store
        .load_one(&id)
        .await
        .map_err(|e| ApiError(VaporaError::InternalError(e.to_string())))?
        .ok_or_else(|| ApiError(VaporaError::NotFound(format!("Schedule {} not found", id))))?;

    let runs = store.load_runs(&id).await.map_err(|e| {
        error!(schedule_id = %id, "list_schedule_runs: {e}");
        ApiError(VaporaError::InternalError(e.to_string()))
    })?;
    let total = runs.len();
    Ok(Json(RunListResponse {
        total,
        runs: runs.into_iter().map(ScheduleRunResponse::from).collect(),
    }))
}

/// `POST /api/v1/schedules/:id/fire` — immediately trigger a scheduled
/// workflow bypassing the cron timer.
///
/// Records an auditable `ScheduleRun` with `status = Fired` and advances
/// `last_fired_at` / `next_fire_at` exactly like the background scheduler.
#[onto_api(
    method = "POST",
    path = "/api/v1/schedules/:id/fire",
    description = "Manually fires a scheduled workflow immediately, bypassing the cron timer.",
    auth = "bearer",
    actors = "developer, admin",
    params = "id:string:required:Schedule identifier",
    tags = "schedules, write"
)]
pub async fn fire_schedule(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<(StatusCode, Json<ScheduleRunResponse>), ApiError> {
    let store = require_store(&state)?;
    let orchestrator = state.workflow_orchestrator.as_ref().ok_or_else(|| {
        ApiError(VaporaError::InternalError(
            "Workflow orchestrator not available".to_string(),
        ))
    })?;

    let schedule = store
        .load_one(&id)
        .await
        .map_err(|e| ApiError(VaporaError::InternalError(e.to_string())))?
        .ok_or_else(|| ApiError(VaporaError::NotFound(format!("Schedule {} not found", id))))?;

    if !schedule.enabled {
        return Err(ApiError(VaporaError::InvalidInput(format!(
            "Schedule {} is disabled",
            id
        ))));
    }

    let now = Utc::now();

    let workflow_id = orchestrator
        .start_workflow(&schedule.template_name, schedule.initial_context.clone())
        .await
        .map_err(|e| {
            error!(schedule_id = %id, "fire_schedule start_workflow: {e}");
            ApiError(VaporaError::WorkflowError(e.to_string()))
        })?;

    let run = ScheduleRun {
        id: uuid::Uuid::new_v4().to_string(),
        schedule_id: id.clone(),
        workflow_instance_id: Some(workflow_id.clone()),
        fired_at: now,
        status: RunStatus::Fired,
        notes: Some("Manual fire via API".to_string()),
    };
    store.record_run(&run).await.map_err(|e| {
        warn!(schedule_id = %id, "fire_schedule record_run: {e}");
        ApiError(VaporaError::InternalError(e.to_string()))
    })?;

    let next_fire_at = compute_next_fire_after_tz(
        &schedule.cron_expression,
        &now,
        schedule.timezone.as_deref(),
    );

    store
        .update_after_fire(&id, now, next_fire_at)
        .await
        .map_err(|e| {
            warn!(schedule_id = %id, "fire_schedule update_after_fire: {e}");
            ApiError(VaporaError::InternalError(e.to_string()))
        })?;

    info!(
        schedule_id = %id,
        template    = %schedule.template_name,
        workflow_id = %workflow_id,
        "Schedule manually fired via API"
    );

    Ok((StatusCode::CREATED, Json(ScheduleRunResponse::from(run))))
}
