use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};

use a3s_boot::{BootError, Result as BootResult};
use serde_json::{json, Value};
use tokio::fs;
use tokio::process::Command;
use tokio::time::timeout;

use crate::api::code_web::state::CodeWebState;
use crate::config;

const ACTION_TIMEOUT: Duration = Duration::from_secs(180);

pub(in crate::api::code_web) struct CapabilitiesService {
    state: Arc<CodeWebState>,
}

impl CapabilitiesService {
    pub(in crate::api::code_web) fn new(state: Arc<CodeWebState>) -> Self {
        Self { state }
    }

    pub(in crate::api::code_web) fn overview(&self) -> serde_json::Value {
        let dirs = self.capability_dirs();
        json!({
            "app": "a3s-code-web",
            "workspaceRoot": self.state.default_workspace.display().to_string(),
            "configPath": self.state.config_path.display().to_string(),
            "defaultModel": self.state.current_default_model(),
            "dirs": dirs
                .iter()
                .map(capability_dir_summary)
                .collect::<Vec<_>>(),
        })
    }

    pub(in crate::api::code_web) async fn ensure_dirs(&self) -> BootResult<serde_json::Value> {
        let dirs = self.capability_dirs();
        for dir in &dirs {
            fs::create_dir_all(&dir.path).await.map_err(fs_error)?;
        }
        fs::create_dir_all(self.kb_dir().join("sources"))
            .await
            .map_err(fs_error)?;
        fs::create_dir_all(self.kb_dir().join("wiki"))
            .await
            .map_err(fs_error)?;
        Ok(self.overview())
    }

    pub(in crate::api::code_web) fn lifecycles(&self) -> serde_json::Value {
        lifecycle_catalog()
    }

    pub(in crate::api::code_web) async fn run_action(
        &self,
        request: Value,
    ) -> BootResult<serde_json::Value> {
        let family = required_text(&request, "family")?;
        let action = required_text(&request, "action")?;
        let args = build_code_action_args(&request, family, action)?;
        let started = Instant::now();
        let executable = std::env::current_exe().map_err(|error| {
            BootError::Internal(format!("could not locate current a3s executable: {error}"))
        })?;

        let mut command = Command::new(executable);
        command
            .args(&args)
            .current_dir(&self.state.default_workspace)
            .kill_on_drop(true);

        let output = timeout(ACTION_TIMEOUT, command.output())
            .await
            .map_err(|_| {
                BootError::Internal(format!(
                    "a3s code {family} {action} timed out after {} seconds",
                    ACTION_TIMEOUT.as_secs()
                ))
            })?
            .map_err(|error| {
                BootError::Internal(format!("failed to run a3s code action: {error}"))
            })?;

        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
        let exit_code = output.status.code();

        Ok(json!({
            "success": output.status.success(),
            "family": family,
            "action": action,
            "args": args,
            "command": format!("a3s {}", args.join(" ")),
            "exitCode": exit_code,
            "stdout": stdout,
            "stderr": stderr,
            "durationMs": started.elapsed().as_millis() as u64,
        }))
    }

    fn capability_dirs(&self) -> Vec<CapabilityDir> {
        vec![
            CapabilityDir::new(
                "agents",
                "Agents",
                "agent",
                config::agent_dir(),
                "Local agent definitions",
            ),
            CapabilityDir::new("mcp", "MCP", "mcp", config::mcp_dir(), "MCP server assets"),
            CapabilityDir::new(
                "skills",
                "Skills",
                "skill",
                config::skill_dir(),
                "Local skill packages",
            ),
            CapabilityDir::new(
                "flows",
                "Flows",
                "flow",
                config::flow_dir(),
                "Workflow DAG assets",
            ),
            CapabilityDir::new(
                "okf",
                "OKF",
                "okf",
                self.okf_dir(),
                "Knowledge package assets",
            ),
            CapabilityDir::new(
                "memory",
                "Memory",
                "memory",
                config::memory_dir(),
                "Long-term memory store",
            ),
        ]
    }

    fn kb_dir(&self) -> PathBuf {
        self.state.default_workspace.join(".a3s").join("kb")
    }

    fn okf_dir(&self) -> PathBuf {
        self.state.default_workspace.join(".a3s").join("okf")
    }
}

fn lifecycle_catalog() -> serde_json::Value {
    json!({
        "stages": [
            stage("create", "Create", "Draft a new local asset package"),
            stage("develop", "Develop", "Edit and validate package files locally"),
            stage("test", "Test", "Run service-backed tests"),
            stage("run", "Run", "Start a runtime execution"),
            stage("publish", "Publish", "Publish the local asset to A3S OS"),
            stage("deploy", "Deploy", "Sync or deploy a serving runtime binding"),
            stage("inspect", "Inspect", "Open or inspect an existing OS asset"),
            stage("activity", "Activity", "Inspect runtime activity"),
            stage("observe", "Observe", "Open status, logs, or view links"),
        ],
        "families": [
            lifecycle(
                "agentic-agent",
                "agents",
                "agent",
                "Agentic agent",
                "Agent as a Service",
                ["create", "develop", "run", "publish", "observe"],
                ["local", "clone", "list", "activity", "review", "publish", "run", "open", "logs", "status"],
            ),
            lifecycle(
                "application-agent",
                "agents",
                "agent",
                "Application agent",
                "Agent as a Service",
                ["create", "develop", "publish", "deploy", "observe"],
                ["local", "clone", "list", "activity", "review", "publish", "deploy", "open", "logs", "status"],
            ),
            lifecycle(
                "tool-agent",
                "agents",
                "agent",
                "Tool agent",
                "Function as a Service",
                ["create", "develop", "publish", "activity", "observe"],
                ["local", "clone", "list", "activity", "review", "publish", "open", "logs", "status"],
            ),
            lifecycle(
                "mcp-server",
                "mcp",
                "mcp",
                "MCP server",
                "Function as a Service",
                ["create", "develop", "run", "test", "publish", "deploy", "observe"],
                ["local", "clone", "list", "activity", "review", "publish", "run", "deploy", "test", "open", "logs", "status"],
            ),
            lifecycle(
                "skill",
                "skills",
                "skill",
                "Skill",
                "Function as a Service",
                ["create", "develop", "publish", "deploy", "inspect", "activity"],
                ["local", "clone", "list", "activity", "review", "publish", "deploy", "open", "status"],
            ),
            lifecycle(
                "okf-package",
                "okf",
                "okf",
                "OKF knowledge package",
                "Knowledge service",
                ["create", "develop", "publish", "deploy", "inspect", "activity"],
                ["local", "clone", "list", "activity", "review", "publish", "deploy", "status"],
            ),
            lifecycle(
                "workflow-flow",
                "flows",
                "flow",
                "Workflow flow",
                "Workflow as a Service",
                ["create", "develop", "publish", "run", "deploy", "activity", "observe"],
                ["local", "clone", "list", "activity", "review", "publish", "run", "deploy", "open", "logs", "status"],
            ),
        ],
    })
}

fn stage(id: &str, label: &str, description: &str) -> serde_json::Value {
    json!({
        "id": id,
        "label": label,
        "description": description,
    })
}

fn lifecycle<const STAGES: usize, const ACTIONS: usize>(
    id: &str,
    dir_id: &str,
    family: &str,
    label: &str,
    service: &str,
    stages: [&str; STAGES],
    actions: [&str; ACTIONS],
) -> serde_json::Value {
    json!({
        "id": id,
        "dirId": dir_id,
        "family": family,
        "label": label,
        "service": service,
        "stages": stages.to_vec(),
        "actions": actions.to_vec(),
    })
}

fn build_code_action_args(request: &Value, family: &str, action: &str) -> BootResult<Vec<String>> {
    match family {
        "agent" => build_agent_action_args(request, action),
        "mcp" => build_asset_action_args(
            request,
            "mcp",
            action,
            &[
                "local", "clone", "list", "activity", "review", "publish", "run", "deploy", "test",
                "open", "logs", "status",
            ],
        ),
        "skill" => build_asset_action_args(
            request,
            "skill",
            action,
            &[
                "local", "clone", "list", "activity", "review", "publish", "deploy", "open",
                "status",
            ],
        ),
        "flow" => build_asset_action_args(
            request,
            "flow",
            action,
            &[
                "local", "clone", "list", "activity", "review", "publish", "run", "deploy", "open",
                "logs", "status",
            ],
        ),
        "okf" => build_asset_action_args(
            request,
            "okf",
            action,
            &[
                "local", "clone", "list", "activity", "review", "publish", "deploy", "status",
            ],
        ),
        other => Err(BootError::BadRequest(format!(
            "unsupported capability family `{other}`"
        ))),
    }
}

fn build_agent_action_args(request: &Value, action: &str) -> BootResult<Vec<String>> {
    match action {
        "local" | "list" | "activity" => query_action_args("agent", action, request),
        "clone" => clone_action_args("agent", request),
        "review" | "run" | "deploy" => path_action_args("agent", action, request),
        "publish" => {
            let kind = required_agent_kind(request)?;
            let mut args = vec![
                "code".to_string(),
                "agent".to_string(),
                "publish".to_string(),
                kind,
            ];
            append_optional_field(&mut args, request, "path");
            Ok(args)
        }
        "open" | "logs" | "status" => {
            let mut args = vec!["code".to_string(), "agent".to_string(), action.to_string()];
            if let Some(kind) = optional_agent_kind(request)? {
                args.push(kind);
            }
            append_optional_field(&mut args, request, "path");
            Ok(args)
        }
        other => Err(BootError::BadRequest(format!(
            "unsupported agent action `{other}`"
        ))),
    }
}

fn build_asset_action_args(
    request: &Value,
    family: &str,
    action: &str,
    supported: &[&str],
) -> BootResult<Vec<String>> {
    if !supported.contains(&action) {
        return Err(BootError::BadRequest(format!(
            "unsupported {family} action `{action}`"
        )));
    }

    match action {
        "local" | "list" | "activity" => query_action_args(family, action, request),
        "clone" => clone_action_args(family, request),
        "review" | "publish" | "deploy" | "test" | "run" | "open" | "logs" | "status" => {
            path_action_args(family, action, request)
        }
        other => Err(BootError::BadRequest(format!(
            "unsupported {family} action `{other}`"
        ))),
    }
}

fn query_action_args(family: &str, action: &str, request: &Value) -> BootResult<Vec<String>> {
    let mut args = vec!["code".to_string(), family.to_string(), action.to_string()];
    if let Some(query) = optional_text(request, "query") {
        args.extend(query.split_whitespace().map(str::to_string));
    }
    Ok(args)
}

fn clone_action_args(family: &str, request: &Value) -> BootResult<Vec<String>> {
    Ok(vec![
        "code".to_string(),
        family.to_string(),
        "clone".to_string(),
        required_text(request, "url")?.to_string(),
    ])
}

fn path_action_args(family: &str, action: &str, request: &Value) -> BootResult<Vec<String>> {
    let mut args = vec!["code".to_string(), family.to_string(), action.to_string()];
    append_optional_field(&mut args, request, "path");
    Ok(args)
}

fn append_optional_field(args: &mut Vec<String>, request: &Value, name: &str) {
    if let Some(value) = optional_text(request, name) {
        args.push(value.to_string());
    }
}

fn required_text<'a>(request: &'a Value, name: &str) -> BootResult<&'a str> {
    optional_text(request, name).ok_or_else(|| BootError::BadRequest(format!("{name} is required")))
}

fn optional_text<'a>(request: &'a Value, name: &str) -> Option<&'a str> {
    request
        .get(name)
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
}

fn required_agent_kind(request: &Value) -> BootResult<String> {
    optional_agent_kind(request)?
        .ok_or_else(|| BootError::BadRequest("kind is required for agent publish".to_string()))
}

fn optional_agent_kind(request: &Value) -> BootResult<Option<String>> {
    let Some(kind) = optional_text(request, "kind") else {
        return Ok(None);
    };
    match kind {
        "agentic" | "application" | "tool" => Ok(Some(kind.to_string())),
        other => Err(BootError::BadRequest(format!(
            "unsupported agent kind `{other}`"
        ))),
    }
}

struct CapabilityDir {
    id: &'static str,
    label: &'static str,
    kind: &'static str,
    path: PathBuf,
    description: &'static str,
}

impl CapabilityDir {
    fn new(
        id: &'static str,
        label: &'static str,
        kind: &'static str,
        path: PathBuf,
        description: &'static str,
    ) -> Self {
        Self {
            id,
            label,
            kind,
            path,
            description,
        }
    }
}

fn capability_dir_summary(dir: &CapabilityDir) -> serde_json::Value {
    let exists = dir.path.is_dir();
    json!({
        "id": dir.id,
        "label": dir.label,
        "kind": dir.kind,
        "path": dir.path.display().to_string(),
        "description": dir.description,
        "exists": exists,
        "itemCount": if exists { top_level_item_count(&dir.path) } else { 0 },
    })
}

fn top_level_item_count(path: &Path) -> usize {
    std::fs::read_dir(path)
        .ok()
        .map(|entries| entries.filter_map(Result::ok).count())
        .unwrap_or(0)
}

fn fs_error(error: std::io::Error) -> BootError {
    match error.kind() {
        std::io::ErrorKind::PermissionDenied => BootError::Forbidden(error.to_string()),
        std::io::ErrorKind::InvalidInput | std::io::ErrorKind::InvalidData => {
            BootError::BadRequest(error.to_string())
        }
        _ => BootError::Io(error),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn lifecycle_catalog_exposes_mcp_run_without_debug_or_invoke() {
        let catalog = lifecycle_catalog();
        let stages = catalog["stages"].as_array().unwrap();
        assert!(stages.iter().any(|stage| stage["id"] == "run"));
        assert!(stages.iter().all(|stage| stage["id"] != "debug"));

        let mcp = catalog["families"]
            .as_array()
            .unwrap()
            .iter()
            .find(|family| family["id"] == "mcp-server")
            .expect("mcp lifecycle");
        let mcp_stages = mcp["stages"].as_array().unwrap();
        let mcp_actions = mcp["actions"].as_array().unwrap();
        assert!(mcp_stages.iter().any(|stage| stage == "run"));
        assert!(mcp_actions.iter().any(|action| action == "run"));
        assert!(mcp_stages.iter().all(|stage| stage != "debug"));
        assert!(mcp_actions.iter().all(|action| action != "debug"));
        assert!(mcp_actions.iter().all(|action| action != "invoke"));
    }

    #[test]
    fn capability_action_builder_rejects_mcp_debug_and_invoke() {
        let request = json!({"path": "mcps/weather"});
        assert_eq!(
            build_code_action_args(&request, "mcp", "run").unwrap(),
            vec!["code", "mcp", "run", "mcps/weather"]
        );
        assert!(build_code_action_args(&request, "mcp", "debug").is_err());
        assert!(build_code_action_args(&request, "mcp", "invoke").is_err());
    }
}
