use anyhow::{anyhow, Result};
use serde::Serialize;
use serde_json::{json, Value};
use std::io::{self, BufRead, Write};

pub const MCP_PROTOCOL_VERSION: &str = "2025-06-18";

#[derive(Clone, Debug, Serialize)]
pub struct RuntimeAdapterDefinition {
    pub id: &'static str,
    pub kind: &'static str,
    pub crate_name: &'static str,
    pub feature: &'static str,
    pub enabled: bool,
    pub version_requirement: &'static str,
    pub role: &'static str,
    pub fallback: &'static str,
    pub capabilities: &'static [&'static str],
}

#[derive(Debug)]
pub struct McpResource {
    pub uri: String,
    pub mime_type: String,
    pub text: String,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct McpToolDefinition {
    pub name: &'static str,
    pub title: &'static str,
    pub description: &'static str,
    pub input_schema: Value,
    pub annotations: Value,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct McpResourceTemplateDefinition {
    pub uri_template: &'static str,
    pub name: &'static str,
    pub title: &'static str,
    pub description: &'static str,
    pub mime_type: &'static str,
}

#[derive(Clone, Debug, Serialize)]
pub struct McpPromptDefinition {
    pub name: &'static str,
    pub title: &'static str,
    pub description: &'static str,
    pub arguments: Value,
}

pub trait McpBackend {
    fn call_tool(&self, name: &str, arguments: &Value) -> Result<Value>;
    fn list_resources(&self) -> Result<Vec<Value>>;
    fn read_resource(&self, uri: &str) -> Result<McpResource>;
    fn get_prompt(&self, name: &str, arguments: &Value) -> Result<Value>;
}

pub fn serve_stdio<B: McpBackend>(backend: &B) -> Result<()> {
    let stdin = io::stdin();
    let mut stdout = io::stdout();
    serve_lines(stdin.lock(), &mut stdout, backend)
}

pub fn serve_lines<R: BufRead, W: Write, B: McpBackend>(
    reader: R,
    writer: &mut W,
    backend: &B,
) -> Result<()> {
    for line in reader.lines() {
        let line = line?;
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        let request: Value = match serde_json::from_str(trimmed) {
            Ok(value) => value,
            Err(error) => {
                writeln!(
                    writer,
                    "{}",
                    jsonrpc_error(Value::Null, -32700, &error.to_string())
                )?;
                continue;
            }
        };
        if let Some(response) = handle_request(backend, request) {
            writeln!(writer, "{}", serde_json::to_string(&response)?)?;
            writer.flush()?;
        }
    }
    Ok(())
}

fn handle_request<B: McpBackend>(backend: &B, request: Value) -> Option<Value> {
    let id = request.get("id").cloned().unwrap_or(Value::Null);
    let method = request.get("method").and_then(Value::as_str).unwrap_or("");
    if id.is_null() && method.starts_with("notifications/") {
        return None;
    }
    let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
    let result = match method {
        "initialize" => Ok(initialize_result(&params)),
        "tools/list" => Ok(json!({ "tools": tool_definitions() })),
        "tools/call" => call_tool(backend, &params),
        "resources/list" => backend
            .list_resources()
            .map(|resources| json!({ "resources": resources })),
        "resources/templates/list" => Ok(json!({ "resourceTemplates": resource_templates() })),
        "resources/read" => read_resource(backend, &params),
        "prompts/list" => Ok(json!({ "prompts": prompt_definitions() })),
        "prompts/get" => get_prompt(backend, &params),
        _ => Err(anyhow!("method not found: {method}")),
    };
    Some(match result {
        Ok(result) => json!({ "jsonrpc": "2.0", "id": id, "result": result }),
        Err(error) => jsonrpc_error(id, -32603, &error.to_string()),
    })
}

fn initialize_result(params: &Value) -> Value {
    let protocol_version = params
        .get("protocolVersion")
        .and_then(Value::as_str)
        .unwrap_or(MCP_PROTOCOL_VERSION);
    json!({
        "protocolVersion": protocol_version,
        "capabilities": {
            "tools": { "listChanged": false },
            "resources": { "listChanged": false },
            "prompts": { "listChanged": false }
        },
        "serverInfo": {
            "name": "sdd-layer",
            "version": env!("CARGO_PKG_VERSION")
        }
    })
}

pub fn runtime_adapters() -> Vec<RuntimeAdapterDefinition> {
    vec![
        RuntimeAdapterDefinition {
            id: "rmcp",
            kind: "mcp-sdk",
            crate_name: "rmcp",
            feature: "mcp-rmcp",
            enabled: cfg!(feature = "mcp-rmcp"),
            version_requirement: "^1.7",
            role: "Official Rust MCP SDK compatibility layer for typed server metadata.",
            fallback: "manual-jsonrpc-stdio",
            capabilities: &["tools", "resources", "prompts", "stdio"],
        },
        RuntimeAdapterDefinition {
            id: "rig",
            kind: "provider-tool-adapter",
            crate_name: "rig-core",
            feature: "rig-adapter",
            enabled: cfg!(feature = "rig-adapter"),
            version_requirement: "^0.38",
            role: "Optional provider/tool abstraction for future direct model and tool execution.",
            fallback: "provider-registry-and-local-cli-adapters",
            capabilities: &["chat", "tools", "provider-routing"],
        },
    ]
}

pub fn rmcp_server_info_json() -> Result<Option<Value>> {
    #[cfg(feature = "mcp-rmcp")]
    {
        use rmcp::model::{Implementation, InitializeResult, ServerCapabilities};

        let capabilities = ServerCapabilities::builder()
            .enable_tools()
            .enable_resources()
            .enable_prompts()
            .build();
        let server_info = InitializeResult::new(capabilities).with_server_info(
            Implementation::new("sdd-layer", env!("CARGO_PKG_VERSION"))
                .with_title("SDD Layer MCP")
                .with_description(
                    "Read-only SDD artifacts, context packs, traces and client diagnostics.",
                ),
        );
        Ok(Some(serde_json::to_value(server_info)?))
    }
    #[cfg(not(feature = "mcp-rmcp"))]
    {
        Ok(None)
    }
}

fn call_tool<B: McpBackend>(backend: &B, params: &Value) -> Result<Value> {
    let name = params
        .get("name")
        .and_then(Value::as_str)
        .ok_or_else(|| anyhow!("tools/call missing params.name"))?;
    let arguments = params.get("arguments").unwrap_or(&Value::Null);
    let value = backend.call_tool(name, arguments)?;
    Ok(tool_result(value, false))
}

fn read_resource<B: McpBackend>(backend: &B, params: &Value) -> Result<Value> {
    let uri = params
        .get("uri")
        .and_then(Value::as_str)
        .ok_or_else(|| anyhow!("resources/read missing params.uri"))?;
    let resource = backend.read_resource(uri)?;
    Ok(json!({
        "contents": [{
            "uri": resource.uri,
            "mimeType": resource.mime_type,
            "text": resource.text
        }]
    }))
}

fn get_prompt<B: McpBackend>(backend: &B, params: &Value) -> Result<Value> {
    let name = params
        .get("name")
        .and_then(Value::as_str)
        .ok_or_else(|| anyhow!("prompts/get missing params.name"))?;
    let arguments = params.get("arguments").unwrap_or(&Value::Null);
    backend.get_prompt(name, arguments)
}

fn tool_result(value: Value, is_error: bool) -> Value {
    let text = if let Some(text) = value.as_str() {
        text.to_string()
    } else {
        serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string())
    };
    json!({
        "content": [{ "type": "text", "text": text }],
        "isError": is_error
    })
}

fn jsonrpc_error(id: Value, code: i32, message: &str) -> Value {
    json!({
        "jsonrpc": "2.0",
        "id": id,
        "error": { "code": code, "message": message }
    })
}

fn tool_definitions() -> Vec<Value> {
    mcp_tool_definitions()
        .into_iter()
        .map(|tool| serde_json::to_value(tool).expect("MCP tool definition serializes"))
        .collect()
}

pub fn mcp_tool_definitions() -> Vec<McpToolDefinition> {
    vec![
        tool(
            "sdd_trace_list",
            "List SDD trace events",
            object_schema(&[("orchestration", false)]),
        ),
        tool(
            "sdd_trace_show",
            "Show one trace tree by run_id",
            object_schema(&[("run_id", true)]),
        ),
        tool(
            "sdd_trace_summary",
            "Summarize SDD traces",
            object_schema(&[("orchestration", false)]),
        ),
        tool(
            "sdd_artifact_status",
            "Read local artifact store status",
            object_schema(&[("orchestration", false)]),
        ),
        tool(
            "sdd_context_build",
            "Build read-only SDD context from local artifacts",
            object_schema(&[("orchestration", true), ("stage", true), ("task", false)]),
        ),
        tool(
            "sdd_context_bundle",
            "Build the primary read-only SDD MCP context bundle with artifacts, handoff, traces, capabilities and CodeGraph guidance",
            object_schema(&[
                ("orchestration", true),
                ("stage", true),
                ("task", false),
                ("query", false),
            ]),
        ),
        tool(
            "sdd_clients_doctor",
            "Run clients doctor data in read-only mode",
            object_schema(&[]),
        ),
        tool(
            "sdd_project_status",
            "Read compact SDD MCP cache, client and CodeGraph status",
            object_schema(&[]),
        ),
        tool(
            "sdd_search",
            "Search SDD artifacts, traces and handoffs through one lightweight surface",
            object_schema(&[("query", false), ("limit", false)]),
        ),
        tool(
            "sdd_runtime_adapters",
            "Inspect optional MCP/provider runtime adapters compiled into this sdd binary",
            object_schema(&[]),
        ),
        tool(
            "sdd_optimize_status",
            "Inspect optimization wrapper status for CodeGraph, RTK and Caveman",
            object_schema(&[]),
        ),
        tool(
            "sdd_context_handoff",
            "Build read-only execution handoff from artifacts and optimization status",
            object_schema(&[("orchestration", true), ("stage", true), ("task", false)]),
        ),
        tool(
            "sdd_readiness_summary",
            "Aggregate MCP doctor, health, auto/workflow status, cache freshness, capabilities and quality report into ready/warn/blocked",
            object_schema(&[("orchestration", false), ("workflow", false)]),
        ),
        tool(
            "sdd_capabilities_status",
            "Inspect capability catalog and doctor status",
            object_schema(&[]),
        ),
        tool(
            "sdd_agents_manifest",
            "Read the canonical SDD agent manifest",
            object_schema(&[("agent_id", false)]),
        ),
    ]
}

fn object_schema(fields: &[(&'static str, bool)]) -> Value {
    let mut properties = serde_json::Map::new();
    let mut required = Vec::new();
    for (name, is_required) in fields {
        properties.insert((*name).to_string(), json!({ "type": "string" }));
        if *is_required {
            required.push(*name);
        }
    }
    let mut schema = json!({
        "type": "object",
        "properties": properties,
    });
    if !required.is_empty() {
        schema["required"] = json!(required);
    }
    schema
}

fn tool(name: &'static str, description: &'static str, input_schema: Value) -> McpToolDefinition {
    McpToolDefinition {
        name,
        title: name,
        description,
        input_schema,
        annotations: json!({ "readOnlyHint": true }),
    }
}

fn resource_templates() -> Vec<Value> {
    mcp_resource_templates()
        .into_iter()
        .map(|resource| serde_json::to_value(resource).expect("MCP resource template serializes"))
        .collect()
}

pub fn mcp_resource_templates() -> Vec<McpResourceTemplateDefinition> {
    vec![
        resource_template(
            "sdd://artifact/{orchestration}/{artifact}",
            "sdd_artifact",
            "SDD artifact",
            "Read a local SDD artifact from docs/<slug>/",
            "text/markdown",
        ),
        resource_template(
            "sdd://trace/{run_id}",
            "sdd_trace",
            "SDD trace tree",
            "Read an SDD trace tree as JSON",
            "application/json",
        ),
        resource_template(
            "sdd://context/{orchestration}/{stage}",
            "sdd_context",
            "SDD context pack",
            "Read a generated context pack for one stage",
            "text/markdown",
        ),
        resource_template(
            "sdd://context-bundle/{orchestration}/{stage}",
            "sdd_context_bundle",
            "SDD context bundle",
            "Read the primary MCP context bundle for one stage",
            "application/json",
        ),
        resource_template(
            "sdd://handoff/{orchestration}/{stage}",
            "sdd_context_handoff",
            "SDD context handoff",
            "Read optimization-aware handoff for one stage",
            "application/json",
        ),
        resource_template(
            "sdd://optimization/status",
            "sdd_optimization_status",
            "SDD optimization status",
            "Read optimization wrapper status",
            "application/json",
        ),
        resource_template(
            "sdd://capabilities/catalog",
            "sdd_capabilities_catalog",
            "SDD capability catalog",
            "Read capability catalog and doctor status",
            "application/json",
        ),
        resource_template(
            "sdd://agents/{agent_id}",
            "sdd_agent_manifest",
            "SDD agent manifest",
            "Read canonical SDD agent manifests",
            "application/json",
        ),
        resource_template(
            "sdd://runtime/adapters",
            "sdd_runtime_adapters",
            "SDD runtime adapters",
            "Read optional MCP/provider adapter metadata for this sdd binary",
            "application/json",
        ),
        resource_template(
            "sdd://auto/status",
            "sdd_auto_status",
            "SDD autonomous engine status",
            "Read the autonomous engine status including demand queue and engine states",
            "application/json",
        ),
        resource_template(
            "sdd://workflow/{id}/status",
            "sdd_workflow_status",
            "SDD workflow run status",
            "Read a persisted workflow run report by workflow ID",
            "application/json",
        ),
        resource_template(
            "sdd://quality/{slug}",
            "sdd_quality_report",
            "SDD quality report",
            "Read the quality evaluation report for an orchestration",
            "application/json",
        ),
    ]
}

fn resource_template(
    uri_template: &'static str,
    name: &'static str,
    title: &'static str,
    description: &'static str,
    mime_type: &'static str,
) -> McpResourceTemplateDefinition {
    McpResourceTemplateDefinition {
        uri_template,
        name,
        title,
        description,
        mime_type,
    }
}

fn prompt_definitions() -> Vec<Value> {
    mcp_prompt_definitions()
        .into_iter()
        .map(|prompt| serde_json::to_value(prompt).expect("MCP prompt definition serializes"))
        .collect()
}

pub fn mcp_prompt_definitions() -> Vec<McpPromptDefinition> {
    vec![
        prompt(
            "sdd_orchestration",
            "Run SDD orchestration",
            "Prompt for starting a full SDD orchestration",
            json!([{ "name": "idea", "required": true }]),
        ),
        prompt(
            "sdd_stage_handoff",
            "Prepare stage handoff",
            "Prompt for handing off one SDD stage with trace context",
            json!([
                { "name": "orchestration", "required": true },
                { "name": "stage", "required": true }
            ]),
        ),
        prompt(
            "sdd_trace_review",
            "Review SDD trace",
            "Prompt for reviewing an execution trace",
            json!([{ "name": "run_id", "required": true }]),
        ),
    ]
}

fn prompt(
    name: &'static str,
    title: &'static str,
    description: &'static str,
    arguments: Value,
) -> McpPromptDefinition {
    McpPromptDefinition {
        name,
        title,
        description,
        arguments,
    }
}

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

    struct DummyBackend;

    impl McpBackend for DummyBackend {
        fn call_tool(&self, name: &str, arguments: &Value) -> Result<Value> {
            Ok(json!({ "name": name, "arguments": arguments }))
        }

        fn list_resources(&self) -> Result<Vec<Value>> {
            Ok(vec![json!({"uri": "sdd://trace/root", "name": "root"})])
        }

        fn read_resource(&self, uri: &str) -> Result<McpResource> {
            Ok(McpResource {
                uri: uri.to_string(),
                mime_type: "text/plain".to_string(),
                text: "ok".to_string(),
            })
        }

        fn get_prompt(&self, name: &str, arguments: &Value) -> Result<Value> {
            Ok(json!({
                "description": name,
                "messages": [{ "role": "user", "content": { "type": "text", "text": arguments.to_string() } }]
            }))
        }
    }

    #[test]
    fn serves_jsonrpc_tools_over_lines() {
        let input = br#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"sdd_trace_list","arguments":{}}}
"#;
        let mut output = Vec::new();
        serve_lines(&input[..], &mut output, &DummyBackend).unwrap();
        let text = String::from_utf8(output).unwrap();
        assert!(text.contains("\"tools\""));
        assert!(text.contains("sdd_trace_list"));
        assert!(text.contains("\"isError\":false"));
    }

    #[test]
    fn lists_prompts_and_reads_resources() {
        let input = br#"{"jsonrpc":"2.0","id":1,"method":"prompts/list","params":{}}
{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"sdd://trace/root"}}
"#;
        let mut output = Vec::new();
        serve_lines(&input[..], &mut output, &DummyBackend).unwrap();
        let text = String::from_utf8(output).unwrap();
        assert!(text.contains("sdd_orchestration"));
        assert!(text.contains("sdd://trace/root"));
    }

    #[test]
    fn exposes_runtime_adapter_metadata() {
        let adapters = runtime_adapters();
        assert!(adapters.iter().any(|adapter| adapter.id == "rmcp"));
        assert!(adapters.iter().any(|adapter| adapter.id == "rig"));

        let input = br#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
"#;
        let mut output = Vec::new();
        serve_lines(&input[..], &mut output, &DummyBackend).unwrap();
        let text = String::from_utf8(output).unwrap();
        assert!(text.contains("sdd_runtime_adapters"));
    }

    #[test]
    fn typed_registry_includes_optimized_mcp_surface() {
        let tools = mcp_tool_definitions();
        assert!(tools
            .iter()
            .all(|tool| tool.annotations["readOnlyHint"] == true));
        let tool_names = tools.into_iter().map(|tool| tool.name).collect::<Vec<_>>();
        assert!(tool_names.contains(&"sdd_optimize_status"));
        assert!(tool_names.contains(&"sdd_context_handoff"));
        assert!(tool_names.contains(&"sdd_context_bundle"));
        assert!(tool_names.contains(&"sdd_readiness_summary"));
        assert!(tool_names.contains(&"sdd_capabilities_status"));
        assert!(tool_names.contains(&"sdd_agents_manifest"));
        assert!(tool_names.contains(&"sdd_project_status"));
        assert!(tool_names.contains(&"sdd_search"));

        let resource_names = mcp_resource_templates()
            .into_iter()
            .map(|resource| resource.name)
            .collect::<Vec<_>>();
        assert!(resource_names.contains(&"sdd_context_handoff"));
        assert!(resource_names.contains(&"sdd_context_bundle"));
        assert!(resource_names.contains(&"sdd_agent_manifest"));
        assert!(resource_names.contains(&"sdd_auto_status"));
        assert!(resource_names.contains(&"sdd_workflow_status"));
        assert!(resource_names.contains(&"sdd_quality_report"));
    }

    #[cfg(feature = "mcp-rmcp")]
    #[test]
    fn builds_rmcp_server_info_when_feature_is_enabled() {
        let info = rmcp_server_info_json().unwrap().unwrap();
        assert_eq!(info["serverInfo"]["name"], "sdd-layer");
        assert!(info["capabilities"]["tools"].is_object());
        assert!(info["capabilities"]["resources"].is_object());
        assert!(info["capabilities"]["prompts"].is_object());
    }
}
