//! Orchestrator domain tools — job inspection and workflow dispatch.
//!
//! Read tools (list/show) use the shared `OrchestratorClient`.
//! `orchestrator_run_workflow` dispatches to the correct workflow endpoint by type.
//! `orchestrator_job_cancel` attempts `POST /tasks/{id}/cancel`; returns an invocation
//! error if the orchestrator does not support the endpoint (graceful degradation).

use crate::protocol::{ToolCategory, ToolError};
use crate::sources::OrchestratorClient;
use crate::tool::{Context, Tool};
use async_trait::async_trait;
use serde_json::{Value, json};
use std::sync::Arc;

// ── orchestrator_jobs ─────────────────────────────────────────────────────────

pub struct OrchestratorJobs {
    client: Arc<OrchestratorClient>,
}
impl OrchestratorJobs {
    pub fn new(client: Arc<OrchestratorClient>) -> Self { Self { client } }
}

#[async_trait]
impl Tool for OrchestratorJobs {
    fn name(&self) -> &'static str { "orchestrator_jobs" }
    fn description(&self) -> &'static str { "List recent orchestrator jobs (tasks)" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "status": {
                    "type": "string",
                    "enum": ["Pending", "Running", "Completed", "Failed", "Cancelled"],
                    "description": "Filter by task state"
                }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, _ctx: &Context) -> Result<Value, ToolError> {
        let status_filter = params["status"].as_str();
        let mut tasks = self.client.list_tasks().await?;

        if let Some(f) = status_filter {
            tasks.retain(|t| t.status.to_string() == f);
        }
        tasks.sort_by(|a, b| b.created_at.cmp(&a.created_at));

        let items: Vec<Value> = tasks
            .into_iter()
            .map(|t| serde_json::to_value(t).unwrap_or(Value::Null))
            .collect();

        Ok(json!({ "items": items, "total": items.len() }))
    }
}

// ── orchestrator_job_show ─────────────────────────────────────────────────────

pub struct OrchestratorJobShow {
    client: Arc<OrchestratorClient>,
}
impl OrchestratorJobShow {
    pub fn new(client: Arc<OrchestratorClient>) -> Self { Self { client } }
}

#[async_trait]
impl Tool for OrchestratorJobShow {
    fn name(&self) -> &'static str { "orchestrator_job_show" }
    fn description(&self) -> &'static str { "Show details of a specific orchestrator job" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["id"],
            "properties": {
                "id": { "type": "string", "description": "Task UUID" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, _ctx: &Context) -> Result<Value, ToolError> {
        let id = params["id"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("id", "required string"))?;
        let task = self.client.get_task(id).await?;
        serde_json::to_value(task)
            .map_err(|e| ToolError::invocation(format!("serialize task: {e}")))
    }
}

// ── orchestrator_job_cancel ───────────────────────────────────────────────────

pub struct OrchestratorJobCancel;
impl OrchestratorJobCancel {
    pub fn new() -> Self { Self }
}
impl Default for OrchestratorJobCancel {
    fn default() -> Self { Self::new() }
}

#[async_trait]
impl Tool for OrchestratorJobCancel {
    fn name(&self) -> &'static str { "orchestrator_job_cancel" }
    fn description(&self) -> &'static str {
        "Cancel a running orchestrator job (POST /tasks/{id}/cancel)"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["id"],
            "properties": {
                "id": { "type": "string", "description": "Task UUID to cancel" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Mutation }

    async fn invoke(&self, params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let id = params["id"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("id", "required string"))?;

        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(10))
            .build()
            .map_err(|e| ToolError::invocation(format!("http client: {e}")))?;

        let url = format!("{}/tasks/{id}/cancel", ctx.env.orchestrator_url);
        let resp = client
            .post(&url)
            .send()
            .await
            .map_err(|e| ToolError::invocation(format!("orchestrator unreachable: {e}")))?;

        let status = resp.status();
        if status == reqwest::StatusCode::NOT_FOUND || status == reqwest::StatusCode::METHOD_NOT_ALLOWED {
            return Err(ToolError::invocation(
                "orchestrator does not support job cancellation — kill the process manually",
            ));
        }
        if !status.is_success() {
            return Err(ToolError::invocation(format!(
                "orchestrator returned {status} cancelling task {id}"
            )));
        }

        Ok(json!({ "id": id, "cancelled": true }))
    }
}

// ── orchestrator_run_workflow ─────────────────────────────────────────────────

pub struct OrchestratorRunWorkflow;
impl OrchestratorRunWorkflow {
    pub fn new() -> Self { Self }
}
impl Default for OrchestratorRunWorkflow {
    fn default() -> Self { Self::new() }
}

#[async_trait]
impl Tool for OrchestratorRunWorkflow {
    fn name(&self) -> &'static str { "orchestrator_run_workflow" }
    fn description(&self) -> &'static str {
        "Dispatch an arbitrary workflow to the orchestrator (servers, cluster, or taskserv)"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["kind", "infra", "settings"],
            "properties": {
                "kind": {
                    "type": "string",
                    "enum": ["servers", "cluster", "taskserv"],
                    "description": "Workflow type"
                },
                "infra":     { "type": "string" },
                "settings":  { "type": "string" },
                "payload":   {
                    "type": "object",
                    "description": "Extra fields merged into the workflow body (e.g. taskserv, cluster_type, servers)",
                    "additionalProperties": true
                }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Mutation }

    async fn invoke(&self, params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let kind = params["kind"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("kind", "required string"))?;
        let infra = params["infra"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("infra", "required string"))?;
        let settings = params["settings"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("settings", "required string"))?;

        let endpoint = match kind {
            "servers"  => "workflows/servers/create",
            "cluster"  => "workflows/cluster/create",
            "taskserv" => "workflows/taskserv/create",
            other => {
                return Err(ToolError::invalid_param(
                    "kind",
                    format!("unknown workflow kind '{other}'"),
                ))
            }
        };

        let mut body = params["payload"]
            .as_object()
            .cloned()
            .map(serde_json::Value::Object)
            .unwrap_or_else(|| json!({}));

        body["infra"] = json!(infra);
        body["settings"] = json!(settings);

        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(10))
            .build()
            .map_err(|e| ToolError::invocation(format!("http client: {e}")))?;

        let url = format!("{}/{endpoint}", ctx.env.orchestrator_url);
        let resp = client
            .post(&url)
            .json(&body)
            .send()
            .await
            .map_err(|e| ToolError::invocation(format!("orchestrator unreachable: {e}")))?;

        let status = resp.status();
        if !status.is_success() {
            return Err(ToolError::invocation(format!(
                "orchestrator returned {status} for {kind} workflow"
            )));
        }

        let result: Value = resp
            .json()
            .await
            .map_err(|e| ToolError::invocation(format!("parse response: {e}")))?;

        Ok(json!({
            "kind":    kind,
            "infra":   infra,
            "task_id": result["data"],
            "status":  "queued",
        }))
    }
}
