//! Workspace domain tools — list, show, active, register, dag, validate.

use crate::protocol::{SmartResponse, ToolCategory, ToolError};
use crate::sources::{
    NclCache, get_workspace, list_components, list_infra_envs, list_workspaces, list_workflows,
};
use crate::tool::{Context, Tool};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::path::PathBuf;
use std::sync::Arc;

// ── Response types ────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceSummary {
    pub name: String,
    pub path: String,
    pub environments: Vec<String>,
}

impl SmartResponse for WorkspaceSummary {
    fn summary(&self) -> String {
        format!("{} ({} env(s))", self.name, self.environments.len())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct UserConfig {
    #[serde(default)]
    active_workspace: Option<String>,
}

// ── User config helpers ───────────────────────────────────────────────────────

fn user_config_path() -> Option<PathBuf> {
    // Honour explicit override first (mirrors Nushell get-user-config-dir logic).
    if let Ok(dir) = std::env::var("PROVISIONING_USER_CONFIG_DIR") {
        return Some(PathBuf::from(dir).join("user_config.yaml"));
    }
    dirs::config_dir().map(|d| d.join("provisioning").join("user_config.yaml"))
}

fn read_user_config() -> UserConfig {
    let path = match user_config_path() {
        Some(p) if p.exists() => p,
        _ => return UserConfig { active_workspace: None },
    };
    let contents = std::fs::read_to_string(&path).unwrap_or_default();
    serde_yaml::from_str(&contents).unwrap_or(UserConfig { active_workspace: None })
}

fn write_user_config(cfg: &UserConfig) -> Result<(), ToolError> {
    let path = user_config_path()
        .ok_or_else(|| ToolError::invocation("cannot resolve user config path"))?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| ToolError::invocation(format!("create config dir: {e}")))?;
    }
    let yaml = serde_yaml::to_string(cfg)
        .map_err(|e| ToolError::invocation(format!("serialize user config: {e}")))?;
    std::fs::write(&path, yaml)
        .map_err(|e| ToolError::invocation(format!("write user config: {e}")))
}

// ── workspace_list ────────────────────────────────────────────────────────────

pub struct WorkspaceList;

#[async_trait]
impl Tool for WorkspaceList {
    fn name(&self) -> &'static str { "workspace_list" }
    fn description(&self) -> &'static str { "List all registered workspaces" }
    fn schema(&self) -> Value {
        json!({ "type": "object", "properties": {}, "additionalProperties": false })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, _params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let root = &ctx.env.workspaces_root;
        let entries = list_workspaces(root).await?;
        let active = read_user_config().active_workspace;

        let mut summaries = Vec::with_capacity(entries.len());
        for ws in &entries {
            let envs = list_infra_envs(&ws.name, &ws.path).await?;
            summaries.push(json!({
                "name": ws.name,
                "path": ws.path.to_string_lossy(),
                "active": active.as_deref() == Some(ws.name.as_str()),
                "environments": envs.iter().map(|e| &e.environment).collect::<Vec<_>>(),
            }));
        }

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

// ── workspace_show ────────────────────────────────────────────────────────────

pub struct WorkspaceShow {
    cache: Arc<NclCache>,
}

impl WorkspaceShow {
    pub fn new(cache: Arc<NclCache>) -> Self { Self { cache } }
}

#[async_trait]
impl Tool for WorkspaceShow {
    fn name(&self) -> &'static str { "workspace_show" }
    fn description(&self) -> &'static str { "Show workspace details including infra environments and NCL settings" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": { "type": "string", "description": "Workspace name" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

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

        let root = &ctx.env.workspaces_root;
        let ws = get_workspace(root, name).await?;
        let envs = list_infra_envs(name, &ws.path).await?;
        let active = read_user_config().active_workspace;

        let mut env_details = Vec::with_capacity(envs.len());
        for env in &envs {
            let comps = list_components(name, &env.environment, &env.path).await?;
            let flows = list_workflows(name, &env.environment, &env.path).await?;

            // Try to export settings.ncl if present
            let settings_path = env.path.join("settings.ncl");
            let settings = if settings_path.exists() {
                self.cache.get(&settings_path).await.ok().map(|v| (*v).clone())
            } else {
                None
            };

            env_details.push(json!({
                "name": env.environment,
                "components": comps.iter().map(|c| &c.name).collect::<Vec<_>>(),
                "workflows": flows.iter().map(|w| &w.name).collect::<Vec<_>>(),
                "settings": settings,
            }));
        }

        Ok(json!({
            "name": name,
            "path": ws.path.to_string_lossy(),
            "active": active.as_deref() == Some(name),
            "environments": env_details,
        }))
    }
}

// ── workspace_active ──────────────────────────────────────────────────────────

pub struct WorkspaceActive;

#[async_trait]
impl Tool for WorkspaceActive {
    fn name(&self) -> &'static str { "workspace_active" }
    fn description(&self) -> &'static str { "Show the currently active workspace" }
    fn schema(&self) -> Value {
        json!({ "type": "object", "properties": {}, "additionalProperties": false })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, _params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let cfg = read_user_config();
        match cfg.active_workspace {
            None => Ok(json!({ "active": null })),
            Some(name) => {
                let root = &ctx.env.workspaces_root;
                let exists = root.join(&name).is_dir();
                Ok(json!({ "active": name, "exists": exists }))
            }
        }
    }
}

// ── workspace_register ────────────────────────────────────────────────────────

pub struct WorkspaceRegister;

#[async_trait]
impl Tool for WorkspaceRegister {
    fn name(&self) -> &'static str { "workspace_register" }
    fn description(&self) -> &'static str { "Set the active workspace (writes to user config)" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": { "type": "string", "description": "Workspace name to activate" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Mutation }

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

        // Verify workspace exists on disk before registering.
        get_workspace(&ctx.env.workspaces_root, name).await?;

        let mut cfg = read_user_config();
        let previous = cfg.active_workspace.take();
        cfg.active_workspace = Some(name.to_owned());
        write_user_config(&cfg)?;

        Ok(json!({
            "activated": name,
            "previous": previous,
        }))
    }
}

// ── workspace_dag ─────────────────────────────────────────────────────────────

pub struct WorkspaceDag {
    cache: Arc<NclCache>,
}

impl WorkspaceDag {
    pub fn new(cache: Arc<NclCache>) -> Self { Self { cache } }
}

#[async_trait]
impl Tool for WorkspaceDag {
    fn name(&self) -> &'static str { "workspace_dag" }
    fn description(&self) -> &'static str { "List DAG files for a workspace, optionally filtered by environment" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": { "type": "string" },
                "env":  { "type": "string", "description": "Filter to a specific environment" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

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

        let root = &ctx.env.workspaces_root;
        let ws = get_workspace(root, name).await?;
        let envs = list_infra_envs(name, &ws.path).await?;

        let mut dags = Vec::new();
        for env in &envs {
            if let Some(f) = env_filter {
                if env.environment != f {
                    continue;
                }
            }
            let dag_path = env.path.join("dag.ncl");
            if !dag_path.exists() {
                continue;
            }
            // Prepend the workspace directory so non-relative imports like
            // "infra/lib/..." resolve against the workspace root.
            let ws_ip = ws.path.to_string_lossy();
            let content = self.cache.get_with_path(&dag_path, Some(ws_ip.as_ref())).await.ok().map(|v| (*v).clone());
            dags.push(json!({
                "workspace": name,
                "env": env.environment,
                "path": dag_path.to_string_lossy(),
                "dag": content,
            }));
        }

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

// ── workspace_validate ────────────────────────────────────────────────────────

pub struct WorkspaceValidate {
    cache: Arc<NclCache>,
}

impl WorkspaceValidate {
    pub fn new(cache: Arc<NclCache>) -> Self { Self { cache } }
}

#[async_trait]
impl Tool for WorkspaceValidate {
    fn name(&self) -> &'static str { "workspace_validate" }
    fn description(&self) -> &'static str { "Validate NCL config files in a workspace" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": { "type": "string" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

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

        let root = &ctx.env.workspaces_root;
        let ws = get_workspace(root, name).await?;
        let envs = list_infra_envs(name, &ws.path).await?;

        let mut errors: Vec<Value> = Vec::new();
        let mut checked = 0u32;

        for env in &envs {
            // Check every .ncl file in the env directory (non-recursive, top level only)
            let mut dir = tokio::fs::read_dir(&env.path).await
                .map_err(|e| ToolError::invocation(e.to_string()))?;
            while let Some(entry) = dir.next_entry().await
                .map_err(|e| ToolError::invocation(e.to_string()))?
            {
                let path = entry.path();
                if path.extension().and_then(|e| e.to_str()) != Some("ncl") {
                    continue;
                }
                checked += 1;
                if let Err(e) = self.cache.get(&path).await {
                    errors.push(json!({
                        "file": path.to_string_lossy(),
                        "error": e.to_string(),
                    }));
                }
            }
        }

        Ok(json!({
            "workspace": name,
            "valid": errors.is_empty(),
            "files_checked": checked,
            "errors": errors,
        }))
    }
}
