//! HTTP handlers for contract CRUD operations.

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

use crate::db::models::{
    AddLocalRepositoryRequest, AddRemoteRepositoryRequest, ChangePhaseRequest,
    ContractListResponse, ContractRepository, ContractSummary, ContractWithRelations,
    CreateContractRequest, CreateManagedRepositoryRequest, UpdateContractRequest,
    UpdateTaskRequest,
};
use crate::db::repository::{self, RepositoryError};
use crate::server::auth::Authenticated;
use crate::server::messages::ApiError;
use crate::server::state::SharedState;

/// Helper function to update the supervisor task with repository info when a primary repo is added.
/// This ensures the supervisor has access to the repository when it starts.
async fn update_supervisor_with_repo_if_needed(
    pool: &sqlx::PgPool,
    contract_id: uuid::Uuid,
    owner_id: uuid::Uuid,
    repo: &ContractRepository,
) {
    // Only update for primary repositories
    if !repo.is_primary {
        return;
    }

    // Get the supervisor task
    let supervisor = match repository::get_contract_supervisor_task(pool, contract_id).await {
        Ok(Some(s)) => s,
        Ok(None) => {
            tracing::debug!(contract_id = %contract_id, "No supervisor task found");
            return;
        }
        Err(e) => {
            tracing::warn!(contract_id = %contract_id, error = %e, "Failed to get supervisor task");
            return;
        }
    };

    // Only update if supervisor doesn't have a repository URL yet
    if supervisor.repository_url.is_some() {
        tracing::debug!(
            supervisor_id = %supervisor.id,
            "Supervisor already has repository URL"
        );
        return;
    }

    // Get repository URL (for remote repos) or local path (for local repos)
    let repo_url = repo.repository_url.clone().or_else(|| repo.local_path.clone());

    if repo_url.is_none() && repo.source_type != "managed" {
        tracing::debug!(
            supervisor_id = %supervisor.id,
            "Repository has no URL or path to assign"
        );
        return;
    }

    // Update supervisor task with repository info
    let update_req = UpdateTaskRequest {
        repository_url: repo_url,
        version: Some(supervisor.version),
        ..Default::default()
    };

    match repository::update_task_for_owner(pool, supervisor.id, owner_id, update_req).await {
        Ok(Some(updated)) => {
            tracing::info!(
                supervisor_id = %updated.id,
                repository_url = ?updated.repository_url,
                "Updated supervisor task with repository URL"
            );
        }
        Ok(None) => {
            tracing::warn!(supervisor_id = %supervisor.id, "Supervisor task not found during update");
        }
        Err(e) => {
            tracing::warn!(
                supervisor_id = %supervisor.id,
                error = %e,
                "Failed to update supervisor with repository URL"
            );
        }
    }
}

/// List all root contracts (no parent) for the authenticated user's owner.
#[utoipa::path(
    get,
    path = "/api/v1/contracts",
    responses(
        (status = 200, description = "List of root contracts", body = ContractListResponse),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn list_contracts(
    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_contracts_for_owner(pool, auth.owner_id).await {
        Ok(contracts) => {
            let total = contracts.len() as i64;
            Json(ContractListResponse { contracts, total }).into_response()
        }
        Err(e) => {
            tracing::error!("Failed to list contracts: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Get a contract by ID with all its relations (repositories, files, tasks).
#[utoipa::path(
    get,
    path = "/api/v1/contracts/{id}",
    params(
        ("id" = Uuid, Path, description = "Contract ID")
    ),
    responses(
        (status = 200, description = "Contract details with relations", body = ContractWithRelations),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn get_contract(
    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();
    };

    // Get the contract
    let contract = match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(c)) => c,
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    };

    // Get repositories
    let repositories = match repository::list_contract_repositories(pool, id).await {
        Ok(r) => r,
        Err(e) => {
            tracing::warn!("Failed to get repositories for {}: {}", id, e);
            Vec::new()
        }
    };

    // Get files
    let files = match repository::list_files_in_contract(pool, id, auth.owner_id).await {
        Ok(f) => f,
        Err(e) => {
            tracing::warn!("Failed to get files for contract {}: {}", id, e);
            Vec::new()
        }
    };

    // Get tasks
    let tasks = match repository::list_tasks_in_contract(pool, id, auth.owner_id).await {
        Ok(t) => t,
        Err(e) => {
            tracing::warn!("Failed to get tasks for contract {}: {}", id, e);
            Vec::new()
        }
    };

    Json(ContractWithRelations {
        contract,
        repositories,
        files,
        tasks,
    })
    .into_response()
}

/// Create a new contract.
#[utoipa::path(
    post,
    path = "/api/v1/contracts",
    request_body = CreateContractRequest,
    responses(
        (status = 201, description = "Contract created", body = ContractSummary),
        (status = 400, description = "Invalid request", body = ApiError),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn create_contract(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Json(req): Json<CreateContractRequest>,
) -> 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_contract_for_owner(pool, auth.owner_id, req.clone()).await {
        Ok(contract) => {
            // Create supervisor task for this contract
            let supervisor_name = format!("{} Supervisor", contract.name);
            let supervisor_plan = format!(
                "You are the supervisor for contract '{}'. Your goal is to drive this contract to completion.\n\n{}",
                contract.name,
                contract.description.as_deref().unwrap_or("No description provided.")
            );

            // Get repository info from contract if available
            let repo_url = {
                // Try to get the first repository associated with this contract
                match repository::list_contract_repositories(pool, contract.id).await {
                    Ok(repos) if !repos.is_empty() => {
                        let repo = &repos[0];
                        repo.repository_url.clone()
                    }
                    _ => None,
                }
            };

            let supervisor_req = crate::db::models::CreateTaskRequest {
                name: supervisor_name,
                description: None,
                plan: supervisor_plan,
                repository_url: repo_url,
                base_branch: None,
                target_branch: None,
                parent_task_id: None,
                contract_id: Some(contract.id),
                target_repo_path: None,
                completion_action: None,
                continue_from_task_id: None,
                copy_files: None,
                is_supervisor: true,
                checkpoint_sha: None,
                priority: 0,
                merge_mode: None,
                branched_from_task_id: None,
                conversation_history: None,
            };

            match repository::create_task_for_owner(pool, auth.owner_id, supervisor_req).await {
                Ok(supervisor_task) => {
                    tracing::info!(
                        contract_id = %contract.id,
                        supervisor_task_id = %supervisor_task.id,
                        is_supervisor = supervisor_task.is_supervisor,
                        "Created supervisor task for contract"
                    );

                    // Update contract with supervisor_task_id
                    let update_req = crate::db::models::UpdateContractRequest {
                        supervisor_task_id: Some(supervisor_task.id),
                        version: Some(contract.version),
                        ..Default::default()
                    };
                    if let Err(e) = repository::update_contract_for_owner(pool, contract.id, auth.owner_id, update_req).await {
                        tracing::warn!(
                            contract_id = %contract.id,
                            error = %e,
                            "Failed to link supervisor task to contract"
                        );
                    }
                }
                Err(e) => {
                    tracing::warn!(
                        contract_id = %contract.id,
                        error = %e,
                        "Failed to create supervisor task for contract"
                    );
                }
            }

            // Record history event for contract creation
            let _ = repository::record_history_event(
                pool,
                auth.owner_id,
                Some(contract.id),
                None,
                "contract",
                Some("created"),
                Some(&contract.phase),
                serde_json::json!({
                    "name": &contract.name,
                    "type": &contract.contract_type,
                    "description": &contract.description,
                }),
            ).await;

            // Get the summary version with counts
            match repository::get_contract_summary_for_owner(pool, contract.id, auth.owner_id).await
            {
                Ok(Some(summary)) => (StatusCode::CREATED, Json(summary)).into_response(),
                Ok(None) => {
                    // Shouldn't happen, but return basic info if it does
                    (
                        StatusCode::CREATED,
                        Json(ContractSummary {
                            id: contract.id,
                            name: contract.name,
                            description: contract.description,
                            contract_type: contract.contract_type,
                            phase: contract.phase,
                            status: contract.status,
                            supervisor_task_id: contract.supervisor_task_id,
                            local_only: contract.local_only,
                            file_count: 0,
                            task_count: 0,
                            repository_count: 0,
                            version: contract.version,
                            created_at: contract.created_at,
                        }),
                    )
                        .into_response()
                }
                Err(e) => {
                    tracing::warn!("Failed to get contract summary: {}", e);
                    (
                        StatusCode::CREATED,
                        Json(ContractSummary {
                            id: contract.id,
                            name: contract.name,
                            description: contract.description,
                            contract_type: contract.contract_type,
                            phase: contract.phase,
                            status: contract.status,
                            supervisor_task_id: contract.supervisor_task_id,
                            local_only: contract.local_only,
                            file_count: 0,
                            task_count: 0,
                            repository_count: 0,
                            version: contract.version,
                            created_at: contract.created_at,
                        }),
                    )
                        .into_response()
                }
            }
        }
        Err(e) => {
            tracing::error!("Failed to create contract: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Update a contract.
#[utoipa::path(
    put,
    path = "/api/v1/contracts/{id}",
    params(
        ("id" = Uuid, Path, description = "Contract ID")
    ),
    request_body = UpdateContractRequest,
    responses(
        (status = 200, description = "Contract updated", body = ContractSummary),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract not found", body = ApiError),
        (status = 409, description = "Version conflict", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn update_contract(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
    Json(req): Json<UpdateContractRequest>,
) -> 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_contract_for_owner(pool, id, auth.owner_id, req).await {
        Ok(Some(contract)) => {
            // If contract is completed, stop the supervisor task and clean up worktrees
            if contract.status == "completed" {
                if let Some(supervisor_task_id) = contract.supervisor_task_id {
                    // Get the supervisor task to find its daemon
                    if let Ok(Some(supervisor)) = repository::get_task_for_owner(pool, supervisor_task_id, auth.owner_id).await {
                        if let Some(daemon_id) = supervisor.daemon_id {
                            let state_clone = state.clone();
                            tokio::spawn(async move {
                                // Gracefully interrupt the supervisor
                                let cmd = crate::server::state::DaemonCommand::InterruptTask {
                                    task_id: supervisor_task_id,
                                    graceful: true,
                                };
                                if let Err(e) = state_clone.send_daemon_command(daemon_id, cmd).await {
                                    tracing::warn!(
                                        supervisor_task_id = %supervisor_task_id,
                                        daemon_id = %daemon_id,
                                        error = %e,
                                        "Failed to stop supervisor task on contract completion"
                                    );
                                } else {
                                    tracing::info!(
                                        supervisor_task_id = %supervisor_task_id,
                                        contract_id = %id,
                                        "Stopped supervisor task on contract completion"
                                    );
                                }
                            });
                        }
                    }
                }

                // Clean up all task worktrees for this contract
                let pool_clone = pool.clone();
                let state_clone = state.clone();
                let contract_id = id;
                tokio::spawn(async move {
                    cleanup_contract_worktrees(&pool_clone, &state_clone, contract_id).await;
                });

                // Record history event for contract completion
                let _ = repository::record_history_event(
                    pool,
                    auth.owner_id,
                    Some(contract.id),
                    None,
                    "contract",
                    Some("completed"),
                    Some(&contract.phase),
                    serde_json::json!({
                        "name": &contract.name,
                        "status": &contract.status,
                    }),
                ).await;
            }

            // Get summary with counts
            match repository::get_contract_summary_for_owner(pool, contract.id, auth.owner_id).await
            {
                Ok(Some(summary)) => Json(summary).into_response(),
                _ => Json(ContractSummary {
                    id: contract.id,
                    name: contract.name,
                    description: contract.description,
                    contract_type: contract.contract_type,
                    phase: contract.phase,
                    status: contract.status,
                    supervisor_task_id: contract.supervisor_task_id,
                    local_only: contract.local_only,
                    file_count: 0,
                    task_count: 0,
                    repository_count: 0,
                    version: contract.version,
                    created_at: contract.created_at,
                })
                .into_response(),
            }
        }
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Contract not found")),
        )
            .into_response(),
        Err(RepositoryError::VersionConflict { expected, actual }) => {
            tracing::info!(
                "Version conflict on contract {}: expected {}, actual {}",
                id,
                expected,
                actual
            );
            (
                StatusCode::CONFLICT,
                Json(serde_json::json!({
                    "code": "VERSION_CONFLICT",
                    "message": format!(
                        "Contract was modified. Expected version {}, actual version {}",
                        expected, actual
                    ),
                    "expectedVersion": expected,
                    "actualVersion": actual,
                })),
            )
                .into_response()
        }
        Err(RepositoryError::Database(e)) => {
            tracing::error!("Failed to update contract {}: {}", id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Delete a contract.
#[utoipa::path(
    delete,
    path = "/api/v1/contracts/{id}",
    params(
        ("id" = Uuid, Path, description = "Contract ID")
    ),
    responses(
        (status = 204, description = "Contract deleted"),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn delete_contract(
    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();
    };

    // First, verify contract exists and belongs to owner
    match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    }

    // Clean up any pending supervisor questions for this contract
    state.remove_pending_questions_for_contract(id);

    // Clean up all task worktrees BEFORE deleting the contract
    // (because CASCADE delete will remove tasks from DB)
    cleanup_contract_worktrees(pool, &state, id).await;

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

// =============================================================================
// Repository Management
// =============================================================================

/// Add a remote repository to a contract.
#[utoipa::path(
    post,
    path = "/api/v1/contracts/{id}/repositories/remote",
    params(
        ("id" = Uuid, Path, description = "Contract ID")
    ),
    request_body = AddRemoteRepositoryRequest,
    responses(
        (status = 201, description = "Repository added", body = ContractRepository),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn add_remote_repository(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
    Json(req): Json<AddRemoteRepositoryRequest>,
) -> 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 contract exists and belongs to owner
    match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    }

    match repository::add_remote_repository(pool, id, &req.name, &req.repository_url, req.is_primary)
        .await
    {
        Ok(repo) => {
            // Update supervisor task with repository info if this is a primary repo
            update_supervisor_with_repo_if_needed(pool, id, auth.owner_id, &repo).await;

            // Track repository in history for future suggestions
            if let Err(e) = repository::add_or_update_repository_history(
                pool,
                auth.owner_id,
                &req.name,
                Some(&req.repository_url),
                None,
                "remote",
            )
            .await
            {
                // Log but don't fail the request if history tracking fails
                tracing::warn!("Failed to track repository in history: {}", e);
            }

            (StatusCode::CREATED, Json(repo)).into_response()
        }
        Err(e) => {
            tracing::error!("Failed to add remote repository to contract {}: {}", id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Add a local repository to a contract.
#[utoipa::path(
    post,
    path = "/api/v1/contracts/{id}/repositories/local",
    params(
        ("id" = Uuid, Path, description = "Contract ID")
    ),
    request_body = AddLocalRepositoryRequest,
    responses(
        (status = 201, description = "Repository added", body = ContractRepository),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn add_local_repository(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
    Json(req): Json<AddLocalRepositoryRequest>,
) -> 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 contract exists and belongs to owner
    match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    }

    match repository::add_local_repository(pool, id, &req.name, &req.local_path, req.is_primary)
        .await
    {
        Ok(repo) => {
            // Update supervisor task with repository info if this is a primary repo
            update_supervisor_with_repo_if_needed(pool, id, auth.owner_id, &repo).await;

            // Track repository in history for future suggestions
            if let Err(e) = repository::add_or_update_repository_history(
                pool,
                auth.owner_id,
                &req.name,
                None,
                Some(&req.local_path),
                "local",
            )
            .await
            {
                // Log but don't fail the request if history tracking fails
                tracing::warn!("Failed to track repository in history: {}", e);
            }

            (StatusCode::CREATED, Json(repo)).into_response()
        }
        Err(e) => {
            tracing::error!("Failed to add local repository to contract {}: {}", id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Create a managed repository (daemon will create it).
#[utoipa::path(
    post,
    path = "/api/v1/contracts/{id}/repositories/managed",
    params(
        ("id" = Uuid, Path, description = "Contract ID")
    ),
    request_body = CreateManagedRepositoryRequest,
    responses(
        (status = 201, description = "Repository creation requested", body = ContractRepository),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn create_managed_repository(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
    Json(req): Json<CreateManagedRepositoryRequest>,
) -> 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 contract exists and belongs to owner
    match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    }

    match repository::create_managed_repository(pool, id, &req.name, req.is_primary).await {
        Ok(repo) => {
            // For managed repos, the daemon will create the repo and we'll update later
            // For now, just mark that this is a managed repo configuration
            // The helper handles the case where repo has no URL yet
            update_supervisor_with_repo_if_needed(pool, id, auth.owner_id, &repo).await;
            (StatusCode::CREATED, Json(repo)).into_response()
        }
        Err(e) => {
            tracing::error!(
                "Failed to create managed repository for contract {}: {}",
                id,
                e
            );
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Delete a repository from a contract.
#[utoipa::path(
    delete,
    path = "/api/v1/contracts/{id}/repositories/{repo_id}",
    params(
        ("id" = Uuid, Path, description = "Contract ID"),
        ("repo_id" = Uuid, Path, description = "Repository ID")
    ),
    responses(
        (status = 204, description = "Repository removed"),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract or repository not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn delete_repository(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path((id, repo_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 contract exists and belongs to owner
    match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    }

    match repository::delete_contract_repository(pool, repo_id, id).await {
        Ok(true) => StatusCode::NO_CONTENT.into_response(),
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Repository not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!(
                "Failed to delete repository {} from contract {}: {}",
                repo_id,
                id,
                e
            );
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Set a repository as primary for a contract.
#[utoipa::path(
    put,
    path = "/api/v1/contracts/{id}/repositories/{repo_id}/primary",
    params(
        ("id" = Uuid, Path, description = "Contract ID"),
        ("repo_id" = Uuid, Path, description = "Repository ID")
    ),
    responses(
        (status = 204, description = "Repository set as primary"),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract or repository not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn set_repository_primary(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path((id, repo_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 contract exists and belongs to owner
    match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    }

    match repository::set_repository_primary(pool, repo_id, id).await {
        Ok(true) => StatusCode::NO_CONTENT.into_response(),
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Repository not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!(
                "Failed to set repository {} as primary for contract {}: {}",
                repo_id,
                id,
                e
            );
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

// =============================================================================
// Task Association
// =============================================================================

/// Add a task to a contract.
#[utoipa::path(
    post,
    path = "/api/v1/contracts/{id}/tasks/{task_id}",
    params(
        ("id" = Uuid, Path, description = "Contract ID"),
        ("task_id" = Uuid, Path, description = "Task ID")
    ),
    responses(
        (status = 204, description = "Task added to contract"),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract or task not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn add_task_to_contract(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path((id, task_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 contract exists and belongs to owner
    match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    }

    // Verify task exists and belongs to owner
    match repository::get_task_for_owner(pool, task_id, auth.owner_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Task not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get task {}: {}", task_id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    }

    match repository::add_task_to_contract(pool, id, task_id, auth.owner_id).await {
        Ok(true) => StatusCode::NO_CONTENT.into_response(),
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Task not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to add task {} to contract {}: {}", task_id, id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Remove a task from a contract.
#[utoipa::path(
    delete,
    path = "/api/v1/contracts/{id}/tasks/{task_id}",
    params(
        ("id" = Uuid, Path, description = "Contract ID"),
        ("task_id" = Uuid, Path, description = "Task ID")
    ),
    responses(
        (status = 204, description = "Task removed from contract"),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract or task not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn remove_task_from_contract(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path((id, task_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 contract exists and belongs to owner
    match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    }

    match repository::remove_task_from_contract(pool, id, task_id, auth.owner_id).await {
        Ok(true) => StatusCode::NO_CONTENT.into_response(),
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Task not found in this contract")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!(
                "Failed to remove task {} from contract {}: {}",
                task_id,
                id,
                e
            );
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

// =============================================================================
// Phase Management
// =============================================================================

/// Change contract phase.
#[utoipa::path(
    post,
    path = "/api/v1/contracts/{id}/phase",
    params(
        ("id" = Uuid, Path, description = "Contract ID")
    ),
    request_body = ChangePhaseRequest,
    responses(
        (status = 200, description = "Phase changed", body = ContractSummary),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn change_phase(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
    Json(req): Json<ChangePhaseRequest>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    // First, get the contract to check phase_guard
    let contract = match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(c)) => c,
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    };

    // If phase_guard is enabled and not confirmed, return phase deliverables for review
    if contract.phase_guard && !req.confirmed.unwrap_or(false) {
        // If user provided feedback, return it
        if let Some(ref feedback) = req.feedback {
            return Json(serde_json::json!({
                "status": "changes_requested",
                "currentPhase": contract.phase,
                "requestedPhase": req.phase,
                "feedback": feedback,
                "message": "Feedback has been noted. Address the changes and try again."
            }))
            .into_response();
        }

        // Get files created in this phase
        let phase_files = match repository::list_files_in_contract(pool, id, auth.owner_id).await {
            Ok(files) => files
                .into_iter()
                .filter(|f| f.contract_phase.as_deref() == Some(&contract.phase))
                .map(|f| serde_json::json!({
                    "id": f.id,
                    "name": f.name,
                    "description": f.description
                }))
                .collect::<Vec<_>>(),
            Err(_) => Vec::new(),
        };

        // Get tasks completed in this contract
        let phase_tasks = match repository::list_tasks_in_contract(pool, id, auth.owner_id).await {
            Ok(tasks) => tasks
                .into_iter()
                .filter(|t| t.status == "done" || t.status == "completed")
                .map(|t| serde_json::json!({
                    "id": t.id,
                    "name": t.name,
                    "status": t.status
                }))
                .collect::<Vec<_>>(),
            Err(_) => Vec::new(),
        };

        let deliverables_summary = format!(
            "Phase '{}' deliverables: {} files created, {} tasks completed.",
            contract.phase,
            phase_files.len(),
            phase_tasks.len()
        );

        let transition_id = uuid::Uuid::new_v4().to_string();

        return Json(serde_json::json!({
            "status": "pending_confirmation",
            "transitionId": transition_id,
            "currentPhase": contract.phase,
            "nextPhase": req.phase,
            "deliverablesSummary": deliverables_summary,
            "phaseFiles": phase_files,
            "phaseTasks": phase_tasks,
            "requiresConfirmation": true,
            "message": "Phase transition requires confirmation. Set confirmed=true in the request to proceed."
        }))
        .into_response();
    }

    // Phase guard is disabled or user confirmed - proceed with phase change
    match repository::change_contract_phase_for_owner(pool, id, auth.owner_id, &req.phase).await {
        Ok(Some(updated_contract)) => {
            // Notify supervisor of phase change
            if let Some(supervisor_task_id) = updated_contract.supervisor_task_id {
                if let Ok(Some(supervisor)) = repository::get_task_for_owner(pool, supervisor_task_id, auth.owner_id).await {
                    let state_clone = state.clone();
                    let contract_id = updated_contract.id;
                    let new_phase = updated_contract.phase.clone();
                    tokio::spawn(async move {
                        state_clone.notify_supervisor_of_phase_change(
                            supervisor.id,
                            supervisor.daemon_id,
                            contract_id,
                            &new_phase,
                        ).await;
                    });
                }
            }

            // Record history event for phase change
            let _ = repository::record_history_event(
                pool,
                auth.owner_id,
                Some(contract.id),
                None,
                "phase",
                Some("changed"),
                Some(&contract.phase),
                serde_json::json!({
                    "contractName": &contract.name,
                    "newPhase": &contract.phase,
                }),
            ).await;

            // Get summary with counts
            match repository::get_contract_summary_for_owner(pool, updated_contract.id, auth.owner_id).await
            {
                Ok(Some(summary)) => Json(summary).into_response(),
                _ => Json(ContractSummary {
                    id: updated_contract.id,
                    name: updated_contract.name,
                    description: updated_contract.description,
                    contract_type: updated_contract.contract_type,
                    phase: updated_contract.phase,
                    status: updated_contract.status,
                    supervisor_task_id: updated_contract.supervisor_task_id,
                    local_only: updated_contract.local_only,
                    file_count: 0,
                    task_count: 0,
                    repository_count: 0,
                    version: updated_contract.version,
                    created_at: updated_contract.created_at,
                })
                .into_response(),
            }
        }
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Contract not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to change phase for contract {}: {}", id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

// =============================================================================
// Deliverables
// =============================================================================

/// Request body for marking a deliverable complete
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MarkDeliverableRequest {
    /// The deliverable ID to mark as complete (e.g., 'plan-document', 'pull-request')
    pub deliverable_id: String,
    /// Phase the deliverable belongs to. Defaults to current contract phase if not specified.
    pub phase: Option<String>,
}

/// Mark a deliverable as complete for a contract phase.
#[utoipa::path(
    post,
    path = "/api/v1/contracts/{id}/deliverables/complete",
    params(
        ("id" = Uuid, Path, description = "Contract ID")
    ),
    request_body = MarkDeliverableRequest,
    responses(
        (status = 200, description = "Deliverable marked complete", body = serde_json::Value),
        (status = 400, description = "Invalid deliverable ID", body = ApiError),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn mark_deliverable_complete(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
    Json(req): Json<MarkDeliverableRequest>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    // Get contract
    let contract = match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(c)) => c,
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    };

    // Use specified phase or default to current contract phase
    let target_phase = req.phase.unwrap_or_else(|| contract.phase.clone());

    // Validate the deliverable ID exists for this phase/contract type
    let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&target_phase, &contract.contract_type);
    let deliverable = phase_deliverables.deliverables.iter().find(|d| d.id == req.deliverable_id);

    if deliverable.is_none() {
        let valid_ids: Vec<&str> = phase_deliverables.deliverables.iter().map(|d| d.id.as_str()).collect();
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiError::new(
                "INVALID_DELIVERABLE",
                format!(
                    "Invalid deliverable_id '{}' for {} phase (contract type: {}). Valid IDs: {:?}",
                    req.deliverable_id, target_phase, contract.contract_type, valid_ids
                ),
            )),
        )
            .into_response();
    }

    // Check if already completed
    if contract.is_deliverable_complete(&target_phase, &req.deliverable_id) {
        return Json(serde_json::json!({
            "success": true,
            "message": format!("Deliverable '{}' is already marked complete for {} phase", req.deliverable_id, target_phase),
            "deliverableId": req.deliverable_id,
            "phase": target_phase,
            "alreadyComplete": true,
        }))
            .into_response();
    }

    // Mark the deliverable as complete
    match repository::mark_deliverable_complete(pool, id, &target_phase, &req.deliverable_id).await {
        Ok(updated_contract) => {
            let completed = updated_contract.get_completed_deliverables(&target_phase);
            Json(serde_json::json!({
                "success": true,
                "message": format!("Marked deliverable '{}' as complete for {} phase", req.deliverable_id, target_phase),
                "deliverableId": req.deliverable_id,
                "phase": target_phase,
                "completedDeliverables": completed,
            }))
                .into_response()
        }
        Err(e) => {
            tracing::error!("Failed to mark deliverable complete for contract {}: {}", id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

// =============================================================================
// Events
// =============================================================================

/// Get contract event history.
#[utoipa::path(
    get,
    path = "/api/v1/contracts/{id}/events",
    params(
        ("id" = Uuid, Path, description = "Contract ID")
    ),
    responses(
        (status = 200, description = "Event history", body = Vec<crate::db::models::ContractEvent>),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Contract not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Contracts"
)]
pub async fn get_events(
    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 contract exists and belongs to owner
    match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
        Ok(Some(_)) => {}
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiError::new("NOT_FOUND", "Contract not found")),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to get contract {}: {}", id, e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response();
        }
    }

    match repository::list_contract_events(pool, id).await {
        Ok(events) => Json(events).into_response(),
        Err(e) => {
            tracing::error!("Failed to get events for contract {}: {}", id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

// =============================================================================
// Internal Helper Functions
// =============================================================================

/// Clean up all worktrees for tasks in a contract.
///
/// This is called when a contract is completed or deleted to remove
/// all associated task worktrees from connected daemons.
async fn cleanup_contract_worktrees(
    pool: &sqlx::PgPool,
    state: &SharedState,
    contract_id: Uuid,
) {
    tracing::info!(
        contract_id = %contract_id,
        "Cleaning up worktrees for contract tasks"
    );

    // Get all tasks with worktree info for this contract
    let tasks = match repository::list_contract_tasks_with_worktree_info(pool, contract_id).await {
        Ok(tasks) => tasks,
        Err(e) => {
            tracing::error!(
                contract_id = %contract_id,
                error = %e,
                "Failed to list tasks for worktree cleanup"
            );
            return;
        }
    };

    if tasks.is_empty() {
        tracing::debug!(
            contract_id = %contract_id,
            "No tasks with worktrees to clean up"
        );
        return;
    }

    tracing::info!(
        contract_id = %contract_id,
        task_count = tasks.len(),
        "Found tasks with worktrees to clean up"
    );

    // Send cleanup command to each task's daemon
    for task in tasks {
        if let Some(daemon_id) = task.daemon_id {
            let cmd = crate::server::state::DaemonCommand::CleanupWorktree {
                task_id: task.id,
                delete_branch: true, // Delete the branch when contract is done
            };

            match state.send_daemon_command(daemon_id, cmd).await {
                Ok(()) => {
                    tracing::info!(
                        task_id = %task.id,
                        daemon_id = %daemon_id,
                        contract_id = %contract_id,
                        "Sent worktree cleanup command"
                    );
                }
                Err(e) => {
                    tracing::warn!(
                        task_id = %task.id,
                        daemon_id = %daemon_id,
                        contract_id = %contract_id,
                        error = %e,
                        "Failed to send worktree cleanup command (daemon may be offline)"
                    );
                }
            }
        } else {
            tracing::debug!(
                task_id = %task.id,
                contract_id = %contract_id,
                "Task has no daemon assigned, skipping worktree cleanup"
            );
        }
    }
}
