use crate::context::RequestContext;
use crate::error::WebError;
use crate::hermes_tools::ToolCallInput;
use axum::http::StatusCode;
use serde_json::{json, Value};
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};

pub async fn mindmap_fetch(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    let target = ResourceToolTarget::from_input(context, input, "mindmap", "mindmapId")?;
    ensure_resource_scope_allowed(context, input, &target)?;
    let path = target.resolve_read_path(context, input)?;
    let content = fs::read_to_string(&path).map_err(|error| {
        WebError::bad_request_code(
            "mnote_resource_read_failed",
            format!("无法读取 mindmap resource: {error}"),
        )
        .with_context(context)
    })?;
    let data =
        serde_json::from_str::<Value>(&content).unwrap_or_else(|_| json!({ "raw": content }));
    let nodes = collect_mindmap_nodes(&data);
    let scope = input
        .arg_string("scope")
        .unwrap_or_else(|| "tree".into())
        .to_ascii_lowercase();
    Ok(json!({
        "objectIdentity": target.object_identity,
        "resourceKind": "mindmap",
        "documentId": target.document_id,
        "mindmapId": target.resource_id,
        "resourcePath": target.resource_path,
        "scope": scope,
        "nodes": nodes,
        "edges": [],
        "markdownSummary": mindmap_markdown_summary(&nodes),
        "revision": file_revision(&path),
        "source": "local_folder"
    }))
}

pub async fn mindmap_apply_ops(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    ensure_resource_write_contract(context, input)?;
    let target = ResourceToolTarget::from_input(context, input, "mindmap", "mindmapId")?;
    ensure_resource_scope_allowed(context, input, &target)?;
    let path = target.resolve_write_path(context, input)?;
    let ops = input.arg_value("ops").ok_or_else(|| {
        WebError::bad_request_code("mnote_resource_ops_required", "mindmap 写工具缺少 ops")
            .with_context(context)
    })?;
    if input.dry_run.unwrap_or(false) {
        return Ok(json!({
            "dryRun": true,
            "commandName": "mnote.mindmap.apply_ops",
            "objectIdentity": target.object_identity,
            "resourceKind": "mindmap",
            "documentId": target.document_id,
            "mindmapId": target.resource_id,
            "resourcePath": target.resource_path,
            "diff": [{"op": "mindmap.apply_ops", "ops": ops}]
        }));
    }
    Err(WebError::bad_request_code(
        "mnote_resource_native_patch_required",
        format!(
            "本地 mindmap resource 写入请使用 agent 原生 patch 编辑授权文件；已校验可写资源路径 {}",
            path.display()
        ),
    )
    .with_context(context))
}

pub async fn office_fetch_summary(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    let target = ResourceToolTarget::from_input(context, input, "onlyoffice", "assetId")?;
    ensure_resource_scope_allowed(context, input, &target)?;
    let path = target.resolve_read_path(context, input)?;
    let metadata = fs::metadata(&path).map_err(|error| {
        WebError::bad_request_code(
            "mnote_resource_metadata_failed",
            format!("无法读取 Office resource 元数据: {error}"),
        )
        .with_context(context)
    })?;
    let file_name = path
        .file_name()
        .and_then(|value| value.to_str())
        .unwrap_or(target.resource_id.as_str());
    Ok(json!({
        "objectIdentity": target.object_identity,
        "resourceKind": "only_office",
        "documentId": target.document_id,
        "assetId": target.resource_id,
        "resourcePath": target.resource_path,
        "fileName": file_name,
        "mimeType": office_mime_type(&path),
        "textSummary": office_text_preview(&path),
        "outline": [],
        "revision": file_revision(&path),
        "size": metadata.len(),
        "source": "local_folder"
    }))
}

pub async fn office_propose_changes(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    let target = ResourceToolTarget::from_input(context, input, "onlyoffice", "assetId")?;
    ensure_resource_scope_allowed(context, input, &target)?;
    let path = target.resolve_write_path(context, input)?;
    let instructions = input.arg_string("instructions").ok_or_else(|| {
        WebError::bad_request_code(
            "mnote_resource_instructions_required",
            "office 建议工具缺少 instructions",
        )
        .with_context(context)
    })?;
    Ok(json!({
        "dryRun": input.dry_run.unwrap_or(false),
        "commandName": "mnote.office.propose_changes",
        "objectIdentity": target.object_identity,
        "resourceKind": "only_office",
        "documentId": target.document_id,
        "assetId": target.resource_id,
        "resourcePath": target.resource_path,
        "basisRevision": input.arg_value("basisRevision"),
        "revision": file_revision(&path),
        "changeSummary": instructions,
        "suggestedEdits": [{
            "kind": "instruction",
            "text": instructions
        }],
        "requiresOnlyOffice": true,
        "requiresOfficeCli": true,
        "writesBinary": false
    }))
}

struct ResourceToolTarget {
    document_id: String,
    resource_id: String,
    object_identity: String,
    resource_path: Option<String>,
}

impl ResourceToolTarget {
    fn from_input(
        context: &RequestContext,
        input: &ToolCallInput,
        resource_kind: &str,
        resource_id_arg: &str,
    ) -> Result<Self, WebError> {
        let document_id = input.effective_document_id().ok_or_else(|| {
            WebError::bad_request_code(
                "mnote_resource_document_required",
                "资源工具缺少 documentId",
            )
            .with_context(context)
        })?;
        let resource_id = input.arg_string(resource_id_arg).ok_or_else(|| {
            WebError::bad_request_code(
                "mnote_resource_id_required",
                format!("资源工具缺少 {resource_id_arg}"),
            )
            .with_context(context)
        })?;
        let object_identity = format!("resource:{resource_kind}:{document_id}:{resource_id}");
        Ok(Self {
            document_id,
            resource_id,
            object_identity,
            resource_path: input.arg_string("resourcePath"),
        })
    }

    fn resolve_read_path(
        &self,
        context: &RequestContext,
        input: &ToolCallInput,
    ) -> Result<PathBuf, WebError> {
        let root_uri = local_root_uri_for_resource(input).ok_or_else(|| {
            WebError::new(
                StatusCode::FORBIDDEN,
                "mnote_resource_root_uri_required",
                "资源工具需要授权 rootUri",
            )
            .with_context(context)
        })?;
        let relative = self.relative_path();
        crate::routes::ensure_local_path_read_access(context, &root_uri, &relative)
            .map_err(|error| error.with_context(context))
    }

    fn resolve_write_path(
        &self,
        context: &RequestContext,
        input: &ToolCallInput,
    ) -> Result<PathBuf, WebError> {
        let root_uri = local_root_uri_for_resource(input).ok_or_else(|| {
            WebError::new(
                StatusCode::FORBIDDEN,
                "mnote_resource_root_uri_required",
                "资源工具需要授权 rootUri",
            )
            .with_context(context)
        })?;
        let root = crate::routes::ensure_local_workspace_access(context, &root_uri)
            .map_err(|error| error.with_context(context))?;
        let relative = self.relative_path();
        let path = root.join(relative);
        let canonical = path.canonicalize().map_err(|error| {
            WebError::bad_request_code(
                "mnote_resource_unavailable",
                format!("无法访问资源文件: {error}"),
            )
            .with_context(context)
        })?;
        if !canonical.starts_with(root) {
            return Err(WebError::bad_request_code(
                "mnote_resource_root_escape",
                "资源工具不能越过授权目录",
            )
            .with_context(context));
        }
        Ok(canonical)
    }

    fn relative_path(&self) -> String {
        self.resource_path
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
            .unwrap_or_else(|| self.resource_id.clone())
    }
}

fn ensure_resource_scope_allowed(
    context: &RequestContext,
    input: &ToolCallInput,
    target: &ResourceToolTarget,
) -> Result<(), WebError> {
    let Some(scope) = input.arg_value("aiAccessScope") else {
        return Ok(());
    };
    let allowed = scope
        .get("allowedResourceIds")
        .or_else(|| scope.get("allowed_resource_ids"))
        .and_then(Value::as_array)
        .map(|values| {
            values
                .iter()
                .filter_map(Value::as_str)
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(ToOwned::to_owned)
                .collect::<HashSet<_>>()
        })
        .unwrap_or_default();
    if allowed.is_empty()
        || allowed.contains(&target.resource_id)
        || allowed.contains(&target.object_identity)
    {
        return Ok(());
    }
    Err(WebError::new(
        StatusCode::FORBIDDEN,
        "mnote_resource_ai_scope_forbidden",
        "当前 AI scope 不允许访问该资源",
    )
    .with_context(context))
}

fn ensure_resource_write_contract(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<(), WebError> {
    crate::hermes_tools::ensure_write_authorized(context, input)
}

fn local_root_uri_for_resource(input: &ToolCallInput) -> Option<String> {
    input.effective_root_uri().or_else(|| {
        input
            .arg_value("aiAccessScope")
            .and_then(|scope| {
                scope
                    .get("allowedRoots")
                    .or_else(|| scope.get("allowed_roots"))
                    .cloned()
            })
            .and_then(|allowed_roots| {
                allowed_roots.as_array().and_then(|roots| {
                    roots
                        .iter()
                        .filter_map(|root| {
                            root.get("rootUri")
                                .or_else(|| root.get("root_uri"))
                                .and_then(Value::as_str)
                        })
                        .map(str::trim)
                        .find(|root_uri| !root_uri.is_empty())
                        .map(ToOwned::to_owned)
                })
            })
    })
}

fn collect_mindmap_nodes(value: &Value) -> Vec<Value> {
    let mut nodes = Vec::new();
    collect_mindmap_nodes_inner(value, &mut nodes);
    nodes
}

fn collect_mindmap_nodes_inner(value: &Value, nodes: &mut Vec<Value>) {
    let text = value
        .pointer("/data/text")
        .or_else(|| value.get("text"))
        .and_then(Value::as_str)
        .unwrap_or("");
    let id = value
        .pointer("/data/uid")
        .or_else(|| value.pointer("/data/id"))
        .or_else(|| value.get("uid"))
        .or_else(|| value.get("id"))
        .and_then(Value::as_str)
        .unwrap_or("root");
    if !text.is_empty() {
        nodes.push(json!({ "id": id, "text": text }));
    }
    if let Some(children) = value.get("children").and_then(Value::as_array) {
        for child in children {
            collect_mindmap_nodes_inner(child, nodes);
        }
    }
}

fn mindmap_markdown_summary(nodes: &[Value]) -> String {
    nodes
        .iter()
        .filter_map(|node| node.get("text").and_then(Value::as_str))
        .take(20)
        .map(|text| format!("- {text}"))
        .collect::<Vec<_>>()
        .join("\n")
}

fn file_revision(path: &Path) -> Value {
    let Ok(metadata) = fs::metadata(path) else {
        return Value::Null;
    };
    let modified_ms = metadata
        .modified()
        .ok()
        .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|duration| duration.as_millis())
        .unwrap_or_default();
    json!(format!("{}:{}", metadata.len(), modified_ms))
}

fn office_mime_type(path: &Path) -> &'static str {
    match path
        .extension()
        .and_then(|value| value.to_str())
        .unwrap_or("")
    {
        "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
        _ => "application/octet-stream",
    }
}

fn office_text_preview(path: &Path) -> Value {
    match fs::read_to_string(path) {
        Ok(text) => json!(text.chars().take(4000).collect::<String>()),
        Err(_) => Value::Null,
    }
}
