use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use runx_contracts::{JsonObject, JsonValue};
use runx_core::policy::CwdPolicy;
use runx_parser::SkillSandbox;

use crate::RuntimeError;
use crate::sandbox::SandboxPlan;

const DEFAULT_SANDBOX_ENV_ALLOWLIST: [&str; 9] = [
    "PATH",
    "HOME",
    "TMPDIR",
    "TMP",
    "TEMP",
    "SystemRoot",
    "WINDIR",
    "COMSPEC",
    "PATHEXT",
];

pub(super) fn mcp_process_sandbox_metadata(
    sandbox: Option<&SkillSandbox>,
    plan: &SandboxPlan,
    env: &BTreeMap<String, String>,
) -> Result<JsonObject, RuntimeError> {
    let config = mcp_sandbox_metadata_config(sandbox);
    let mut metadata = mcp_sandbox_location_metadata(&config, plan, env)?;
    metadata.insert(
        "env".to_owned(),
        JsonValue::Object(mcp_sandbox_env_metadata(
            config.env_allowlist,
            config.inherited_ambient,
        )),
    );
    metadata.insert(
        "network".to_owned(),
        JsonValue::Object(mcp_sandbox_network_metadata(config.profile, config.network)),
    );
    metadata.insert(
        "writable_paths".to_owned(),
        mcp_sandbox_writable_paths_metadata(config.writable_paths),
    );
    metadata.insert(
        "require_enforcement".to_owned(),
        JsonValue::Bool(config.require_enforcement),
    );
    metadata.insert(
        "filesystem".to_owned(),
        JsonValue::Object(mcp_sandbox_filesystem_metadata(config.profile)),
    );
    metadata.insert(
        "approval".to_owned(),
        JsonValue::Object(mcp_sandbox_approval_metadata(&config)),
    );
    metadata.insert(
        "runtime".to_owned(),
        JsonValue::Object(mcp_sandbox_runtime_metadata(config.profile)),
    );
    Ok(metadata)
}

struct McpSandboxMetadataConfig<'a> {
    profile: &'a str,
    cwd_policy: &'a str,
    env_allowlist: Option<&'a Vec<String>>,
    approved_escalation: bool,
    require_enforcement: bool,
    network: bool,
    writable_paths: &'a [String],
    inherited_ambient: bool,
}

fn mcp_sandbox_metadata_config(sandbox: Option<&SkillSandbox>) -> McpSandboxMetadataConfig<'_> {
    let profile = sandbox
        .map(|sandbox| sandbox.profile.as_str())
        .unwrap_or("readonly");
    let approved_escalation = sandbox
        .and_then(|sandbox| sandbox.approved_escalation)
        .unwrap_or(false);
    let env_allowlist = sandbox.and_then(|sandbox| sandbox.env_allowlist.as_ref());
    McpSandboxMetadataConfig {
        profile,
        cwd_policy: sandbox
            .and_then(|sandbox| sandbox.cwd_policy.as_ref().map(CwdPolicy::as_str))
            .unwrap_or("skill-directory"),
        env_allowlist,
        approved_escalation,
        require_enforcement: sandbox
            .and_then(|sandbox| sandbox.require_enforcement)
            .unwrap_or(false),
        network: sandbox.and_then(|sandbox| sandbox.network).unwrap_or(false),
        writable_paths: sandbox
            .map(|sandbox| sandbox.writable_paths.as_slice())
            .unwrap_or(&[]),
        inherited_ambient: env_allowlist.is_none()
            && profile == "unrestricted-local-dev"
            && approved_escalation,
    }
}

fn mcp_sandbox_location_metadata(
    config: &McpSandboxMetadataConfig<'_>,
    plan: &SandboxPlan,
    env: &BTreeMap<String, String>,
) -> Result<JsonObject, RuntimeError> {
    let mut metadata = JsonObject::new();
    metadata.insert(
        "profile".to_owned(),
        JsonValue::String(config.profile.to_owned()),
    );
    metadata.insert("cwd".to_owned(), JsonValue::String(path_string(&plan.cwd)));
    metadata.insert(
        "workspace_root".to_owned(),
        JsonValue::String(path_string(&workspace_root(env)?)),
    );
    metadata.insert(
        "cwd_policy".to_owned(),
        JsonValue::String(config.cwd_policy.to_owned()),
    );
    Ok(metadata)
}

fn mcp_sandbox_writable_paths_metadata(writable_paths: &[String]) -> JsonValue {
    JsonValue::Array(
        writable_paths
            .iter()
            .cloned()
            .map(JsonValue::String)
            .collect(),
    )
}

fn mcp_sandbox_approval_metadata(config: &McpSandboxMetadataConfig<'_>) -> JsonObject {
    [
        (
            "required".to_owned(),
            JsonValue::Bool(config.profile == "unrestricted-local-dev"),
        ),
        (
            "approved".to_owned(),
            JsonValue::Bool(config.approved_escalation),
        ),
    ]
    .into()
}

fn mcp_sandbox_env_metadata(
    env_allowlist: Option<&Vec<String>>,
    inherited_ambient: bool,
) -> JsonObject {
    if inherited_ambient {
        return [(
            "mode".to_owned(),
            JsonValue::String("ambient-inherited".to_owned()),
        )]
        .into();
    }

    let allowlist = env_allowlist
        .cloned()
        .unwrap_or_else(|| {
            DEFAULT_SANDBOX_ENV_ALLOWLIST
                .into_iter()
                .map(str::to_owned)
                .collect()
        })
        .into_iter()
        .map(JsonValue::String)
        .collect();
    [
        (
            "mode".to_owned(),
            JsonValue::String(if env_allowlist.is_some() {
                "allowlist".to_owned()
            } else {
                "default-allowlist".to_owned()
            }),
        ),
        ("allowlist".to_owned(), JsonValue::Array(allowlist)),
    ]
    .into()
}

fn mcp_sandbox_network_metadata(profile: &str, network: bool) -> JsonObject {
    [
        ("declared".to_owned(), JsonValue::Bool(network)),
        (
            "enforcement".to_owned(),
            JsonValue::String(if profile == "unrestricted-local-dev" {
                "host-ambient".to_owned()
            } else {
                "not-enforced-local".to_owned()
            }),
        ),
    ]
    .into()
}

fn mcp_sandbox_filesystem_metadata(profile: &str) -> JsonObject {
    [
        (
            "enforcement".to_owned(),
            JsonValue::String(if profile == "unrestricted-local-dev" {
                "host-ambient".to_owned()
            } else {
                "not-enforced-local".to_owned()
            }),
        ),
        (
            "readonly_paths".to_owned(),
            JsonValue::Bool(profile != "unrestricted-local-dev"),
        ),
        ("writable_paths_enforced".to_owned(), JsonValue::Bool(false)),
        ("private_tmp".to_owned(), JsonValue::Bool(false)),
    ]
    .into()
}

fn mcp_sandbox_runtime_metadata(profile: &str) -> JsonObject {
    if profile == "unrestricted-local-dev" {
        return [(
            "enforcer".to_owned(),
            JsonValue::String("direct".to_owned()),
        )]
        .into();
    }
    [
        (
            "enforcer".to_owned(),
            JsonValue::String("declared-policy-only".to_owned()),
        ),
        (
            "reason".to_owned(),
            JsonValue::String(format!(
                "local sandbox profile '{profile}' requires Linux bubblewrap or macOS sandbox-exec for filesystem and network enforcement"
            )),
        ),
    ]
    .into()
}

fn workspace_root(env: &BTreeMap<String, String>) -> Result<PathBuf, RuntimeError> {
    if let Some(path) = env.get("RUNX_CWD").or_else(|| env.get("INIT_CWD")) {
        return absolute_path(path);
    }
    std::env::current_dir().map_err(|source| RuntimeError::io("resolving workspace cwd", source))
}

fn absolute_path(path: &str) -> Result<PathBuf, RuntimeError> {
    let path = PathBuf::from(path);
    if path.is_absolute() {
        Ok(path)
    } else {
        Ok(std::env::current_dir()
            .map_err(|source| RuntimeError::io("resolving relative workspace cwd", source))?
            .join(path))
    }
}

fn path_string(path: &Path) -> String {
    path.components()
        .collect::<PathBuf>()
        .to_string_lossy()
        .replace('\\', "/")
}
