//! HTTP handlers for directive CRUD and DAG progression.

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::IntoResponse,
    Json,
};
use uuid::Uuid;

use crate::db::models::{
    CleanupTasksResponse, CreateDirectiveRequest,
    CreateDirectiveStepRequest, Directive, DirectiveListResponse,
    DirectiveStep, DirectiveWithSteps,
    UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest,
};
use crate::db::repository;
use crate::server::auth::Authenticated;
use crate::server::messages::ApiError;
use crate::server::state::SharedState;

// =============================================================================
// Directive CRUD
// =============================================================================

/// List all directives for the authenticated user.
#[utoipa::path(
    get,
    path = "/api/v1/directives",
    responses(
        (status = 200, description = "List of directives", body = DirectiveListResponse),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn list_directives(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::list_directives_for_owner(pool, auth.owner_id).await {
        Ok(directives) => {
            let total = directives.len() as i64;
            Json(DirectiveListResponse { directives, total }).into_response()
        }
        Err(e) => {
            tracing::error!("Failed to list directives: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("LIST_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

/// Create a new directive.
#[utoipa::path(
    post,
    path = "/api/v1/directives",
    request_body = CreateDirectiveRequest,
    responses(
        (status = 201, description = "Directive created", body = Directive),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn create_directive(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Json(req): Json<CreateDirectiveRequest>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::create_directive_for_owner(pool, auth.owner_id, req).await {
        Ok(directive) => (StatusCode::CREATED, Json(directive)).into_response(),
        Err(e) => {
            tracing::error!("Failed to create directive: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("CREATE_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

/// Get a directive with all its steps.
#[utoipa::path(
    get,
    path = "/api/v1/directives/{id}",
    params(("id" = Uuid, Path, description = "Directive ID")),
    responses(
        (status = 200, description = "Directive with steps", body = DirectiveWithSteps),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn get_directive(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::get_directive_with_steps_for_owner(pool, auth.owner_id, id).await {
        Ok(Some((directive, steps))) => {
            Json(DirectiveWithSteps { directive, steps }).into_response()
        }
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Directive not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to get directive: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("GET_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

/// Update a directive.
#[utoipa::path(
    put,
    path = "/api/v1/directives/{id}",
    params(("id" = Uuid, Path, description = "Directive ID")),
    request_body = UpdateDirectiveRequest,
    responses(
        (status = 200, description = "Directive updated", body = Directive),
        (status = 404, description = "Not found", body = ApiError),
        (status = 409, description = "Version conflict", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn update_directive(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
    Json(req): Json<UpdateDirectiveRequest>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::update_directive_for_owner(pool, auth.owner_id, id, req).await {
        Ok(Some(directive)) => Json(directive).into_response(),
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Directive not found")),
        )
            .into_response(),
        Err(repository::RepositoryError::VersionConflict { expected, actual }) => (
            StatusCode::CONFLICT,
            Json(ApiError::new(
                "VERSION_CONFLICT",
                &format!("Expected version {}, but current is {}", expected, actual),
            )),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to update directive: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

/// Delete a directive.
#[utoipa::path(
    delete,
    path = "/api/v1/directives/{id}",
    params(("id" = Uuid, Path, description = "Directive ID")),
    responses(
        (status = 204, description = "Deleted"),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn delete_directive(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::delete_directive_for_owner(pool, auth.owner_id, id).await {
        Ok(true) => StatusCode::NO_CONTENT.into_response(),
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Directive not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to delete directive: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DELETE_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

// =============================================================================
// Step CRUD
// =============================================================================

/// Create a step in a directive.
#[utoipa::path(
    post,
    path = "/api/v1/directives/{id}/steps",
    params(("id" = Uuid, Path, description = "Directive ID")),
    request_body = CreateDirectiveStepRequest,
    responses(
        (status = 201, description = "Step created", body = DirectiveStep),
        (status = 404, description = "Directive not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn create_step(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
    Json(req): Json<CreateDirectiveStepRequest>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    // Verify directive ownership
    match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Directive not found")),
            )
                .into_response();
        }
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("GET_FAILED", &e.to_string())),
            )
                .into_response();
        }
    }

    match repository::create_directive_step(pool, id, req).await {
        Ok(step) => (StatusCode::CREATED, Json(step)).into_response(),
        Err(e) => {
            tracing::error!("Failed to create step: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("CREATE_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

/// Batch create steps in a directive.
#[utoipa::path(
    post,
    path = "/api/v1/directives/{id}/steps/batch",
    params(("id" = Uuid, Path, description = "Directive ID")),
    request_body = Vec<CreateDirectiveStepRequest>,
    responses(
        (status = 201, description = "Steps created", body = Vec<DirectiveStep>),
        (status = 404, description = "Directive not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn batch_create_steps(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
    Json(steps): Json<Vec<CreateDirectiveStepRequest>>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    // Verify directive ownership
    match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Directive not found")),
            )
                .into_response();
        }
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("GET_FAILED", &e.to_string())),
            )
                .into_response();
        }
    }

    match repository::batch_create_directive_steps(pool, id, steps).await {
        Ok(created) => (StatusCode::CREATED, Json(created)).into_response(),
        Err(e) => {
            tracing::error!("Failed to batch create steps: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("CREATE_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

/// Update a step.
#[utoipa::path(
    put,
    path = "/api/v1/directives/{id}/steps/{step_id}",
    params(
        ("id" = Uuid, Path, description = "Directive ID"),
        ("step_id" = Uuid, Path, description = "Step ID"),
    ),
    request_body = UpdateDirectiveStepRequest,
    responses(
        (status = 200, description = "Step updated", body = DirectiveStep),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn update_step(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path((id, step_id)): Path<(Uuid, Uuid)>,
    Json(req): Json<UpdateDirectiveStepRequest>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    // Verify directive ownership
    match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Directive not found")),
            )
                .into_response();
        }
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("GET_FAILED", &e.to_string())),
            )
                .into_response();
        }
    }

    match repository::update_directive_step(pool, step_id, req).await {
        Ok(Some(step)) => Json(step).into_response(),
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Step not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to update step: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

/// Delete a step.
#[utoipa::path(
    delete,
    path = "/api/v1/directives/{id}/steps/{step_id}",
    params(
        ("id" = Uuid, Path, description = "Directive ID"),
        ("step_id" = Uuid, Path, description = "Step ID"),
    ),
    responses(
        (status = 204, description = "Deleted"),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn delete_step(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path((id, step_id)): Path<(Uuid, Uuid)>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    // Verify directive ownership
    match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Directive not found")),
            )
                .into_response();
        }
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("GET_FAILED", &e.to_string())),
            )
                .into_response();
        }
    }

    match repository::delete_directive_step(pool, step_id).await {
        Ok(true) => StatusCode::NO_CONTENT.into_response(),
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Step not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to delete step: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DELETE_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

// =============================================================================
// Directive Lifecycle Actions
// =============================================================================

/// Start a directive: sets status=active, advances ready steps.
#[utoipa::path(
    post,
    path = "/api/v1/directives/{id}/start",
    params(("id" = Uuid, Path, description = "Directive ID")),
    responses(
        (status = 200, description = "Directive started", body = DirectiveWithSteps),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn start_directive(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    // Set to active
    match repository::set_directive_status(pool, auth.owner_id, id, "active").await {
        Ok(Some(directive)) => {
            // Advance ready steps
            let _ = repository::advance_directive_ready_steps(pool, id).await;
            let steps = repository::list_directive_steps(pool, id).await.unwrap_or_default();
            Json(DirectiveWithSteps { directive, steps }).into_response()
        }
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Directive not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to start directive: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("START_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

/// Pause a directive.
#[utoipa::path(
    post,
    path = "/api/v1/directives/{id}/pause",
    params(("id" = Uuid, Path, description = "Directive ID")),
    responses(
        (status = 200, description = "Directive paused", body = Directive),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn pause_directive(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::set_directive_status(pool, auth.owner_id, id, "paused").await {
        Ok(Some(directive)) => Json(directive).into_response(),
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Directive not found")),
        )
            .into_response(),
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ApiError::new("PAUSE_FAILED", &e.to_string())),
        )
            .into_response(),
    }
}

/// Advance a directive: find newly-ready steps. If all steps done, set idle.
#[utoipa::path(
    post,
    path = "/api/v1/directives/{id}/advance",
    params(("id" = Uuid, Path, description = "Directive ID")),
    responses(
        (status = 200, description = "Advance result", body = DirectiveWithSteps),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn advance_directive(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    // Verify ownership
    let directive = match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
        Ok(Some(d)) => d,
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Directive not found")),
            )
                .into_response();
        }
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("GET_FAILED", &e.to_string())),
            )
                .into_response();
        }
    };

    // Advance ready steps
    let _ = repository::advance_directive_ready_steps(pool, id).await;

    // Check if idle
    let _ = repository::check_directive_idle(pool, id).await;

    // Return updated state
    let directive = match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
        Ok(Some(d)) => d,
        _ => directive,
    };
    let steps = repository::list_directive_steps(pool, id).await.unwrap_or_default();
    Json(DirectiveWithSteps { directive, steps }).into_response()
}

/// Mark a step as completed.
#[utoipa::path(
    post,
    path = "/api/v1/directives/{id}/steps/{step_id}/complete",
    params(
        ("id" = Uuid, Path, description = "Directive ID"),
        ("step_id" = Uuid, Path, description = "Step ID"),
    ),
    responses(
        (status = 200, description = "Step completed", body = DirectiveStep),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn complete_step(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path((id, step_id)): Path<(Uuid, Uuid)>,
) -> impl IntoResponse {
    step_status_change(state, auth, id, step_id, "completed").await
}

/// Mark a step as failed.
#[utoipa::path(
    post,
    path = "/api/v1/directives/{id}/steps/{step_id}/fail",
    params(
        ("id" = Uuid, Path, description = "Directive ID"),
        ("step_id" = Uuid, Path, description = "Step ID"),
    ),
    responses(
        (status = 200, description = "Step failed", body = DirectiveStep),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn fail_step(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path((id, step_id)): Path<(Uuid, Uuid)>,
) -> impl IntoResponse {
    step_status_change(state, auth, id, step_id, "failed").await
}

/// Mark a step as skipped.
#[utoipa::path(
    post,
    path = "/api/v1/directives/{id}/steps/{step_id}/skip",
    params(
        ("id" = Uuid, Path, description = "Directive ID"),
        ("step_id" = Uuid, Path, description = "Step ID"),
    ),
    responses(
        (status = 200, description = "Step skipped", body = DirectiveStep),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn skip_step(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path((id, step_id)): Path<(Uuid, Uuid)>,
) -> impl IntoResponse {
    step_status_change(state, auth, id, step_id, "skipped").await
}

/// Helper for step status changes.
async fn step_status_change(
    state: SharedState,
    auth: crate::server::auth::AuthenticatedUser,
    directive_id: Uuid,
    step_id: Uuid,
    new_status: &str,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    // Verify directive ownership
    match repository::get_directive_for_owner(pool, auth.owner_id, directive_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Directive not found")),
            )
                .into_response();
        }
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("GET_FAILED", &e.to_string())),
            )
                .into_response();
        }
    }

    let req = UpdateDirectiveStepRequest {
        status: Some(new_status.to_string()),
        ..Default::default()
    };

    match repository::update_directive_step(pool, step_id, req).await {
        Ok(Some(step)) => {
            // After step status change, advance the DAG
            let _ = repository::advance_directive_ready_steps(pool, directive_id).await;
            let _ = repository::check_directive_idle(pool, directive_id).await;
            Json(step).into_response()
        }
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Step not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to update step status: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

/// Update a directive's goal (triggers re-planning).
#[utoipa::path(
    put,
    path = "/api/v1/directives/{id}/goal",
    params(("id" = Uuid, Path, description = "Directive ID")),
    request_body = UpdateGoalRequest,
    responses(
        (status = 200, description = "Goal updated", body = Directive),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn update_goal(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
    Json(req): Json<UpdateGoalRequest>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::update_directive_goal(pool, auth.owner_id, id, &req.goal).await {
        Ok(Some(directive)) => {
            // Clear non-started steps so replanning starts fresh
            match repository::clear_pending_directive_steps(pool, id).await {
                Ok(count) => {
                    if count > 0 {
                        tracing::info!(
                            directive_id = %id,
                            removed_steps = count,
                            "Cleared pending steps after goal update — replanning will generate new steps"
                        );
                    }
                }
                Err(e) => {
                    tracing::warn!(
                        directive_id = %id,
                        error = %e,
                        "Failed to clear pending steps after goal update"
                    );
                }
            }
            Json(directive).into_response()
        }
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Directive not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to update goal: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}

// =============================================================================
// Task Cleanup
// =============================================================================


/// Clean up terminal tasks associated with a directive.
#[utoipa::path(
    post,
    path = "/api/v1/directives/{id}/cleanup-tasks",
    params(("id" = Uuid, Path, description = "Directive ID")),
    responses(
        (status = 200, description = "Tasks cleaned up", body = CleanupTasksResponse),
        (status = 404, description = "Not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
    ),
    security(("bearer_auth" = []), ("api_key" = [])),
    tag = "Directives"
)]
pub async fn cleanup_tasks(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    // Verify directive ownership
    match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Directive not found")),
            )
                .into_response();
        }
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("GET_FAILED", &e.to_string())),
            )
                .into_response();
        }
    }

    match repository::cleanup_directive_tasks(pool, auth.owner_id, id).await {
        Ok(deleted) => Json(CleanupTasksResponse { deleted }).into_response(),
        Err(e) => {
            tracing::error!("Failed to cleanup directive tasks: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("CLEANUP_FAILED", &e.to_string())),
            )
                .into_response()
        }
    }
}
