use anyhow::{anyhow, bail, Result};
use serde_json::{json, Value};
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
use walkdir::WalkDir;

use crate::{contract, runtime};

pub struct SddMcpBackend {
    pub root: PathBuf,
}

impl runtime::mcp::McpBackend for SddMcpBackend {
    fn call_tool(&self, name: &str, arguments: &Value) -> Result<Value> {
        match name {
            "sdd_trace_list" => {
                let mut events = runtime::trace::read_events(&self.root)?;
                if let Some(orchestration) = string_arg(arguments, "orchestration") {
                    events = runtime::trace::filter_by_orchestration(events, &orchestration);
                }
                Ok(json!({ "events": events }))
            }
            "sdd_trace_show" => {
                let run_id = required_string_arg(arguments, "run_id")?;
                Ok(serde_json::to_value(runtime::trace::trace_tree(
                    &self.root, &run_id,
                )?)?)
            }
            "sdd_trace_summary" => {
                let mut events = runtime::trace::read_events(&self.root)?;
                if let Some(orchestration) = string_arg(arguments, "orchestration") {
                    events = runtime::trace::filter_by_orchestration(events, &orchestration);
                }
                Ok(serde_json::to_value(runtime::trace::summary(&events))?)
            }
            "sdd_artifact_status" => {
                let orchestration = string_arg(arguments, "orchestration")
                    .unwrap_or_else(|| "sdd-orchestration".to_string());
                crate::artifact_status_value(&self.root, &orchestration)
            }
            "sdd_context_build" => {
                let orchestration = required_string_arg(arguments, "orchestration")?;
                let stage = required_string_arg(arguments, "stage")?;
                let task = string_arg(arguments, "task");
                crate::sdd_mcp_context_build_value(
                    &self.root,
                    &orchestration,
                    &stage,
                    task.as_deref(),
                )
            }
            "sdd_context_bundle" => {
                let orchestration = required_string_arg(arguments, "orchestration")?;
                let stage = required_string_arg(arguments, "stage")?;
                let task = string_arg(arguments, "task");
                let query = string_arg(arguments, "query");
                crate::sdd_mcp_context_bundle_value(
                    &self.root,
                    &orchestration,
                    &stage,
                    task.as_deref(),
                    query.as_deref(),
                )
            }
            "sdd_clients_doctor" => Ok(crate::sdd_mcp_clients_doctor_value(&self.root)),
            "sdd_project_status" => crate::sdd_mcp_project_status_value(&self.root),
            "sdd_search" => {
                let query = string_arg(arguments, "query").unwrap_or_default();
                let limit = usize_arg(arguments, "limit").unwrap_or(10);
                crate::sdd_mcp_search_value(&self.root, &query, limit)
            }
            "sdd_runtime_adapters" => runtime_adapters_value(),
            "sdd_optimize_status" => crate::sdd_mcp_optimize_status_value(&self.root),
            "sdd_context_handoff" => {
                let orchestration = required_string_arg(arguments, "orchestration")?;
                let stage = required_string_arg(arguments, "stage")?;
                let task = string_arg(arguments, "task");
                crate::sdd_mcp_context_handoff_value(
                    &self.root,
                    &orchestration,
                    &stage,
                    task.as_deref(),
                )
            }
            "sdd_capabilities_status" => crate::sdd_mcp_capabilities_status_value(&self.root),
            "sdd_agents_manifest" => {
                let agent_id =
                    string_arg(arguments, "agent_id").unwrap_or_else(|| "sdd-orchestrator".into());
                crate::sdd_mcp_agents_manifest_value(&agent_id)
            }
            other => bail!("unknown SDD MCP tool `{other}`"),
        }
    }

    fn list_resources(&self) -> Result<Vec<Value>> {
        let mut resources = vec![
            resource(
                "sdd://runtime/adapters",
                "runtime/adapters",
                "SDD runtime adapters",
                "Optional MCP/provider adapter metadata for this sdd binary",
                "application/json",
            ),
            resource(
                "sdd://optimization/status",
                "optimization/status",
                "SDD optimization status",
                "Read optimization wrapper status for CodeGraph, RTK and Caveman",
                "application/json",
            ),
            resource(
                "sdd://capabilities/catalog",
                "capabilities/catalog",
                "SDD capability catalog",
                "Read capability catalog and doctor status",
                "application/json",
            ),
            resource(
                "sdd://agents/sdd-orchestrator",
                "agents/sdd-orchestrator",
                "SDD orchestrator agent manifest",
                "Read the canonical SDD agent manifest",
                "application/json",
            ),
        ];
        let docs = self.root.join("docs");
        if docs.exists() {
            for entry in WalkDir::new(&docs)
                .max_depth(2)
                .into_iter()
                .filter_map(|entry| entry.ok())
                .filter(|entry| entry.file_type().is_file())
            {
                let path = entry.path();
                let Some(parent) = path
                    .parent()
                    .and_then(|path| path.file_name())
                    .and_then(OsStr::to_str)
                else {
                    continue;
                };
                let Some(stage) = path
                    .file_stem()
                    .and_then(OsStr::to_str)
                    .and_then(contract::stage_by_filename_stem)
                else {
                    continue;
                };
                resources.push(resource(
                    &format!("sdd://artifact/{parent}/{}", stage.key),
                    &format!("{parent}/{}", stage.filename),
                    &stage.label,
                    "Local SDD artifact",
                    "text/markdown",
                ));
                resources.push(resource(
                    &format!("sdd://context/{parent}/{}", stage.key),
                    &format!("{parent}/context/{}", stage.key),
                    "SDD context pack",
                    "Read a generated context pack for one stage",
                    "text/markdown",
                ));
                resources.push(resource(
                    &format!("sdd://handoff/{parent}/{}", stage.key),
                    &format!("{parent}/handoff/{}", stage.key),
                    "SDD context handoff",
                    "Read a context handoff derived from optimization and artifacts",
                    "application/json",
                ));
                resources.push(resource(
                    &format!("sdd://context-bundle/{parent}/{}", stage.key),
                    &format!("{parent}/context-bundle/{}", stage.key),
                    "SDD context bundle",
                    "Read the primary MCP context bundle combining artifacts, handoff, traces, capabilities, runtime adapters and CodeGraph guidance",
                    "application/json",
                ));
            }
        }
        for run_id in runtime::trace::summary(&runtime::trace::read_events(&self.root)?).roots {
            resources.push(resource(
                &format!("sdd://trace/{run_id}"),
                &run_id,
                "SDD trace tree",
                "Read an SDD trace tree as JSON",
                "application/json",
            ));
        }
        Ok(resources)
    }

    fn read_resource(&self, uri: &str) -> Result<runtime::mcp::McpResource> {
        let segments = runtime::trace::validate_sdd_uri(uri)?;
        match segments.as_slice() {
            [kind, orchestration, artifact] if kind == "artifact" => {
                let stage = contract::stage_by_key(artifact)
                    .or_else(|| contract::stage_by_command(artifact))
                    .ok_or_else(|| anyhow!("unknown artifact `{artifact}`"))?;
                let path = crate::artifact_dir(&self.root, orchestration).join(&stage.filename);
                let text = fs::read_to_string(&path)?;
                Ok(mcp_resource(uri, "text/markdown", text))
            }
            [kind, run_id] if kind == "trace" => {
                let tree = runtime::trace::trace_tree(&self.root, run_id)?;
                Ok(mcp_resource(
                    uri,
                    "application/json",
                    serde_json::to_string_pretty(&tree)?,
                ))
            }
            [kind, orchestration, stage] if kind == "context" => {
                let text = crate::sdd_mcp_context_resource_text(&self.root, orchestration, stage)?;
                Ok(mcp_resource(uri, "text/markdown", text))
            }
            [kind, orchestration, stage] if kind == "context-bundle" => Ok(mcp_resource(
                uri,
                "application/json",
                serde_json::to_string_pretty(&crate::sdd_mcp_context_bundle_value(
                    &self.root,
                    orchestration,
                    stage,
                    None,
                    None,
                )?)?,
            )),
            [kind, orchestration, stage] if kind == "handoff" => Ok(mcp_resource(
                uri,
                "application/json",
                serde_json::to_string_pretty(&crate::sdd_mcp_context_handoff_value(
                    &self.root,
                    orchestration,
                    stage,
                    None,
                )?)?,
            )),
            [kind, resource] if kind == "runtime" && resource == "adapters" => Ok(mcp_resource(
                uri,
                "application/json",
                serde_json::to_string_pretty(&runtime_adapters_value()?)?,
            )),
            [kind, resource] if kind == "optimization" && resource == "status" => Ok(mcp_resource(
                uri,
                "application/json",
                serde_json::to_string_pretty(&crate::sdd_mcp_optimize_status_value(&self.root)?)?,
            )),
            [kind, resource] if kind == "capabilities" && resource == "catalog" => {
                Ok(mcp_resource(
                    uri,
                    "application/json",
                    serde_json::to_string_pretty(&crate::sdd_mcp_capabilities_status_value(
                        &self.root,
                    )?)?,
                ))
            }
            [kind, agent_id] if kind == "agents" => Ok(mcp_resource(
                uri,
                "application/json",
                serde_json::to_string_pretty(&crate::sdd_mcp_agents_manifest_value(agent_id)?)?,
            )),
            _ => bail!("unsupported SDD resource URI `{uri}`"),
        }
    }

    fn get_prompt(&self, name: &str, arguments: &Value) -> Result<Value> {
        let text = match name {
            "sdd_orchestration" => {
                let idea = required_string_arg(arguments, "idea")?;
                format!(
                    "Conduza a orquestração SDD para a ideia abaixo. Use `sdd orchestration`, preserve checkpoints humanos, salve artefatos em docs/<slug>/ e consulte `sdd_trace_summary` quando precisar de observabilidade.\n\nIdeia:\n{idea}"
                )
            }
            "sdd_stage_handoff" => {
                let orchestration = required_string_arg(arguments, "orchestration")?;
                let stage = required_string_arg(arguments, "stage")?;
                format!(
                    "Prepare o handoff da etapa `{stage}` da orquestração `{orchestration}`. Inclua origem, estado, evidências, riscos, próximo artefato e comandos `sdd trace` úteis."
                )
            }
            "sdd_trace_review" => {
                let run_id = required_string_arg(arguments, "run_id")?;
                format!(
                    "Revise a árvore de trace `{run_id}`. Procure falhas, falta de evidência, eventos sem parent_run_id quando deveriam ser filhos e riscos de segredo/transcript bruto."
                )
            }
            other => bail!("unknown SDD prompt `{other}`"),
        };
        Ok(json!({
            "description": name,
            "messages": [{ "role": "user", "content": { "type": "text", "text": text } }]
        }))
    }
}

fn runtime_adapters_value() -> Result<Value> {
    Ok(json!({
        "adapters": runtime::mcp::runtime_adapters(),
        "rmcp_server_info": runtime::mcp::rmcp_server_info_json()?,
    }))
}

fn resource(uri: &str, name: &str, title: &str, description: &str, mime_type: &str) -> Value {
    json!({
        "uri": uri,
        "name": name,
        "title": title,
        "description": description,
        "mimeType": mime_type
    })
}

fn mcp_resource(uri: &str, mime_type: &str, text: String) -> runtime::mcp::McpResource {
    runtime::mcp::McpResource {
        uri: uri.to_string(),
        mime_type: mime_type.to_string(),
        text,
    }
}

fn string_arg(arguments: &Value, key: &str) -> Option<String> {
    arguments
        .get(key)
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
}

fn required_string_arg(arguments: &Value, key: &str) -> Result<String> {
    string_arg(arguments, key).ok_or_else(|| anyhow!("missing required argument `{key}`"))
}

fn usize_arg(arguments: &Value, key: &str) -> Option<usize> {
    arguments
        .get(key)
        .and_then(|value| {
            value
                .as_u64()
                .map(|number| number as usize)
                .or_else(|| value.as_str().and_then(|text| text.parse::<usize>().ok()))
        })
        .filter(|value| *value > 0)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::runtime::mcp::McpBackend;
    use tempfile::tempdir;

    #[test]
    fn exposes_agent_manifest_tool_and_resource() {
        let root = tempdir().unwrap();
        let backend = SddMcpBackend {
            root: root.path().to_path_buf(),
        };

        let value = backend
            .call_tool("sdd_agents_manifest", &json!({}))
            .expect("agent manifest tool should be readable");
        assert_eq!(value["manifest"]["id"], "sdd-orchestrator");
        assert_eq!(
            value["canonical_source"],
            ".agents/agents/sdd-orchestrator/AGENT.md"
        );

        let resource = backend
            .read_resource("sdd://agents/sdd-orchestrator")
            .expect("agent manifest resource should be readable");
        assert_eq!(resource.mime_type, "application/json");
        assert!(resource.text.contains("sdd-orchestrator"));
    }

    #[test]
    fn exposes_project_status_and_search_tools() {
        let root = tempdir().unwrap();
        let docs = root.path().join("docs/demo");
        fs::create_dir_all(&docs).unwrap();
        fs::write(docs.join("02-prd.md"), "# PRD\n\nMCP clean e leve").unwrap();
        fs::write(
            root.path().join("AGENTS.md"),
            "# AGENTS\n\nRoteamento de Contexto SDD clean",
        )
        .unwrap();
        fs::write(
            root.path().join("sdd.config.yaml"),
            "project:\n  name: demo\nsystems: {}\npaths: {}\ncommands: {}\nquality: {}\nrisk_policy: {}\nadapters: {}\nartifact_store: {}\n",
        )
        .unwrap();
        let backend = SddMcpBackend {
            root: root.path().to_path_buf(),
        };

        let status = backend
            .call_tool("sdd_project_status", &json!({}))
            .expect("project status should be readable");
        assert_eq!(status["derived_cache"], ".sdd/cache/sdd.sqlite");
        assert_eq!(status["agent_contexts"]["policy"], "root_only");

        let search = backend
            .call_tool("sdd_search", &json!({"query": "clean", "limit": "5"}))
            .expect("search should use cache or fallback");
        assert_eq!(search["query"], "clean");
        assert!(search["results"].as_array().unwrap().iter().any(|item| {
            item["kind"].as_str() == Some("context") && item["path"].as_str() == Some("AGENTS.md")
        }));
    }

    #[test]
    fn exposes_primary_context_bundle_tool_and_resource() {
        let root = tempdir().unwrap();
        let docs = root.path().join("docs/demo");
        fs::create_dir_all(&docs).unwrap();
        fs::write(
            docs.join("traceability-map.yaml"),
            "name: Demo\nstages:\n  prd:\n    state: approved\n  techspec:\n    state: approved\n",
        )
        .unwrap();
        fs::write(
            docs.join("02-prd.md"),
            "# PRD\n\nMCP como contexto principal.",
        )
        .unwrap();
        fs::write(
            docs.join("03-techspec.md"),
            "# Tech Spec\n\n## Decisões\n\n- Contexto MCP: bundle primário read-only.\n",
        )
        .unwrap();
        fs::write(
            root.path().join("sdd.config.yaml"),
            "project:\n  name: demo\nsystems: {}\npaths: {}\ncommands: {}\nquality: {}\nrisk_policy: {}\nadapters: {}\nartifact_store: {}\n",
        )
        .unwrap();
        let backend = SddMcpBackend {
            root: root.path().to_path_buf(),
        };

        let value = backend
            .call_tool(
                "sdd_context_bundle",
                &json!({"orchestration": "Demo", "stage": "execution", "task": "T-01"}),
            )
            .expect("context bundle should be readable");
        assert_eq!(value["mcp_policy"]["role"], "primary_read_surface");
        assert_eq!(value["mcp_policy"]["read_only"], true);
        assert!(value["context_pack"]["content"]
            .as_str()
            .unwrap()
            .contains("Context Pack"));
        assert_eq!(
            value["codegraph_companion"]["recommended_mcp_calls"][1]["tool"],
            "codegraph_context"
        );

        let resource = backend
            .read_resource("sdd://context-bundle/demo/execution")
            .expect("context bundle resource should be readable");
        assert_eq!(resource.mime_type, "application/json");
        assert!(resource.text.contains("primary_read_surface"));

        let resources = backend
            .list_resources()
            .expect("resources list should work");
        assert!(resources
            .iter()
            .any(|item| item["uri"].as_str() == Some("sdd://context-bundle/demo/prd")));
        assert!(resources
            .iter()
            .any(|item| item["uri"].as_str() == Some("sdd://context/demo/prd")));
    }

    #[test]
    fn context_bundle_degrades_trace_errors_to_warning_status() {
        let root = tempdir().unwrap();
        let docs = root.path().join("docs/demo");
        fs::create_dir_all(&docs).unwrap();
        fs::write(
            docs.join("traceability-map.yaml"),
            "name: Demo\nstages:\n  prd:\n    state: approved\n  techspec:\n    state: approved\n",
        )
        .unwrap();
        fs::write(docs.join("02-prd.md"), "# PRD\n\nContexto MCP.").unwrap();
        fs::write(docs.join("03-techspec.md"), "# Tech Spec\n\nPlano técnico.").unwrap();
        fs::write(
            root.path().join("sdd.config.yaml"),
            "project:\n  name: demo\nsystems: {}\npaths: {}\ncommands: {}\nquality: {}\nrisk_policy: {}\nadapters: {}\nartifact_store: {}\n",
        )
        .unwrap();
        fs::create_dir_all(root.path().join(".sdd/events.jsonl")).unwrap();
        let backend = SddMcpBackend {
            root: root.path().to_path_buf(),
        };

        let value = backend
            .call_tool(
                "sdd_context_bundle",
                &json!({"orchestration": "Demo", "stage": "execution"}),
            )
            .expect("trace read errors should not fail the context bundle");

        assert_eq!(value["status"], "warn");
        assert_eq!(value["trace_summary"]["status"], "warn");
        assert!(value["trace_summary"]["error"]
            .as_str()
            .unwrap()
            .contains("events.jsonl"));
    }
}
