//! MCP server state.
//!
//! [`RipvecServer`] holds the shared in-memory ripvec indices, the ripvec
//! encoder/reranker handles, and the PageRank repo graph. Post-v3.0.0
//! there is no background indexing — the index is built on first query
//! against a root by the `search` tool handler and kept current via
//! incremental `apply_diff` reconciliation on subsequent calls.

use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::Arc;

use rmcp::handler::server::tool::ToolRouter;
use tokio::sync::{OnceCell, RwLock};

/// Thread-safe ring buffer that captures the last N log lines for the `debug_log` tool.
#[derive(Clone)]
pub struct LogBuffer {
    inner: Arc<std::sync::Mutex<VecDeque<String>>>,
    capacity: usize,
}

impl LogBuffer {
    /// Create a new buffer that retains the last `capacity` log lines.
    pub fn new(capacity: usize) -> Self {
        Self {
            inner: Arc::new(std::sync::Mutex::new(VecDeque::with_capacity(capacity))),
            capacity,
        }
    }

    /// Push a formatted log line into the buffer, evicting the oldest if full.
    pub fn push(&self, line: String) {
        if let Ok(mut buf) = self.inner.lock() {
            if buf.len() >= self.capacity {
                buf.pop_front();
            }
            buf.push_back(line);
        }
    }

    /// Read all buffered lines without consuming them.
    pub fn snapshot(&self) -> Vec<String> {
        self.inner
            .lock()
            .map(|buf| buf.iter().cloned().collect())
            .unwrap_or_default()
    }
}

/// A tracing [`Layer`](tracing_subscriber::Layer) that writes formatted events
/// into a [`LogBuffer`] so they can be retrieved via the `debug_log` MCP tool.
pub struct BufferLayer {
    buffer: LogBuffer,
}

impl BufferLayer {
    /// Wrap a [`LogBuffer`] in a tracing layer.
    pub fn new(buffer: LogBuffer) -> Self {
        Self { buffer }
    }
}

/// Visitor that extracts fields from a tracing event into a string buffer.
struct MessageVisitor<'a>(&'a mut String);

impl tracing::field::Visit for MessageVisitor<'_> {
    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
        use std::fmt::Write;
        if field.name() == "message" {
            let _ = write!(self.0, "{value:?}");
        } else {
            let _ = write!(self.0, " {field}={value:?}");
        }
    }
    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
        use std::fmt::Write;
        if field.name() == "message" {
            let _ = write!(self.0, "{value}");
        } else {
            let _ = write!(self.0, " {field}={value}");
        }
    }
}

impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for BufferLayer {
    fn on_event(
        &self,
        event: &tracing::Event<'_>,
        _ctx: tracing_subscriber::layer::Context<'_, S>,
    ) {
        use std::fmt::Write;
        let meta = event.metadata();
        let mut message = String::new();
        let _ = write!(message, "{} ", meta.level());
        event.record(&mut MessageVisitor(&mut message));
        self.buffer.push(message);
    }
}

/// Type alias for the reload handle used to change log filters at runtime.
pub type FilterReloadHandle =
    tracing_subscriber::reload::Handle<tracing_subscriber::EnvFilter, tracing_subscriber::Registry>;

/// MCP search engine selection.
///
/// `Ripvec` (the only engine since v3.0.0): cacheless composite engine -
/// Model2Vec static bi-encoder + TinyBERT-L-2-v2 cross-encoder rerank +
/// path-enriched BM25 + PageRank percentile boost. All-CPU. Per-root
/// in-memory index built on first query; kept current via `apply_diff`
/// reconciliation when file changes are detected.
///
/// The transformer-backed `Modernbert` and `Bert` variants were removed in
/// v3.0.0 along with the `legacy-transformer-mcp` feature. Passing
/// `RIPVEC_MCP_ENGINE=modernbert` or `RIPVEC_MCP_ENGINE=bert` now causes an
/// error at startup per the v3.0.0 breaking-change contract - @Postel (1980):
/// a breaking-change boundary should reject doomed inputs explicitly.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum McpEngine {
    #[default]
    Ripvec,
}

/// The ripvec MCP server, holding shared backend and tokenizer state.
///
/// All fields use `Arc` / `OnceCell` / `RwLock` so the struct is cheaply
/// cloneable and safe to share across async tasks.
#[derive(Clone)]
pub struct RipvecServer {
    /// Active search engine. Always `Ripvec` since v3.0.0.
    pub engine: McpEngine,
    /// Per-root in-memory `RipvecIndex` cache. Populated on first
    /// query per root via `ensure_ripvec_root`; rebuilt selectively via
    /// `apply_diff` when file changes are detected. Never touches disk.
    pub ripvec_indices: Arc<
        RwLock<
            std::collections::HashMap<
                PathBuf,
                Arc<ripvec_core::encoder::ripvec::index::RipvecIndex>,
            >,
        >,
    >,
    /// Lazy-loaded ripvec bi-encoder (StaticEncoder, Model2Vec 32M).
    pub ripvec_encoder: Arc<OnceCell<Arc<ripvec_core::encoder::ripvec::dense::StaticEncoder>>>,
    /// Lazy-loaded cross-encoder reranker (L-12).
    pub ripvec_reranker: Arc<OnceCell<Arc<ripvec_core::rerank::Reranker>>>,
    /// Root directory of the project being indexed.
    pub project_root: PathBuf,
    /// Generated tool router mapping tool names to handlers.
    pub tool_router: ToolRouter<Self>,
    /// Cached PageRank-weighted repository graph (built after indexing).
    pub repo_graph: Arc<RwLock<Option<ripvec_core::repo_map::RepoGraph>>>,
    /// Cache of on-demand indices for non-default roots (keyed by canonical path).
    /// Populated by tools with `root` parameter. Type-erased through
    /// `SearchableIndex` so the cache always holds a `RipvecIndex`.
    pub root_indices: Arc<
        RwLock<
            std::collections::HashMap<PathBuf, Arc<dyn ripvec_core::searchable::SearchableIndex>>,
        >,
    >,
    /// Cache of on-demand repo graphs for non-default roots (keyed by canonical path).
    /// Values are `Arc<RepoGraph>` so clones inside `spawn_blocking` closures are
    /// pointer-bumps rather than deep copies of the graph data.
    pub root_graphs:
        Arc<RwLock<std::collections::HashMap<PathBuf, Arc<ripvec_core::repo_map::RepoGraph>>>>,
    /// Ring buffer capturing recent log lines for the `debug_log` tool.
    pub log_buffer: LogBuffer,
    /// Handle to reload the tracing filter at runtime via `log_level` tool.
    pub reload_handle: Arc<FilterReloadHandle>,
}

/// Static instructions block advertised to MCP clients at session start.
///
/// This string is the authoritative description of the 4.0 tool surface.
/// Tests in this module assert that it documents the correct parameters and
/// does NOT reference removed tools (`search_code`, `search_text`).
pub(crate) const SERVER_INSTRUCTIONS: &str = "\
Semantic code search and structural analysis. Finds code by MEANING \
(not text match) using vector embeddings and PageRank dependency graphs.

## WORKFLOW: broad → narrow
1. get_repo_map → architecture overview (which files matter most)
2. search → find specific code by meaning
3. LSP tools → precise navigation (definitions, references, calls)

## TOOLS

**get_repo_map** — START HERE for orientation.
Returns JSON with a `files` array; each file has `lsp_location`, `rank` \
(PageRank score), `content_kind`, `symbols` (each with `kind`, `lsp_location`, \
and def-level `rank`), `calls` (each with `lsp_location` and target `rank`), \
`truncated_symbols`, and `truncated_calls`. \
Response envelope includes `estimated_bytes`, `budget_bytes`, and \
`budget_exhausted` for budget telemetry. \
Use `token_budget` (default 4000 tokens) to control response size — budget is \
allocated across files by PageRank share (40% cap per file); symbols fill each \
file in def-rank order with a logarithmic attenuation cutoff. \
Use `focus_file` to rebias the graph toward a specific file's neighborhood. \
Use `include_metadata=false` (default) to exclude JSON/YAML/config noise. \
Pass `files[N].lsp_location` directly to `lsp_document_symbols` or other \
LSP tools — no destructuring required.

**search** — semantic search over code and prose.
Understands meaning: \"retry logic with exponential backoff\" finds the \
actual retry implementation even if the code never uses those exact words.
Parameters:
- `corpus`: `\"code\"` (default) | `\"docs\"` | `\"all\"` — which file types \
  participate. Use `\"docs\"` for README/design-doc answers; `\"code\"` for \
  implementation; `\"all\"` when you need both.
- `rerank`: `\"auto\"` (default) | `\"always\"` | `\"never\"` — cross-encoder \
  (TinyBERT-L-2-v2) policy. `\"auto\"` fires on prose-heavy corpora under NL \
  queries; `\"always\"` forces it; `\"never\"` skips it (fastest on large code corpora).
- `include_metadata`: `false` (default) | `true` — when `true`, includes \
  chunks from JSON/YAML/TOML/XML/lock/snap files. Default excludes these \
  to prevent calibration data and config noise from flooding results.
Results include `lsp_location` with `start_line` pointing at the symbol \
identifier line (not the doc-comment chunk start).

**find_similar** — find structurally similar code.
Accepts exactly one of:
- `location`: an `lsp_location` object from any tool result — pass \
  `results[0].lsp_location` directly (no destructuring).
- `symbol_name`: a symbol name resolved via the workspace symbol index.

**find_duplicates** — find near-duplicate code chunks.
Response includes `corpus_chunk_count` (total chunks searched) and \
`capped` (true when corpus exceeded 10,000 chunks — scope `root=` down \
to avoid a silent 0-pairs result). `include_metadata` (default `false`) \
and `corpus` (default `\"code\"`) apply the same content filters as `search`.

**find_dead_code** — detect definitions unreachable from any entry point.
BFS over the def-level call graph seeded from per-language entry-point detectors \
(Rust: fn main, pub fns in lib.rs, #[test], #[no_mangle]; \
Python: if __name__==\"__main__\", __all__, def test_*; \
Go: func main, func init, Test*/Benchmark*/Example*, capitalised exports). \
Returns `dead_clusters[]` sorted by size descending; each cluster carries `root_node` \
(highest-PageRank member — best first-investigation target), `size`, `total_lines`, \
and `member_defs[]` with `lsp_location`. \
Use `min_cluster_size=2` to focus on dead call-graph islands (groups of unreachable \
functions that call each other); `include_test_paths=false` to measure \
production-only dead code.


**lsp_prepare_call_hierarchy** + **lsp_incoming_calls** / **lsp_outgoing_calls**:
`lsp_prepare_call_hierarchy(location=...)` returns `results[]`, each with \
a `data` field that IS the `item` for the next call. \
Pass `results[0]` (the whole item object) directly as `item` to \
`lsp_incoming_calls` or `lsp_outgoing_calls` — the `item` parameter is typed \
as an object so the framework preserves it without stringification (A3).

**up_to_date** — check if the running binary is newer than source.

## COMPOSABILITY CONTRACT

Every tool returns `lsp_location`; every position-scoped tool accepts \
`lsp_location` as `location` input. Chain tools without destructuring:

```
hits = search(query)
map  = get_repo_map(focus_file=hits[0].lsp_location.file_path)
syms = lsp_document_symbols(file_path=map.files[0].lsp_location.file_path)
hier = lsp_prepare_call_hierarchy(location=syms.results[0].lsp_location)
out  = lsp_outgoing_calls(item=hier.results[0])   # no destructure
```

## WHEN TO USE (by language / task)

Rust:
- \"How does the trait system work here?\" → get_repo_map (shows trait defs + impls)
- \"Find error handling with thiserror\" → search(corpus=\"code\")
- \"What implements EmbedBackend?\" → search then LSP findReferences

TypeScript / React:
- \"Find components that handle form validation\" → search(corpus=\"code\")
- \"How are API routes organized?\" → get_repo_map (shows route files ranked)
- \"Find hooks similar to useAuth\" → find_similar(symbol_name=\"useAuth\")

Python / Django / FastAPI:
- \"Find database migration logic\" → search(query=\"database migration\")
- \"Which views handle authentication?\" → search(query=\"authentication middleware\")
- \"Show the project structure\" → get_repo_map

SQL / dbt:
- \"Find queries that join users and orders\" → search(query=\"join users orders\")
- \"Which models depend on the staging layer?\" → get_repo_map
- \"Find slowly changing dimension logic\" → search(query=\"SCD type 2\")

Full-stack:
- \"How does the frontend call the backend?\" → get_repo_map (shows API boundary)
- \"Find WebSocket handling\" → search(query=\"WebSocket connection handler\")
- \"What's similar to the payment processing flow?\" → find_similar(symbol_name=\"payment\")

## TIPS
- Natural language queries work best (not regex or exact code)
- Model2Vec static encoder (potion-base-32M); TinyBERT-L-2-v2 reranker \
  fires on prose corpora or when rerank=\"always\"
- Index auto-updates when files change (online reconciliation on every query)
- focus_file on get_repo_map: great when you're editing a specific file";

impl rmcp::ServerHandler for RipvecServer {
    fn get_info(&self) -> rmcp::model::ServerInfo {
        rmcp::model::ServerInfo::new(
            rmcp::model::ServerCapabilities::builder()
                .enable_tools()
                .enable_resources()
                .build(),
        )
        .with_instructions(SERVER_INSTRUCTIONS)
    }

    fn call_tool(
        &self,
        request: rmcp::model::CallToolRequestParams,
        context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> impl std::future::Future<Output = Result<rmcp::model::CallToolResult, rmcp::ErrorData>>
    + Send
    + '_ {
        let ctx = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
        async move { self.tool_router.call(ctx).await }
    }

    fn list_tools(
        &self,
        _request: Option<rmcp::model::PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> impl std::future::Future<Output = Result<rmcp::model::ListToolsResult, rmcp::ErrorData>>
    + Send
    + '_ {
        let tools = self.tool_router.list_all();
        async move {
            Ok(rmcp::model::ListToolsResult {
                tools,
                next_cursor: None,
                meta: None,
            })
        }
    }

    async fn list_resources(
        &self,
        _request: Option<rmcp::model::PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> Result<rmcp::model::ListResourcesResult, rmcp::ErrorData> {
        let resource = rmcp::model::Resource {
            raw: rmcp::model::RawResource {
                uri: "ripvec://repo-map".to_string(),
                name: "Repository Structure Map".to_string(),
                title: None,
                description: Some("PageRank-weighted structural overview".to_string()),
                mime_type: Some("text/markdown".to_string()),
                size: None,
                icons: None,
                meta: None,
            },
            annotations: None,
        };
        Ok(rmcp::model::ListResourcesResult {
            resources: vec![resource],
            next_cursor: None,
            meta: None,
        })
    }

    async fn read_resource(
        &self,
        request: rmcp::model::ReadResourceRequestParams,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> Result<rmcp::model::ReadResourceResult, rmcp::ErrorData> {
        if request.uri != "ripvec://repo-map" {
            return Err(rmcp::ErrorData::internal_error(
                format!("unknown resource URI: {}", request.uri),
                None,
            ));
        }

        let graph_guard = self.repo_graph.read().await;

        let rendered = match graph_guard.as_ref() {
            Some(graph) => ripvec_core::repo_map::render(graph, 1500, None),
            None => "ripvec://repo-map is built on first query against this root; run a search or call `get_repo_map` to populate.".to_string(),
        };

        Ok(rmcp::model::ReadResourceResult::new(vec![
            rmcp::model::ResourceContents::text(rendered, "ripvec://repo-map"),
        ]))
    }
}

#[cfg(test)]
mod tests {
    // IndexingGuard tests removed in v3.0.0: the guard struct itself was
    // removed when run_background_index was stubbed (the StaticEncoder
    // engine doesn't need an async indexing flag - the CLI `ripvec --index`
    // builds the index synchronously). When the MCP server's StaticEncoder
    // rewire lands, re-introduce IndexingGuard if the RAII pattern is needed.

    /// Tests that the instructions block documents the `corpus` parameter.
    ///
    /// F1 acceptance: `test:mcp_instructions_block_documents_corpus_parameter`
    #[test]
    fn mcp_instructions_block_documents_corpus_parameter() {
        let instructions = super::SERVER_INSTRUCTIONS;
        assert!(
            instructions.contains("corpus"),
            "instructions must document the `corpus` parameter (C2)"
        );
        assert!(
            instructions.contains("\"code\"")
                && instructions.contains("\"docs\"")
                && instructions.contains("\"all\""),
            "instructions must document corpus values code/docs/all"
        );
    }

    /// Tests that the instructions block documents the `include_metadata` parameter.
    ///
    /// F1 acceptance: `test:mcp_instructions_block_documents_include_metadata_parameter`
    #[test]
    fn mcp_instructions_block_documents_include_metadata_parameter() {
        let instructions = super::SERVER_INSTRUCTIONS;
        assert!(
            instructions.contains("include_metadata"),
            "instructions must document the `include_metadata` parameter (C2)"
        );
    }

    /// Tests that the instructions block does NOT mention `search_code` or `search_text`.
    ///
    /// F1 acceptance: `test:mcp_instructions_block_does_not_mention_search_code`
    #[test]
    fn mcp_instructions_block_does_not_mention_search_code() {
        let instructions = super::SERVER_INSTRUCTIONS;
        assert!(
            !instructions.contains("search_code"),
            "instructions must not reference the removed `search_code` tool (F1)"
        );
        assert!(
            !instructions.contains("search_text"),
            "instructions must not reference the removed `search_text` tool (F1)"
        );
    }

    // R9.1: root_graphs uses tokio::sync::RwLock so async reads yield to the
    // executor instead of blocking the thread.
    //
    // This test validates the async-safety behavior: a concurrent reader must
    // yield cooperatively while a writer holds the lock, completing within a
    // small time budget rather than deadlocking the executor.
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn root_graphs_async_concurrent_reads_do_not_deadlock() {
        use std::collections::HashMap;
        use std::path::PathBuf;
        use tokio::sync::RwLock;
        use tokio::time::{Duration, timeout};

        // root_graphs is tokio::sync::RwLock<HashMap<PathBuf, Arc<RepoGraph>>>.
        // A concurrent reader must not deadlock while a writer holds the lock —
        // the read yields cooperatively to the executor.
        let root_graphs: std::sync::Arc<
            RwLock<HashMap<PathBuf, std::sync::Arc<ripvec_core::repo_map::RepoGraph>>>,
        > = std::sync::Arc::new(RwLock::new(HashMap::new()));
        let rg_write = root_graphs.clone();

        // Hold a write lock for 50 ms on a separate task.
        let write_task = tokio::spawn(async move {
            let _guard = rg_write.write().await;
            tokio::time::sleep(Duration::from_millis(50)).await;
        });

        // Give the writer a head start.
        tokio::time::sleep(Duration::from_millis(5)).await;

        // A concurrent read must complete within the 200 ms budget.
        let rg_read = root_graphs.clone();
        let read_result = timeout(Duration::from_millis(200), async move {
            let _guard = rg_read.read().await;
        })
        .await;

        write_task.await.unwrap();
        assert!(
            read_result.is_ok(),
            "tokio::sync::RwLock read must complete within 200 ms even while \
             a write is held (this would deadlock a sync RwLock on one thread)"
        );
    }
}
