use anyhow::{Context, Result, bail};
use serde::Deserialize;
use serde_json::{Value, json};

use crate::protocol::{ToolContent, ToolDescriptor, ToolsCallResult};

pub fn descriptors() -> Vec<ToolDescriptor> {
    vec![
        ToolDescriptor {
            name: "recall",
            description: "Query the second brain. Returns relevant memories based on semantic similarity, graph relationships, and recency. Use this before starting any task to check what you already know.",
            input_schema: json!({
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Natural language query describing what you want to recall"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Maximum number of memories to return (default: 5)",
                        "default": 5
                    },
                    "source": {
                        "type": "string",
                        "description": "Filter by source (e.g. 'claude-code', 'cursor')"
                    },
                    "memory_type": {
                        "type": "string",
                        "enum": ["episodic", "semantic", "procedural"],
                        "description": "Filter by memory type"
                    },
                    "project": {
                        "type": "string",
                        "description": "Project name to boost relevance of same-project memories"
                    }
                },
                "required": ["query"]
            }),
        },
        ToolDescriptor {
            name: "remember",
            description: "Store new knowledge in the second brain. Automatically extracts entities, creates relationships, and generates embeddings. Use when you learn something worth preserving across conversations.",
            input_schema: json!({
                "type": "object",
                "properties": {
                    "content": {
                        "type": "string",
                        "description": "The knowledge to remember"
                    },
                    "memory_type": {
                        "type": "string",
                        "enum": ["episodic", "semantic", "procedural"],
                        "description": "Type of memory (default: semantic)",
                        "default": "semantic"
                    },
                    "related_to": {
                        "type": "array",
                        "items": { "type": "string" },
                        "description": "Entity names this memory relates to"
                    },
                    "project": {
                        "type": "string",
                        "description": "Project name this memory belongs to"
                    }
                },
                "required": ["content"]
            }),
        },
        ToolDescriptor {
            name: "associate",
            description: "Create an explicit link between two memories or entities. Strengthens the graph connections for better associative recall.",
            input_schema: json!({
                "type": "object",
                "properties": {
                    "from": {
                        "type": "string",
                        "description": "Source memory ID or entity name"
                    },
                    "to": {
                        "type": "string",
                        "description": "Target memory ID or entity name"
                    },
                    "relation": {
                        "type": "string",
                        "enum": ["relates_to", "contradicts", "reinforces", "supersedes"],
                        "description": "Type of relationship"
                    },
                    "context": {
                        "type": "string",
                        "description": "Why these are related"
                    }
                },
                "required": ["from", "to", "relation"]
            }),
        },
        ToolDescriptor {
            name: "forget",
            description: "Mark a memory as superseded or remove it. Use when information is outdated or incorrect.",
            input_schema: json!({
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "description": "Memory ID to forget"
                    },
                    "reason": {
                        "type": "string",
                        "description": "Why this memory is being removed"
                    }
                },
                "required": ["id"]
            }),
        },
        ToolDescriptor {
            name: "context",
            description: "Get all knowledge related to current working context. Traverses the graph from entities matching the query and returns connected memories up to a configurable depth.",
            input_schema: json!({
                "type": "object",
                "properties": {
                    "project": {
                        "type": "string",
                        "description": "Project name or path to get context for"
                    },
                    "depth": {
                        "type": "integer",
                        "description": "How many hops in the graph to traverse (default: 2)",
                        "default": 2
                    }
                },
                "required": ["project"]
            }),
        },
        ToolDescriptor {
            name: "ingest",
            description: "Ingest new conversation history into the second brain. Incrementally processes only new or changed files. Use to update the brain with recent conversations.",
            input_schema: json!({
                "type": "object",
                "properties": {
                    "source": {
                        "type": "string",
                        "enum": ["claude-code"],
                        "description": "Which conversation source to ingest (default: claude-code)",
                        "default": "claude-code"
                    }
                }
            }),
        },
        ToolDescriptor {
            name: "status",
            description: "Show second brain statistics: total memories, ingested files, memory types.",
            input_schema: json!({
                "type": "object",
                "properties": {}
            }),
        },
    ]
}

pub fn dispatch(
    name: &str,
    args: &Value,
    client: &reqwest::blocking::Client,
    api_base: &str,
) -> Result<ToolsCallResult> {
    match name {
        "recall" => handle_recall(args, client, api_base),
        "remember" => handle_remember(args, client, api_base),
        "associate" => handle_associate(args, client, api_base),
        "forget" => handle_forget(args, client, api_base),
        "context" => handle_context(args, client, api_base),
        "ingest" => handle_ingest(args, client, api_base),
        "status" => handle_status(client, api_base),
        _ => bail!("unknown tool: {name}"),
    }
}

#[derive(Deserialize)]
struct RecallResponse {
    memories: Vec<MemoryItem>,
}

#[derive(Deserialize)]
struct MemoryItem {
    id: String,
    content: String,
    memory_type: String,
    #[allow(dead_code)]
    confidence: f32,
    source: String,
    #[allow(dead_code)]
    created_at: String,
    score: Option<f32>,
    #[serde(default)]
    project_path: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    machine_id: String,
    #[serde(default)]
    machine_name: String,
}

fn handle_recall(
    args: &Value,
    client: &reqwest::blocking::Client,
    api_base: &str,
) -> Result<ToolsCallResult> {
    let query = args["query"].as_str().unwrap_or("").to_string();
    let limit = args["limit"].as_u64().unwrap_or(5) as usize;

    let body = json!({
        "query": query,
        "limit": limit,
        "source": args["source"].as_str(),
        "memory_type": args["memory_type"].as_str(),
        "project": args["project"].as_str(),
    });

    let resp: RecallResponse = client
        .post(format!("{api_base}/v1/recall"))
        .json(&body)
        .send()
        .context("calling recall")?
        .error_for_status()
        .context("recall returned error")?
        .json()
        .context("parsing recall response")?;

    if resp.memories.is_empty() {
        return Ok(ToolsCallResult {
            content: vec![ToolContent::Text {
                text: format!("No memories found for: \"{}\"", query),
            }],
            is_error: None,
        });
    }

    let mut output = format!(
        "Found {} memories for \"{}\":\n\n",
        resp.memories.len(),
        query
    );

    for (i, m) in resp.memories.iter().enumerate() {
        let score_str = m
            .score
            .map(|s| format!("score: {:.3}, ", s))
            .unwrap_or_default();
        let project_str = m
            .project_path
            .as_deref()
            .map(|p| format!(", project: {}", p))
            .unwrap_or_default();
        let machine_str = if m.machine_name.is_empty() {
            String::new()
        } else {
            format!(", machine: {}", m.machine_name)
        };
        output.push_str(&format!(
            "{}. [{}] ({}type: {}, source: {}{}{})\n   {}\n\n",
            i + 1,
            m.id,
            score_str,
            m.memory_type,
            m.source,
            project_str,
            machine_str,
            truncate(&m.content, 200),
        ));
    }

    Ok(ToolsCallResult {
        content: vec![ToolContent::Text { text: output }],
        is_error: None,
    })
}

#[derive(Deserialize)]
struct RememberResponse {
    id: String,
    memory_type: String,
}

fn handle_remember(
    args: &Value,
    client: &reqwest::blocking::Client,
    api_base: &str,
) -> Result<ToolsCallResult> {
    let content = args["content"].as_str().unwrap_or("").to_string();

    if content.is_empty() {
        return Ok(ToolsCallResult {
            content: vec![ToolContent::Text {
                text: "Error: content is required".to_string(),
            }],
            is_error: Some(true),
        });
    }

    let body = json!({
        "content": content,
        "memory_type": args["memory_type"].as_str(),
        "source": args.get("source").and_then(|s| s.as_str()).unwrap_or("mcp-direct"),
        "related_to": args["related_to"].as_array(),
        "project": args["project"].as_str(),
    });

    let resp: RememberResponse = client
        .post(format!("{api_base}/v1/remember"))
        .json(&body)
        .send()
        .context("calling remember")?
        .error_for_status()
        .context("remember returned error")?
        .json()
        .context("parsing remember response")?;

    Ok(ToolsCallResult {
        content: vec![ToolContent::Text {
            text: format!(
                "Remembered ({}): \"{}\" [id: {}]",
                resp.memory_type,
                truncate(&content, 100),
                resp.id
            ),
        }],
        is_error: None,
    })
}

fn handle_associate(
    args: &Value,
    client: &reqwest::blocking::Client,
    api_base: &str,
) -> Result<ToolsCallResult> {
    let from_str = args["from"].as_str().unwrap_or("");
    let to_str = args["to"].as_str().unwrap_or("");
    let relation_str = args["relation"].as_str().unwrap_or("relates_to");
    let context = args["context"].as_str();

    let body = json!({
        "from_id": from_str,
        "to_id": to_str,
        "relation": relation_str,
        "context": context,
    });

    client
        .post(format!("{api_base}/v1/associate"))
        .json(&body)
        .send()
        .context("calling associate")?
        .error_for_status()
        .context("associate returned error")?;

    Ok(ToolsCallResult {
        content: vec![ToolContent::Text {
            text: format!(
                "Associated: {} --[{}]--> {}",
                from_str, relation_str, to_str
            ),
        }],
        is_error: None,
    })
}

fn handle_forget(
    args: &Value,
    client: &reqwest::blocking::Client,
    api_base: &str,
) -> Result<ToolsCallResult> {
    let id_str = args["id"].as_str().unwrap_or("");
    let reason = args["reason"].as_str().unwrap_or("manually forgotten");

    let resp = client
        .delete(format!("{api_base}/v1/memory/{id_str}"))
        .send()
        .context("calling forget")?;

    if resp.status().is_success() {
        Ok(ToolsCallResult {
            content: vec![ToolContent::Text {
                text: format!("Forgot memory {} (reason: {})", id_str, reason),
            }],
            is_error: None,
        })
    } else if resp.status().as_u16() == 404 {
        Ok(ToolsCallResult {
            content: vec![ToolContent::Text {
                text: format!("Memory {} not found", id_str),
            }],
            is_error: Some(true),
        })
    } else {
        bail!("forget returned status {}", resp.status())
    }
}

#[derive(Deserialize)]
struct ContextResponse {
    memories: Vec<ContextMemory>,
}

#[derive(Deserialize)]
#[allow(dead_code)]
struct ContextMemory {
    id: String,
    content: String,
    memory_type: String,
    confidence: f32,
    connected: Vec<String>,
}

fn handle_context(
    args: &Value,
    client: &reqwest::blocking::Client,
    api_base: &str,
) -> Result<ToolsCallResult> {
    let project = args["project"].as_str().unwrap_or("");
    let depth = args["depth"].as_u64().unwrap_or(2);

    let body = json!({
        "project": project,
        "depth": depth,
    });

    let resp: ContextResponse = client
        .post(format!("{api_base}/v1/context"))
        .json(&body)
        .send()
        .context("calling context")?
        .error_for_status()
        .context("context returned error")?
        .json()
        .context("parsing context response")?;

    if resp.memories.is_empty() {
        return Ok(ToolsCallResult {
            content: vec![ToolContent::Text {
                text: format!("No context found for project: \"{}\"", project),
            }],
            is_error: None,
        });
    }

    let mut output = format!(
        "Context for \"{}\" ({} memories, depth {}):\n\n",
        project,
        resp.memories.len(),
        depth
    );

    for (i, m) in resp.memories.iter().enumerate() {
        output.push_str(&format!(
            "{}. [{:?}] (confidence: {:.2})\n   {}\n\n",
            i + 1,
            m.memory_type,
            m.confidence,
            truncate(&m.content, 200),
        ));

        for connected in &m.connected {
            output.push_str(&format!("   -> {}\n", connected));
        }
    }

    Ok(ToolsCallResult {
        content: vec![ToolContent::Text { text: output }],
        is_error: None,
    })
}

#[derive(Deserialize)]
struct IngestResponse {
    new_memories: usize,
    new_entities: usize,
    new_conversations: usize,
    skipped_dupes: usize,
    unchanged: usize,
    errors: usize,
    files_processed: usize,
}

fn handle_ingest(
    _args: &Value,
    client: &reqwest::blocking::Client,
    api_base: &str,
) -> Result<ToolsCallResult> {
    let body = json!({});

    let resp: IngestResponse = client
        .post(format!("{api_base}/v1/ingest"))
        .json(&body)
        .send()
        .context("calling ingest")?
        .error_for_status()
        .context("ingest returned error")?
        .json()
        .context("parsing ingest response")?;

    let text = format!(
        "Ingestion complete: {} memories, {} entities from {} conversations ({} dupes skipped, {} unchanged, {} errors, {} files)",
        resp.new_memories,
        resp.new_entities,
        resp.new_conversations,
        resp.skipped_dupes,
        resp.unchanged,
        resp.errors,
        resp.files_processed
    );

    Ok(ToolsCallResult {
        content: vec![ToolContent::Text { text }],
        is_error: None,
    })
}

#[derive(Deserialize)]
struct StatusResponse {
    memory_count: usize,
    ingested_file_count: usize,
}

fn handle_status(client: &reqwest::blocking::Client, api_base: &str) -> Result<ToolsCallResult> {
    let resp: StatusResponse = client
        .get(format!("{api_base}/v1/status"))
        .send()
        .context("calling status")?
        .error_for_status()
        .context("status returned error")?
        .json()
        .context("parsing status response")?;

    let text = format!(
        "Second Brain status:\n  memories: {}\n  ingested files: {}",
        resp.memory_count, resp.ingested_file_count
    );

    Ok(ToolsCallResult {
        content: vec![ToolContent::Text { text }],
        is_error: None,
    })
}

fn truncate(s: &str, max_len: usize) -> String {
    if s.len() <= max_len {
        s.to_string()
    } else {
        let mut end = max_len.min(s.len());
        while end > 0 && !s.is_char_boundary(end) {
            end -= 1;
        }
        format!("{}...", &s[..end])
    }
}
