pub mod artifact;
pub mod block;
pub mod doc;
pub mod manifest;
pub mod page;
pub mod resource;

use crate::context::RequestContext;
use crate::error::WebError;
use serde::Deserialize;
use serde_json::Value;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolCallInput {
    pub tool_name: String,
    pub workspace_id: Option<String>,
    pub document_id: Option<String>,
    pub source_kind: Option<String>,
    pub root_uri: Option<String>,
    pub actor_id: Option<String>,
    pub profile: Option<String>,
    pub session_id: Option<String>,
    pub run_id: Option<String>,
    pub tool_call_id: Option<String>,
    pub trace_id: Option<String>,
    pub idempotency_key: Option<String>,
    pub dry_run: Option<bool>,
    pub capability_scope: Option<Vec<String>>,
    pub args: Option<Value>,
}

impl ToolCallInput {
    pub fn arg_string(&self, key: &str) -> Option<String> {
        self.args
            .as_ref()
            .and_then(|args| args.get(key))
            .and_then(Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
    }

    pub fn arg_value(&self, key: &str) -> Option<Value> {
        self.args.as_ref().and_then(|args| args.get(key)).cloned()
    }

    pub fn effective_workspace_id(&self) -> Option<String> {
        self.workspace_id
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
            .or_else(|| self.arg_string("workspaceId"))
    }

    pub fn effective_document_id(&self) -> Option<String> {
        self.document_id
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
            .or_else(|| self.arg_string("documentId"))
    }

    pub fn effective_source_kind(&self) -> Option<String> {
        self.source_kind
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
            .or_else(|| self.arg_string("sourceKind"))
    }

    pub fn effective_root_uri(&self) -> Option<String> {
        self.root_uri
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
            .or_else(|| self.arg_string("rootUri"))
    }

    pub fn effective_tool_call_id(&self) -> String {
        self.tool_call_id
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .unwrap_or("tool_call_missing")
            .to_string()
    }

    pub fn effective_trace_id<'a>(&'a self, fallback: &'a str) -> &'a str {
        self.trace_id
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .unwrap_or(fallback)
    }

    pub fn idempotency_key_or_default(&self, fallback: &str) -> String {
        self.idempotency_key
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .unwrap_or(fallback)
            .to_string()
    }

    pub fn has_idempotency_key(&self) -> bool {
        self.idempotency_key
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .is_some()
    }

    pub fn ai_access_scope(&self) -> Option<&Value> {
        self.args.as_ref().and_then(|args| {
            args.get("aiAccessScope")
                .or_else(|| args.get("ai_access_scope"))
        })
    }

    pub fn ai_access_permission_level(&self) -> Option<String> {
        self.ai_access_scope()
            .and_then(|scope| {
                scope
                    .get("permissionLevel")
                    .or_else(|| scope.get("permission_level"))
            })
            .and_then(Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
    }

    pub fn ai_access_scope_is_read_only(&self) -> bool {
        self.ai_access_permission_level()
            .map(|level| {
                let normalized = level.trim().to_ascii_lowercase();
                normalized == "read"
                    || normalized == "readonly"
                    || normalized == "read_only"
                    || normalized == "shared_read"
                    || (normalized.contains("read") && !normalized.contains("write"))
            })
            .unwrap_or(false)
    }

    pub fn command_context_bridge(&self) -> Option<CommandContextBridge> {
        let ctx = self.args.as_ref().and_then(|args| {
            args.get("commandContext")
                .or_else(|| args.get("command_context"))
        })?;
        let workspace_readonly = ctx
            .get("workspace.readonly")
            .or_else(|| ctx.get("workspaceReadonly"))
            .or_else(|| ctx.get("workspace_readonly"))
            .and_then(Value::as_bool)
            .unwrap_or(false);
        let ai_can_write = ctx
            .get("ai.canWrite")
            .or_else(|| ctx.get("aiCanWrite"))
            .or_else(|| ctx.get("ai_can_write"))
            .and_then(Value::as_bool)
            .unwrap_or(true);
        Some(CommandContextBridge {
            workspace_readonly,
            ai_can_write,
        })
    }
}

/// CommandContext 桥接信息，用于将 `core-protocol` 的 command context 引入 hermes_tools 写入守卫。
///
/// 当此桥接可用时，`ensure_write_authorized` 除检查 `ToolCallInput` 自带的
/// `aiAccessScope.permissionLevel` 外，额外检查 `ai_can_write` 和 `workspace_readonly`。
///
/// 设计意图：`CommandContext.ai.canWrite` / `workspace.readonly` 是从 tree/workspace/editor
/// 状态推导的写权限口径，与 `args.aiAccessScope.permissionLevel`（AI 客户端声明的权限）是
/// 两个独立的信息源。桥接合并两者，任一拒绝则阻止写入。
#[derive(Debug, Clone, Copy)]
pub struct CommandContextBridge {
    pub workspace_readonly: bool,
    pub ai_can_write: bool,
}

/// 统一的 hermes_tools 写入守卫。检查：
///
/// - `idempotencyKey` 必须存在
/// - `dryRun` 必须显式携带
/// - `aiAccessScope.permissionLevel` 不是只读（来自 ToolCallInput）
/// - 如果提供了 `bridge` 且 `ai_can_write == false`，拒绝
/// - 如果提供了 `bridge` 且 `workspace_readonly == true`，拒绝
///
/// 拒绝响应可解释（包含具体原因），不静默成功，不 panic。
pub fn ensure_write_authorized(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<(), WebError> {
    if !input.has_idempotency_key() {
        return Err(WebError::bad_request_code(
            "mnote_tool_idempotency_required",
            "写入型 mnote Hermes tool 必须携带 idempotencyKey",
        )
        .with_context(context));
    }
    if input.dry_run.is_none() {
        return Err(WebError::bad_request_code(
            "mnote_tool_dry_run_required",
            "写入型 mnote Hermes tool 必须显式携带 dryRun",
        )
        .with_context(context));
    }
    if input.ai_access_scope_is_read_only() {
        return Err(WebError::new(
            axum::http::StatusCode::FORBIDDEN,
            "mnote_tool_ai_scope_write_forbidden",
            "当前 AI scope 是只读权限，禁止执行写入型 mnote tool",
        )
        .with_context(context));
    }
    if let Some(bridge) = input.command_context_bridge() {
        if bridge.workspace_readonly {
            return Err(WebError::new(
                axum::http::StatusCode::FORBIDDEN,
                "mnote_tool_workspace_readonly",
                "当前 workspace 是只读权限，禁止执行写入型 mnote tool",
            )
            .with_context(context));
        }
        if !bridge.ai_can_write {
            return Err(WebError::new(
                axum::http::StatusCode::FORBIDDEN,
                "mnote_tool_ai_write_forbidden",
                "CommandContext 标记 AI 写入未授权，禁止执行写入型 mnote tool",
            )
            .with_context(context));
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::http::{HeaderMap, Method};
    use serde_json::json;

    fn context() -> RequestContext {
        RequestContext::from_http_parts(
            &Method::POST,
            &"/api/hermes/tools".parse().expect("uri"),
            &HeaderMap::new(),
        )
    }

    fn write_input(args: Value) -> ToolCallInput {
        ToolCallInput {
            tool_name: "mnote.page.save".into(),
            workspace_id: Some("ws_1".into()),
            document_id: Some("doc_1".into()),
            source_kind: Some("local_folder".into()),
            root_uri: Some("file:///tmp/mnote".into()),
            actor_id: Some("user_1".into()),
            profile: None,
            session_id: Some("sess_1".into()),
            run_id: Some("run_1".into()),
            tool_call_id: Some("tool_1".into()),
            trace_id: Some("trace_1".into()),
            idempotency_key: Some("idem_1".into()),
            dry_run: Some(false),
            capability_scope: None,
            args: Some(args),
        }
    }

    #[test]
    fn ensure_write_authorized_rejects_command_context_ai_cannot_write() {
        let error = ensure_write_authorized(
            &context(),
            &write_input(json!({
                "commandContext": {
                    "ai.canWrite": false,
                    "workspace.readonly": false
                }
            })),
        )
        .expect_err("ai.canWrite=false should reject write tools");

        assert_eq!(error.status(), axum::http::StatusCode::FORBIDDEN);
        assert_eq!(error.code(), "mnote_tool_ai_write_forbidden");
    }

    #[test]
    fn ensure_write_authorized_rejects_command_context_readonly_workspace() {
        let error = ensure_write_authorized(
            &context(),
            &write_input(json!({
                "commandContext": {
                    "ai.canWrite": true,
                    "workspace.readonly": true
                }
            })),
        )
        .expect_err("workspace.readonly=true should reject write tools");

        assert_eq!(error.status(), axum::http::StatusCode::FORBIDDEN);
        assert_eq!(error.code(), "mnote_tool_workspace_readonly");
    }
}
