use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::sizing::RunnerSize;

#[derive(Debug, Serialize)]
pub struct SpawnRequest {
    pub cpu: u32,
    pub memory_gb: u32,
    pub disk_gb: u32,
    pub time_budget_min: u32,
    pub workspace: String,
    pub image: String,
}

#[derive(Debug, Deserialize)]
pub struct SpawnResponse {
    pub lease_id: String,
    pub ssh_host: String,
    pub ssh_port: u16,
    pub expires_at: String,
}

#[derive(Debug, Deserialize)]
pub struct P95Stats {
    pub cpu_p95: f64,
    pub memory_mb_p95: f64,
    #[allow(dead_code)]
    pub duration_secs_p95: f64,
    #[allow(dead_code)]
    pub sample_count: u64,
}

#[derive(Debug, Deserialize)]
struct ApiResponse<T> {
    success: bool,
    data: Option<T>,
    error: Option<String>,
}

#[derive(Clone)]
pub struct OrchestratorClient {
    base_url: String,
    http: reqwest::Client,
}

impl OrchestratorClient {
    pub fn new(base_url: String) -> Self {
        Self {
            base_url,
            http: reqwest::Client::new(),
        }
    }

    pub async fn spawn_runner(
        &self,
        size: &RunnerSize,
        workspace: &str,
        image: &str,
    ) -> Result<SpawnResponse> {
        let req = SpawnRequest {
            cpu: size.cpu,
            memory_gb: size.memory_gb,
            disk_gb: size.disk_gb,
            time_budget_min: size.time_budget_min,
            workspace: workspace.to_string(),
            image: image.to_string(),
        };

        let resp: ApiResponse<SpawnResponse> = self
            .http
            .post(format!("{}/api/v1/vm-pool/spawn", self.base_url))
            .json(&req)
            .send()
            .await
            .context("POST /api/v1/vm-pool/spawn failed")?
            .error_for_status()
            .context("orchestrator returned error for spawn")?
            .json()
            .await
            .context("failed to deserialize SpawnResponse")?;

        if !resp.success {
            anyhow::bail!(
                "spawn_runner: {}",
                resp.error.as_deref().unwrap_or("unknown error")
            );
        }
        resp.data.context("spawn_runner: missing data in response")
    }

    pub async fn destroy_runner(&self, lease_id: &str) -> Result<()> {
        let resp = self
            .http
            .delete(format!("{}/api/v1/vm-pool/{}", self.base_url, lease_id))
            .send()
            .await
            .context("DELETE /api/v1/vm-pool/{lease_id} failed")?;

        if !resp.status().is_success() {
            anyhow::bail!("destroy_runner HTTP {}", resp.status());
        }
        Ok(())
    }

    pub async fn get_p95(&self, workspace: &str) -> Result<Option<P95Stats>> {
        let resp = self
            .http
            .get(format!(
                "{}/api/v1/vm-pool/p95/{}",
                self.base_url, workspace
            ))
            .send()
            .await
            .context("GET /api/v1/vm-pool/p95/{workspace} failed")?;

        if resp.status() == reqwest::StatusCode::NOT_FOUND {
            return Ok(None);
        }

        let body: ApiResponse<P95Stats> = resp
            .error_for_status()
            .context("p95 endpoint error")?
            .json()
            .await
            .context("failed to deserialize P95Stats")?;

        Ok(body.data)
    }

    pub async fn record_metrics(
        &self,
        workspace: &str,
        cpu_secs: f64,
        mem_peak_mb: f64,
        duration_secs: f64,
    ) -> Result<()> {
        let payload = serde_json::json!({
            "workspace": workspace,
            "cpu_secs": cpu_secs,
            "mem_peak_mb": mem_peak_mb,
            "duration_secs": duration_secs,
        });

        let resp = self
            .http
            .post(format!("{}/api/v1/vm-pool/metrics", self.base_url))
            .json(&payload)
            .send()
            .await
            .context("POST /api/v1/vm-pool/metrics failed")?;

        if !resp.status().is_success() {
            anyhow::bail!("record_metrics HTTP {}", resp.status());
        }
        Ok(())
    }
}
