//! MCP resource handlers for the beads issue tracker.
//!
//! Resources provide read-only discovery endpoints that agents can inspect
//! before calling tools.

use std::collections::HashMap;
use std::sync::Arc;

use fastmcp_rust::{
    McpContext, McpError, McpErrorCode, McpResult, Resource, ResourceContent, ResourceHandler,
    ResourceTemplate,
};
use serde_json::json;

use crate::error::StructuredError;
use crate::model::{Event, Issue, Status};
use crate::storage::{ListFilters, ReadyFilters, ReadySortPolicy, SqliteStorage};

use super::{BeadsState, to_mcp};

fn read_project_config(storage: &SqliteStorage) -> McpResult<HashMap<String, String>> {
    storage.get_all_config().map_err(to_mcp)
}

/// Build a structured "issue not found" error with fuzzy suggestions,
/// mirroring the tools.rs pattern for consistent agent UX.
fn issue_not_found_resource(storage: &SqliteStorage, id: &str) -> McpResult<McpError> {
    let all_ids = storage.get_all_ids().map_err(to_mcp)?;
    let structured = StructuredError::issue_not_found(id, &all_ids);

    let mut data = json!({
        "error_type": "ISSUE_NOT_FOUND",
        "recoverable": true,
        "message": structured.message,
        "discovery_hint": "Use list_issues tool to find valid issue IDs",
    });

    if let Some(hint) = &structured.hint {
        data["hint"] = json!(hint);
    }
    if let Some(ctx) = &structured.context
        && let Some(similar) = ctx.get("similar_ids")
    {
        data["suggestions"] = similar.clone();
    }

    data["suggested_tool_calls"] = json!([{"tool": "list_issues", "arguments": {}}]);

    Ok(McpError::with_data(
        McpErrorCode::ToolExecutionError,
        structured.message,
        data,
    ))
}

// ---------------------------------------------------------------------------
// 1. project/info — static project metadata
// ---------------------------------------------------------------------------

pub struct ProjectInfoResource(Arc<BeadsState>);
impl ProjectInfoResource {
    pub fn new(state: Arc<BeadsState>) -> Self {
        Self(state)
    }
}

impl ResourceHandler for ProjectInfoResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://project/info".into(),
            name: "Project Info".into(),
            description: Some(
                "Workspace metadata: beads directory, issue prefix, configuration. \
                 Read this first to understand the project context. \
                 Used by: project_overview tool returns similar data with more detail."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        let storage = self.0.open_storage().map_err(to_mcp)?;

        let config = read_project_config(&storage)?;
        let prefix = self.0.issue_prefix.as_deref().unwrap_or("br");

        let info = json!({
            "beads_dir": self.0.beads_dir.display().to_string(),
            "issue_prefix": prefix,
            "actor": self.0.actor,
            "config": config,
        });

        Ok(vec![ResourceContent {
            uri: "beads://project/info".into(),
            mime_type: Some("application/json".into()),
            text: Some(info.to_string()),
            blob: None,
        }])
    }
}

// ---------------------------------------------------------------------------
// 2. issues/{id} — individual issue resource template
// ---------------------------------------------------------------------------

pub struct IssueResource(Arc<BeadsState>);
impl IssueResource {
    pub fn new(state: Arc<BeadsState>) -> Self {
        Self(state)
    }
}

impl ResourceHandler for IssueResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://issues/{id}".into(),
            name: "Issue Details".into(),
            description: Some(
                "Full issue details by ID. Discovery: use list_issues tool to find IDs. \
                 Used by: Complements show_issue tool which returns the same data."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn template(&self) -> Option<ResourceTemplate> {
        Some(ResourceTemplate {
            uri_template: "beads://issues/{id}".into(),
            name: "Issue Details".into(),
            description: Some("Full issue details by ID".into()),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        })
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        Err(McpError::invalid_params(
            "Provide an issue ID via the URI template: beads://issues/{id}",
        ))
    }

    fn read_with_uri(
        &self,
        _ctx: &McpContext,
        uri: &str,
        params: &HashMap<String, String>,
    ) -> McpResult<Vec<ResourceContent>> {
        let id = params.get("id").ok_or_else(|| {
            McpError::invalid_params("'id' parameter is required in the URI template")
        })?;

        let storage = self.0.open_storage().map_err(to_mcp)?;

        let maybe_details = storage
            .get_issue_details(id, true, true, 20)
            .map_err(to_mcp)?;
        let Some(details) = maybe_details else {
            return Err(issue_not_found_resource(&storage, id)?);
        };

        let mut result = serde_json::to_value(&details.issue).unwrap_or_default();
        if let Some(obj) = result.as_object_mut() {
            obj.insert("labels".into(), json!(details.labels));
            obj.insert("comments".into(), json!(details.comments));
            obj.insert(
                "dependencies".into(),
                json!(details.dependencies.iter().map(|d| {
                    json!({"id": d.id, "title": d.title, "status": d.status, "dep_type": d.dep_type})
                }).collect::<Vec<_>>()),
            );
            obj.insert(
                "dependents".into(),
                json!(details.dependents.iter().map(|d| {
                    json!({"id": d.id, "title": d.title, "status": d.status, "dep_type": d.dep_type})
                }).collect::<Vec<_>>()),
            );
            if let Some(parent) = &details.parent {
                obj.insert("parent".into(), json!(parent));
            }
        }

        Ok(vec![ResourceContent {
            uri: uri.to_string(),
            mime_type: Some("application/json".into()),
            text: Some(result.to_string()),
            blob: None,
        }])
    }
}

// ---------------------------------------------------------------------------
// 3. schema — JSON schema reference
// ---------------------------------------------------------------------------

pub struct SchemaResource;

impl ResourceHandler for SchemaResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://schema".into(),
            name: "Issue Schema Reference".into(),
            description: Some(
                "Reference for issue fields, valid statuses, priorities, types, \
                 and dependency types. Read this to understand what values are accepted."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        let schema = json!({
            "statuses": {
                "values": ["open", "in_progress", "blocked", "deferred", "draft", "closed", "pinned"],
                "aliases": {
                    "open": ["new", "todo"],
                    "in_progress": ["wip", "working", "active", "started", "in-progress", "inprogress"],
                    "blocked": ["stuck", "waiting"],
                    "deferred": ["later", "postponed", "backlogged"],
                    "closed": ["done", "completed", "resolved", "fixed", "wontfix", "cancelled"],
                    "pinned": ["sticky", "hold", "on_hold", "on-hold"]
                }
            },
            "priorities": {
                "values": ["critical", "high", "medium", "low", "backlog"],
                "aliases": {
                    "critical": ["p0", "urgent", "asap", "emergency"],
                    "high": ["p1", "important"],
                    "medium": ["p2", "normal", "default", "mid"],
                    "low": ["p3", "minor", "trivial", "nice_to_have", "nice-to-have"],
                    "backlog": ["p4", "someday", "eventually", "whenever"]
                }
            },
            "issue_types": {
                "values": ["task", "bug", "feature", "epic", "chore", "docs", "question"],
                "aliases": {
                    "task": ["issue"],
                    "bug": ["bugfix", "defect", "regression"],
                    "feature": ["feat", "enhancement", "story", "request"],
                    "chore": ["maintenance", "cleanup", "refactor", "tech_debt", "tech-debt"],
                    "docs": ["documentation", "doc"],
                    "question": ["q", "help"]
                }
            },
            "dependency_types": [
                "blocks", "related", "parent-child", "waits-for", "duplicates",
                "supersedes", "caused-by", "conditional-blocks", "discovered-from",
                "replies-to", "relates-to"
            ],
            "issue_fields": {
                "id": "string — unique ID (e.g. br-abc123)",
                "title": "string — 1-500 characters",
                "description": "string|null — detailed description",
                "status": "string — see statuses above",
                "priority": "object — {value: 0-4}",
                "issue_type": "string — see issue_types above",
                "assignee": "string|null",
                "owner": "string|null",
                "labels": "string[] — attached labels",
                "parent": "string|null — parent issue ID (via parent-child dependency; read-only in show_issue)",
                "created_at": "ISO 8601 timestamp",
                "updated_at": "ISO 8601 timestamp",
                "closed_at": "ISO 8601 timestamp|null",
                "close_reason": "string|null",
                "due_at": "ISO 8601 timestamp|null",
                "defer_until": "ISO 8601 timestamp|null",
                "estimated_minutes": "integer|null",
                "external_ref": "string|null — external tracker reference"
            },
            "bead_anatomy": {
                "purpose": "Recommended structure for issue descriptions to ensure self-containment and completeness",
                "sections": {
                    "background": "Why this issue exists — context and motivation",
                    "technical_approach": "How to implement — key design decisions and approach",
                    "success_criteria": "How to verify done — concrete, testable conditions",
                    "test_plan": "Unit and integration tests required — specific test cases",
                    "considerations": "Edge cases, risks, and things to watch out for"
                },
                "principles": [
                    "Self-contained: understandable without consulting external plans",
                    "Granular: one coherent piece of work per issue",
                    "Complete: preserve ALL complexity, do not oversimplify",
                    "Dependency-aware: make ALL blocking relationships explicit",
                    "Test-inclusive: every feature issue should have a companion test plan"
                ]
            }
        });

        Ok(vec![ResourceContent {
            uri: "beads://schema".into(),
            mime_type: Some("application/json".into()),
            text: Some(schema.to_string()),
            blob: None,
        }])
    }
}

// ---------------------------------------------------------------------------
// 4. labels — discovery resource for valid label values
// ---------------------------------------------------------------------------

pub struct LabelsResource(Arc<BeadsState>);
impl LabelsResource {
    pub fn new(state: Arc<BeadsState>) -> Self {
        Self(state)
    }
}

impl ResourceHandler for LabelsResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://labels".into(),
            name: "Labels".into(),
            description: Some(
                "All labels in use with issue counts. Read this to discover valid \
                 label values before filtering with list_issues or tagging with update_issue. \
                 Used by: list_issues (labels filter), update_issue (labels_add/labels_remove), \
                 create_issue (labels param)."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        let storage = self.0.open_storage().map_err(to_mcp)?;
        let labels = storage.get_unique_labels_with_counts().map_err(to_mcp)?;

        let result = json!({
            "labels": labels.iter().map(|(name, count)| {
                json!({"name": name, "count": count})
            }).collect::<Vec<_>>(),
        });

        Ok(vec![ResourceContent {
            uri: "beads://labels".into(),
            mime_type: Some("application/json".into()),
            text: Some(result.to_string()),
            blob: None,
        }])
    }
}

// ---------------------------------------------------------------------------
// 5. issues/ready — actionable work items
// ---------------------------------------------------------------------------

pub struct ReadyIssuesResource(Arc<BeadsState>);
impl ReadyIssuesResource {
    pub fn new(state: Arc<BeadsState>) -> Self {
        Self(state)
    }
}

impl ResourceHandler for ReadyIssuesResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://issues/ready".into(),
            name: "Ready Issues".into(),
            description: Some(
                "Issues ready for work: open, not blocked, not deferred. \
                 Quick view of actionable items sorted by priority. \
                 Used by: project_overview returns the same data. Use list_issues for \
                 filtered queries."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        let storage = self.0.open_storage().map_err(to_mcp)?;
        let ready = storage
            .get_ready_issues(&ReadyFilters::default(), ReadySortPolicy::Hybrid)
            .map_err(to_mcp)?;

        let result = json!({
            "count": ready.len(),
            "issues": ready.iter().map(|issue| {
                json!({
                    "id": issue.id,
                    "title": issue.title,
                    "priority": issue.priority,
                    "type": issue.issue_type,
                })
            }).collect::<Vec<_>>(),
        });

        Ok(vec![ResourceContent {
            uri: "beads://issues/ready".into(),
            mime_type: Some("application/json".into()),
            text: Some(result.to_string()),
            blob: None,
        }])
    }
}

// ---------------------------------------------------------------------------
// 6. issues/blocked — blocked work items
// ---------------------------------------------------------------------------

pub struct BlockedIssuesResource(Arc<BeadsState>);
impl BlockedIssuesResource {
    pub fn new(state: Arc<BeadsState>) -> Self {
        Self(state)
    }
}

impl ResourceHandler for BlockedIssuesResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://issues/blocked".into(),
            name: "Blocked Issues".into(),
            description: Some(
                "Issues that are blocked by other issues. Shows what's stuck and \
                 which issues are blocking progress. \
                 Used by: manage_dependencies can unblock issues. Use show_issue on \
                 blockers to investigate."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        let storage = self.0.open_storage().map_err(to_mcp)?;
        let blocked = storage.get_blocked_issues().map_err(to_mcp)?;

        let result = json!({
            "count": blocked.len(),
            "issues": blocked.iter().map(|(issue, blockers)| {
                json!({
                    "id": issue.id,
                    "title": issue.title,
                    "blocked_by": blockers,
                })
            }).collect::<Vec<_>>(),
        });

        Ok(vec![ResourceContent {
            uri: "beads://issues/blocked".into(),
            mime_type: Some("application/json".into()),
            text: Some(result.to_string()),
            blob: None,
        }])
    }
}

// ---------------------------------------------------------------------------
// 7. issues/in_progress — work currently being done
// ---------------------------------------------------------------------------

pub struct InProgressResource(Arc<BeadsState>);
impl InProgressResource {
    pub fn new(state: Arc<BeadsState>) -> Self {
        Self(state)
    }
}

impl ResourceHandler for InProgressResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://issues/in_progress".into(),
            name: "In-Progress Issues".into(),
            description: Some(
                "Issues currently being worked on (status: in_progress). \
                 Shows who is working on what with priorities. \
                 Used by: update_issue to change assignee/status. Use show_issue for \
                 full details."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        let storage = self.0.open_storage().map_err(to_mcp)?;
        let filters = ListFilters {
            statuses: Some(vec![Status::InProgress]),
            include_closed: false,
            limit: Some(50),
            ..ListFilters::default()
        };
        let issues = storage.list_issues(&filters).map_err(to_mcp)?;

        let result = json!({
            "count": issues.len(),
            "issues": issues.iter().map(|issue| {
                json!({
                    "id": issue.id,
                    "title": issue.title,
                    "priority": issue.priority,
                    "type": issue.issue_type,
                    "assignee": issue.assignee,
                })
            }).collect::<Vec<_>>(),
        });

        Ok(vec![ResourceContent {
            uri: "beads://issues/in_progress".into(),
            mime_type: Some("application/json".into()),
            text: Some(result.to_string()),
            blob: None,
        }])
    }
}

// ---------------------------------------------------------------------------
// 8. events/recent — recent audit events
// ---------------------------------------------------------------------------

pub struct EventsResource(Arc<BeadsState>);
impl EventsResource {
    pub fn new(state: Arc<BeadsState>) -> Self {
        Self(state)
    }
}

impl ResourceHandler for EventsResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://events/recent".into(),
            name: "Recent Activity".into(),
            description: Some(
                "Recent audit events across all issues: status changes, field updates, \
                 comments added. Shows the 50 most recent events. \
                 Used by: Helpful for understanding what changed recently. \
                 Use show_issue for events on a specific issue."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        let storage = self.0.open_storage().map_err(to_mcp)?;
        let events = storage.get_all_events(50).map_err(to_mcp)?;

        let result = json!({
            "count": events.len(),
            "events": events.iter().map(|e: &Event| {
                json!({
                    "issue_id": e.issue_id,
                    "event_type": e.event_type,
                    "actor": e.actor,
                    "old_value": e.old_value,
                    "new_value": e.new_value,
                    "created_at": e.created_at,
                })
            }).collect::<Vec<_>>(),
        });

        Ok(vec![ResourceContent {
            uri: "beads://events/recent".into(),
            mime_type: Some("application/json".into()),
            text: Some(result.to_string()),
            blob: None,
        }])
    }
}

// ---------------------------------------------------------------------------
// 9. issues/deferred — deferred work items
// ---------------------------------------------------------------------------

pub struct DeferredIssuesResource(Arc<BeadsState>);
impl DeferredIssuesResource {
    pub fn new(state: Arc<BeadsState>) -> Self {
        Self(state)
    }
}

impl ResourceHandler for DeferredIssuesResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://issues/deferred".into(),
            name: "Deferred Issues".into(),
            description: Some(
                "Issues that have been deferred (status: deferred). Useful for triage — \
                 review what has been postponed and whether it should be revisited. \
                 Used by: update_issue to change status. Use show_issue for full details."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        let storage = self.0.open_storage().map_err(to_mcp)?;
        let filters = ListFilters {
            statuses: Some(vec![Status::Deferred]),
            include_deferred: true,
            limit: Some(50),
            ..ListFilters::default()
        };
        let issues = storage.list_issues(&filters).map_err(to_mcp)?;

        let result = json!({
            "count": issues.len(),
            "issues": issues.iter().map(|issue| {
                json!({
                    "id": issue.id,
                    "title": issue.title,
                    "priority": issue.priority,
                    "type": issue.issue_type,
                    "defer_until": issue.defer_until,
                })
            }).collect::<Vec<_>>(),
        });

        Ok(vec![ResourceContent {
            uri: "beads://issues/deferred".into(),
            mime_type: Some("application/json".into()),
            text: Some(result.to_string()),
            blob: None,
        }])
    }
}

// ---------------------------------------------------------------------------
// 10. graph/health — dependency graph health metrics (bv-inspired)
// ---------------------------------------------------------------------------

/// Compute the longest path length in the "blocks" DAG from a given node.
/// Uses a `visiting` set to detect cycles and avoid infinite recursion.
fn longest_chain_from(
    node: &str,
    edges: &HashMap<String, Vec<String>>,
    cache: &mut HashMap<String, usize>,
    visiting: &mut std::collections::HashSet<String>,
) -> usize {
    if let Some(&cached) = cache.get(node) {
        return cached;
    }
    // Cycle detection: if we're already visiting this node, stop.
    if !visiting.insert(node.to_string()) {
        return 0;
    }
    let depth = edges.get(node).map_or(0, |children| {
        children
            .iter()
            .map(|c| 1 + longest_chain_from(c, edges, cache, visiting))
            .max()
            .unwrap_or(0)
    });
    visiting.remove(node);
    cache.insert(node.to_string(), depth);
    depth
}

/// Compute graph health metrics from the dependency edges.
fn compute_graph_health(storage: &SqliteStorage) -> McpResult<serde_json::Value> {
    let all_edges = storage.get_blocks_dep_edges().map_err(to_mcp)?;
    let open_filters = ListFilters {
        include_closed: false,
        limit: Some(10_000),
        ..ListFilters::default()
    };
    let open_issues = storage.list_issues(&open_filters).map_err(to_mcp)?;
    let open_ids: std::collections::HashSet<&str> =
        open_issues.iter().map(|i| i.id.as_str()).collect();

    // Filter edges to only open→open relationships
    let open_edges: Vec<(String, String)> = all_edges
        .into_iter()
        .filter(|(from, to)| open_ids.contains(from.as_str()) && open_ids.contains(to.as_str()))
        .collect();

    let edge_count = open_edges.len();
    let node_count = open_ids.len();
    let density = if node_count > 1 {
        #[allow(clippy::cast_precision_loss)]
        let d = edge_count as f64 / (node_count as f64 * (node_count as f64 - 1.0));
        (d * 1000.0).round() / 1000.0
    } else {
        0.0
    };

    // Build adjacency list for chain depth computation
    let mut adj: HashMap<String, Vec<String>> = HashMap::new();
    for (from, to) in &open_edges {
        adj.entry(from.clone()).or_default().push(to.clone());
    }

    // Longest chain depth
    let mut depth_cache: HashMap<String, usize> = HashMap::new();
    let mut visiting: std::collections::HashSet<String> = std::collections::HashSet::new();
    let max_chain_depth = open_ids
        .iter()
        .map(|id| longest_chain_from(id, &adj, &mut depth_cache, &mut visiting))
        .max()
        .unwrap_or(0);

    // High-fan-out issues (block 3+ others)
    let high_fan_out: Vec<_> = adj
        .iter()
        .filter(|(_, targets)| targets.len() >= 3)
        .map(|(id, targets)| json!({"id": id, "blocks_count": targets.len()}))
        .collect();

    // Stale issues (not updated in 30+ days)
    let thirty_days_ago = chrono::Utc::now() - chrono::Duration::days(30);
    let stale_filters = ListFilters {
        include_closed: false,
        updated_before: Some(thirty_days_ago),
        limit: Some(100),
        ..ListFilters::default()
    };
    let stale_issues = storage.list_issues(&stale_filters).map_err(to_mcp)?;

    // Cycle check — look for any self-referential paths in open edges
    let has_cycles = !storage.get_blocked_ids().map_err(to_mcp)?.is_empty()
        && open_edges.iter().any(|(from, to)| {
            // Simple heuristic: check if any blocked issue forms a cycle
            // Full cycle detection would require DFS, but would_create_cycle
            // is per-pair. We use the presence of mutual edges as a proxy.
            adj.get(to.as_str())
                .is_some_and(|targets| targets.iter().any(|t| t == from))
        });

    Ok(json!({
        "open_issue_count": node_count,
        "dependency_edge_count": edge_count,
        "density": density,
        "density_interpretation": if density > 0.5 {
            "Very high — issues are heavily coupled, hard to parallelize"
        } else if density > 0.2 {
            "Moderate — some coupling, review if all deps are necessary"
        } else if density > 0.0 {
            "Healthy — dependencies are focused"
        } else {
            "No dependencies — issues are fully independent"
        },
        "max_chain_depth": max_chain_depth,
        "max_chain_interpretation": if max_chain_depth > 5 {
            "Deep chain — critical path is long, hard to parallelize"
        } else if max_chain_depth > 2 {
            "Moderate chain depth"
        } else {
            "Shallow — good parallelization potential"
        },
        "high_fan_out_issues": high_fan_out,
        "mutual_dependency_detected": has_cycles,
        "stale_issue_count": stale_issues.len(),
        "stale_threshold_days": 30,
    }))
}

pub struct GraphHealthResource(Arc<BeadsState>);
impl GraphHealthResource {
    pub fn new(state: Arc<BeadsState>) -> Self {
        Self(state)
    }
}

impl ResourceHandler for GraphHealthResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://graph/health".into(),
            name: "Dependency Graph Health".into(),
            description: Some(
                "Graph-level health metrics for the dependency network: density, \
                 chain depth, fan-out hotspots, stale issues, cycle detection. \
                 Inspired by bv's graph analysis. Read this to understand project \
                 structure and identify bottlenecks. \
                 Used by: plan_next_work and triage prompts for context."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        let storage = self.0.open_storage().map_err(to_mcp)?;
        let health = compute_graph_health(&storage)?;

        Ok(vec![ResourceContent {
            uri: "beads://graph/health".into(),
            mime_type: Some("application/json".into()),
            text: Some(health.to_string()),
            blob: None,
        }])
    }
}

// ---------------------------------------------------------------------------
// 11. issues/bottlenecks — highest-impact blockers (bv-inspired)
// ---------------------------------------------------------------------------

/// Compute bottleneck issues: those that block the most other open issues.
/// This is a practical approximation of PageRank/betweenness from bv.
fn compute_bottlenecks(storage: &SqliteStorage) -> McpResult<serde_json::Value> {
    let edges = storage.get_blocks_dep_edges().map_err(to_mcp)?;
    let open_filters = ListFilters {
        include_closed: false,
        limit: Some(10_000),
        ..ListFilters::default()
    };
    let open_issues = storage.list_issues(&open_filters).map_err(to_mcp)?;
    let open_map: HashMap<&str, &Issue> = open_issues.iter().map(|i| (i.id.as_str(), i)).collect();

    // Count how many open issues each open issue blocks
    let mut blocks_count: HashMap<&str, usize> = HashMap::new();
    for (blocked, blocker) in &edges {
        if open_map.contains_key(blocker.as_str()) && open_map.contains_key(blocked.as_str()) {
            *blocks_count.entry(blocker.as_str()).or_default() += 1;
        }
    }

    // Sort by blocks_count descending
    let mut ranked: Vec<_> = blocks_count.into_iter().collect();
    ranked.sort_by_key(|b| std::cmp::Reverse(b.1));

    let bottlenecks: Vec<_> = ranked
        .iter()
        .take(15)
        .filter_map(|(id, count)| {
            open_map.get(id).map(|issue| {
                json!({
                    "id": issue.id,
                    "title": issue.title,
                    "priority": issue.priority,
                    "status": issue.status,
                    "blocks_count": count,
                    "interpretation": if *count >= 5 {
                        "Critical bottleneck — blocks many issues, prioritize resolving"
                    } else if *count >= 3 {
                        "Significant blocker — resolve to unblock multiple work streams"
                    } else {
                        "Blocker — has downstream impact"
                    }
                })
            })
        })
        .collect();

    Ok(json!({
        "count": bottlenecks.len(),
        "issues": bottlenecks,
        "analysis_hint": "Issues sorted by how many other open issues they block. \
            High blocks_count = high PageRank equivalent. Resolve these first to \
            maximize unblocked work.",
    }))
}

pub struct BottlenecksResource(Arc<BeadsState>);
impl BottlenecksResource {
    pub fn new(state: Arc<BeadsState>) -> Self {
        Self(state)
    }
}

impl ResourceHandler for BottlenecksResource {
    fn definition(&self) -> Resource {
        Resource {
            uri: "beads://issues/bottlenecks".into(),
            name: "Bottleneck Issues".into(),
            description: Some(
                "Issues that block the most other work, sorted by impact. \
                 Equivalent to bv's PageRank-based prioritization. Resolve these \
                 first to maximize throughput. \
                 Used by: plan_next_work prompt uses this data for recommendations."
                    .into(),
            ),
            mime_type: Some("application/json".into()),
            icon: None,
            version: None,
            tags: vec![],
        }
    }

    fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
        let storage = self.0.open_storage().map_err(to_mcp)?;
        let bottlenecks = compute_bottlenecks(&storage)?;

        Ok(vec![ResourceContent {
            uri: "beads://issues/bottlenecks".into(),
            mime_type: Some("application/json".into()),
            text: Some(bottlenecks.to_string()),
            blob: None,
        }])
    }
}

#[cfg(test)]
mod tests {
    use super::{issue_not_found_resource, read_project_config};
    use crate::storage::SqliteStorage;

    #[test]
    fn issue_not_found_resource_surfaces_id_scan_failure() {
        let storage = SqliteStorage::open_memory().expect("storage");
        storage
            .execute_raw("DROP TABLE issues")
            .expect("drop issues table");

        let err = issue_not_found_resource(&storage, "bd-missing")
            .expect_err("ID scan failure must be returned to MCP clients");

        assert!(
            err.to_string().contains("issues") || err.to_string().contains("no such table"),
            "unexpected MCP error: {err}"
        );
    }

    #[test]
    fn read_project_config_surfaces_storage_failure() {
        let storage = SqliteStorage::open_memory().expect("storage");
        storage
            .execute_raw("DROP TABLE config")
            .expect("drop config table");

        let err = read_project_config(&storage)
            .expect_err("config read failure must be returned to MCP clients");

        assert!(
            err.to_string().contains("config") || err.to_string().contains("no such table"),
            "unexpected MCP error: {err}"
        );
    }
}
