use std::sync::Arc;

use axum::body::Bytes;
use axum::http::Method;
use serde_json::{json, Value};

use crate::db::provider_supports;
use crate::http::{json_response, HttpResponse};
use crate::microservice_proxy::strict_microservice_health_probe;
use crate::state::AppState;
use crate::task_dispatcher::{create_and_send_task, wait_for_task_terminal};
use crate::types::RawTaskRow;

pub async fn get_pgdata_usage(state: &Arc<AppState>) -> anyhow::Result<Value> {
    let client = state.pool.get().await?;
    let row = client
        .query_one(
            r#"
            SELECT
              current_database()::text AS database_name,
              pg_database_size(current_database())::bigint AS database_bytes,
              pg_size_pretty(pg_database_size(current_database()))::text AS database_pretty
            "#,
            &[],
        )
        .await?;
    let database_bytes: i64 = row.get("database_bytes");
    Ok(json!({
        "volumeName": state.config.database_volume_name,
        "volumeSize": state.config.database_volume_size,
        "databaseName": row.get::<_, String>("database_name"),
        "databaseBytes": database_bytes,
        "databasePretty": row.get::<_, String>("database_pretty"),
    }))
}

async fn count_pending_tasks(state: &Arc<AppState>) -> anyhow::Result<i64> {
    let client = state.pool.get().await?;
    let row = client
        .query_one("SELECT count(*)::bigint AS count FROM unidesk_tasks WHERE status IN ('queued', 'dispatched', 'running')", &[])
        .await?;
    Ok(row.get("count"))
}

async fn microservice_availability_summary(state: &Arc<AppState>) -> Value {
    let mut probes = Vec::new();
    for service in &state.config.microservices {
        probes.push(
            strict_microservice_health_probe(
                state,
                service,
                Some(service.backend.timeout_ms.clamp(2500, 10_000)),
            )
            .await,
        );
    }
    let healthy_count = probes
        .iter()
        .filter(|probe| probe.get("healthy").and_then(Value::as_bool) == Some(true))
        .count();
    let mut by_provider = serde_json::Map::new();
    for probe in &probes {
        let provider_id = probe
            .get("providerId")
            .and_then(Value::as_str)
            .unwrap_or("unknown");
        let entry = by_provider
            .entry(provider_id.to_string())
            .or_insert_with(|| json!({ "total": 0, "healthy": 0, "unhealthy": 0 }));
        if let Some(object) = entry.as_object_mut() {
            object["total"] = json!(object.get("total").and_then(Value::as_i64).unwrap_or(0) + 1);
            if probe.get("healthy").and_then(Value::as_bool) == Some(true) {
                object["healthy"] =
                    json!(object.get("healthy").and_then(Value::as_i64).unwrap_or(0) + 1);
            } else {
                object["unhealthy"] =
                    json!(object.get("unhealthy").and_then(Value::as_i64).unwrap_or(0) + 1);
            }
        }
    }
    json!({
        "totalCount": probes.len(),
        "healthyCount": healthy_count,
        "unhealthyCount": probes.len().saturating_sub(healthy_count),
        "checkedAt": crate::json_util::now_iso(),
        "byProvider": by_provider,
        "services": probes.iter().map(|probe| json!({
            "serviceId": probe.get("serviceId").cloned().unwrap_or(Value::Null),
            "name": probe.get("name").cloned().unwrap_or(Value::Null),
            "providerId": probe.get("providerId").cloned().unwrap_or(Value::Null),
            "healthy": probe.get("healthy").cloned().unwrap_or(Value::Null),
            "status": probe.get("status").cloned().unwrap_or(Value::Null),
            "reason": probe.get("reason").cloned().unwrap_or(Value::Null),
            "checkedAt": probe.get("checkedAt").cloned().unwrap_or(Value::Null),
        })).collect::<Vec<_>>(),
    })
}

pub async fn get_overview(state: &Arc<AppState>) -> anyhow::Result<Value> {
    let client = state.pool.get().await?;
    let node_row = client
        .query_one(
            "SELECT count(*)::bigint AS node_count, count(*) FILTER (WHERE status = 'online')::bigint AS online_node_count FROM unidesk_nodes",
            &[],
        )
        .await?;
    let docker_row = client
        .query_one(
            "SELECT count(*) FILTER (WHERE d.status IS NOT NULL)::bigint AS docker_status_node_count FROM unidesk_nodes n LEFT JOIN unidesk_node_docker_status d ON d.provider_id = n.provider_id",
            &[],
        )
        .await?;
    let system_row = client
        .query_one(
            "SELECT count(*) FILTER (WHERE s.status IS NOT NULL)::bigint AS system_status_node_count FROM unidesk_nodes n LEFT JOIN unidesk_node_system_status s ON s.provider_id = n.provider_id",
            &[],
        )
        .await?;
    Ok(json!({
        "service": "unidesk-core",
        "ok": true,
        "dbReady": state.db_ready(),
        "pgdata": get_pgdata_usage(state).await.unwrap_or_else(|error| json!({ "error": error.to_string() })),
        "uptimeSeconds": (chrono::Utc::now() - state.service_started_at).num_seconds(),
        "nodeCount": node_row.get::<_, i64>("node_count"),
        "onlineNodeCount": node_row.get::<_, i64>("online_node_count"),
        "dockerStatusNodeCount": docker_row.get::<_, i64>("docker_status_node_count"),
        "systemStatusNodeCount": system_row.get::<_, i64>("system_status_node_count"),
        "pendingTaskCount": count_pending_tasks(state).await.unwrap_or(0),
        "microserviceAvailability": microservice_availability_summary(state).await,
        "taskPendingTimeoutMs": state.config.task_pending_timeout_ms,
        "activeSocketCount": state.active_provider_count().await,
        "heartbeatTimeoutMs": state.config.heartbeat_timeout_ms,
    }))
}

fn record_from_task(task: Option<&RawTaskRow>) -> Value {
    match task {
        Some(task) => json!({
            "id": task.id,
            "providerId": task.provider_id,
            "command": task.command,
            "status": task.status,
            "payload": task.payload,
            "result": task.result,
            "updatedAt": task.updated_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
        }),
        None => Value::Null,
    }
}

pub async fn run_host_ssh_perf_command(
    state: &Arc<AppState>,
    provider_id: &str,
    command: String,
    timeout_ms: u64,
) -> anyhow::Result<Value> {
    let (task_id, provider_online) = create_and_send_task(
        state,
        provider_id,
        "host.ssh",
        json!({
            "source": "code-queue-performance-panel",
            "mode": "exec",
            "cwd": "/root/unidesk",
            "timeoutMs": 15_000,
            "command": command,
        }),
    )
    .await?;
    if !provider_online {
        return Ok(
            json!({ "ok": false, "taskId": task_id, "task": Value::Null, "stdout": "", "stderr": format!("provider is offline: {provider_id}"), "exitCode": Value::Null, "timedOut": false }),
        );
    }
    let task = wait_for_task_terminal(state, &task_id, timeout_ms).await?;
    let result = task
        .as_ref()
        .and_then(|task| task.result.as_ref())
        .and_then(Value::as_object)
        .cloned()
        .unwrap_or_default();
    Ok(json!({
        "ok": task.as_ref().is_some_and(|task| task.status == "succeeded") && result.get("ok").and_then(Value::as_bool) == Some(true),
        "taskId": task_id,
        "task": record_from_task(task.as_ref()),
        "stdout": result.get("stdout").and_then(Value::as_str).unwrap_or(""),
        "stderr": result.get("stderr").and_then(Value::as_str).unwrap_or(""),
        "exitCode": result.get("exitCode").cloned().unwrap_or(Value::Null),
        "timedOut": result.get("timedOut").and_then(Value::as_bool).unwrap_or(false),
    }))
}

pub async fn codex_queue_load_test(
    state: &Arc<AppState>,
    method: Method,
    body: Bytes,
) -> anyhow::Result<HttpResponse> {
    let body: Value = if method == Method::POST {
        serde_json::from_slice(&body).unwrap_or_else(|_| json!({}))
    } else {
        json!({})
    };
    let codex_service = state
        .config
        .microservices
        .iter()
        .find(|service| service.id == "code-queue");
    let provider_id = body
        .get("providerId")
        .and_then(Value::as_str)
        .filter(|value| !value.is_empty())
        .or_else(|| codex_service.map(|service| service.provider_id.as_str()))
        .unwrap_or("main-server");
    if !provider_supports(state, provider_id, "host.ssh")
        .await
        .unwrap_or(false)
    {
        return Ok(json_response(
            json!({ "ok": true, "measurementOk": false, "error": format!("provider does not declare host.ssh capability: {provider_id}"), "providerId": provider_id }),
            200,
        ));
    }
    let timeout_ms = number_from_body(&body, "timeoutMs", 90_000, 5_000, 180_000);
    let target_ms = number_from_body(&body, "targetMs", 1_000, 100, 60_000);
    let run_id = format!(
        "codex_queue_perf_{}_{}",
        chrono::Utc::now().timestamp_millis(),
        rand::random::<u32>()
    );
    let dir = "/root/.unidesk/.state/code-queue-perf";
    let output_path = format!("{dir}/{run_id}.json");
    let stderr_path = format!("{dir}/{run_id}.stderr");
    let exit_path = format!("{dir}/{run_id}.exit");
    let url_arg = body
        .get("url")
        .and_then(Value::as_str)
        .filter(|value| !value.is_empty())
        .map(|value| format!(" --url {}", shell_quote(value)))
        .unwrap_or_default();
    let start_command = format!(
        "mkdir -p {}; rm -f {} {} {}; (PLAYWRIGHT_BROWSERS_PATH=/root/.unidesk/.state/playwright-browsers bun scripts/src/code-queue-perf.ts --json --timeout-ms {} --target-ms {}{} > {}; printf '%s' \"$?\" > {}) > {} 2>&1 & printf '%s\\n' {}",
        shell_quote(dir),
        shell_quote(&output_path),
        shell_quote(&stderr_path),
        shell_quote(&exit_path),
        timeout_ms,
        target_ms,
        url_arg,
        shell_quote(&output_path),
        shell_quote(&exit_path),
        shell_quote(&stderr_path),
        shell_quote(&run_id),
    );
    let start = run_host_ssh_perf_command(state, provider_id, start_command, 18_000).await?;
    if start.get("ok").and_then(Value::as_bool) != Some(true) {
        return Ok(json_response(
            json!({ "ok": true, "measurementOk": false, "providerId": provider_id, "runId": run_id, "stage": "start", "error": start.get("stderr").and_then(Value::as_str).unwrap_or("failed to start Playwright benchmark"), "taskId": start.get("taskId").cloned().unwrap_or(Value::Null), "task": start.get("task").cloned().unwrap_or(Value::Null) }),
            200,
        ));
    }
    Ok(json_response(
        json!({ "ok": true, "measurementOk": true, "providerId": provider_id, "runId": run_id, "stage": "started", "result": { "ok": true, "targetMs": target_ms, "timeoutMs": timeout_ms } }),
        200,
    ))
}

fn number_from_body(body: &Value, key: &str, fallback: u64, min: u64, max: u64) -> u64 {
    body.get(key)
        .and_then(|value| {
            value
                .as_u64()
                .or_else(|| value.as_str().and_then(|text| text.parse::<u64>().ok()))
        })
        .unwrap_or(fallback)
        .clamp(min, max)
}

fn shell_quote(value: &str) -> String {
    format!("'{}'", value.replace('\'', "'\\''"))
}
