pub mod conventions;
pub(crate) mod edit;
pub(crate) mod edit_format;
pub mod edit_hooks;
pub mod edit_plan;
pub mod explore;
pub mod format;
pub mod investigation;
pub mod prompts;
pub mod resources;
pub(crate) mod search_format;
pub mod session;
pub mod smart_query;
pub mod tools;

use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

use parking_lot::{Mutex, RwLock};

use rmcp::RoleServer;
use rmcp::handler::server::router::prompt::PromptRouter;
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::model::{
    GetPromptRequestParams, GetPromptResult, ListPromptsResult, ListResourceTemplatesResult,
    ListResourcesResult, PaginatedRequestParams, ReadResourceRequestParams, ReadResourceResult,
    ServerCapabilities, ServerInfo, Tool,
};
use rmcp::service::RequestContext;
use rmcp::{ServerHandler, prompt_handler, tool_handler};

use crate::live_index::SharedIndex;
use crate::sidecar::TokenStats;
use crate::watcher::WatcherInfo;

/// The MCP server struct.
///
/// Holds a `SharedIndex` and a `ToolRouter` generated by the `#[tool_router]` macro.
/// Holds a `PromptRouter` generated by the `#[prompt_router]` macro.
/// The `project_name` field is passed through to `format::repo_outline`.
/// The `watcher_info` field is read by the health tool to report live watcher state.
/// The `repo_root` field is used by `index_folder` to restart the watcher at a new root.
/// The `token_stats` field provides live hook token savings to the health tool (AD-4 simpler path).
#[derive(Clone)]
pub struct SymForgeServer {
    pub(crate) index: SharedIndex,
    pub(crate) tool_router: ToolRouter<Self>,
    pub(crate) prompt_router: PromptRouter<Self>,
    pub(crate) project_name: String,
    pub(crate) watcher_info: Arc<Mutex<WatcherInfo>>,
    /// MUST NOT be held across `.await`. Use `.lock().take()` and
    /// `.lock().replace(...)` around async work; `parking_lot::Mutex` is
    /// non-async and held-across-await would deadlock the runtime.
    /// Some only in local-stdio mode where this server owns its watcher.
    /// None in daemon-proxy mode and daemon-degraded mode.
    pub(crate) watcher_handle: Arc<Mutex<Option<crate::watcher::WatcherTaskHandle>>>,
    /// Root directory the watcher is currently watching. Stored in shared mutable
    /// state so local stdio tools can keep using the latest project root after
    /// `index_folder` rebinds the server to a new workspace.
    pub(crate) repo_root: Arc<RwLock<Option<PathBuf>>>,
    /// Shared token stats from the HTTP sidecar. Present when the sidecar is running.
    /// The health tool reads this to display token savings accumulated during the session.
    pub(crate) token_stats: Option<Arc<TokenStats>>,
    /// Optional daemon-backed proxy session. When present, tool calls are forwarded to the
    /// shared daemon-owned project runtime instead of the local in-process index.
    /// Wrapped in `tokio::sync::RwLock` so reconnection can swap in a fresh client.
    pub(crate) daemon_client: Option<Arc<tokio::sync::RwLock<crate::daemon::DaemonSessionClient>>>,
    /// Set to `true` after a failed reconnection attempt. While degraded,
    /// calls make one success probe but avoid repeated reconnect attempts.
    pub(crate) daemon_degraded: Arc<AtomicBool>,
    /// Session context tracking: records what the LLM has fetched this session.
    pub(crate) session_context: Arc<session::SessionContext>,
    /// Tracks edit-tool calls that omitted `working_directory` while the
    /// `SYMFORGE_WORKTREE_AWARE` feature flag was on. Surfaced by the
    /// `health` tool as a rolling "last hour" signal.
    pub(crate) worktree_misuse: Arc<crate::worktree::WorktreeMisuseCounter>,
}

impl SymForgeServer {
    /// Create a new server with the given shared index, project name, and watcher state.
    ///
    /// `token_stats` is optional — when `Some`, the health tool will include a token savings
    /// section showing per-hook-type fire counts and estimated tokens saved this session.
    pub fn new(
        index: SharedIndex,
        project_name: String,
        watcher_info: Arc<Mutex<WatcherInfo>>,
        repo_root: Option<PathBuf>,
        token_stats: Option<Arc<TokenStats>>,
    ) -> Self {
        crate::worktree::register_if_feature_enabled();
        Self {
            index,
            tool_router: Self::tool_router(),
            prompt_router: Self::prompt_router(),
            project_name,
            watcher_info,
            watcher_handle: Arc::new(Mutex::new(None)),
            repo_root: Arc::new(RwLock::new(repo_root)),
            token_stats,
            daemon_client: None,
            daemon_degraded: Arc::new(AtomicBool::new(false)),
            session_context: Arc::new(session::SessionContext::new()),
            worktree_misuse: Arc::new(crate::worktree::WorktreeMisuseCounter::new()),
        }
    }

    pub fn new_daemon_proxy(daemon_client: crate::daemon::DaemonSessionClient) -> Self {
        use crate::watcher::WatcherInfo;

        crate::worktree::register_if_feature_enabled();
        let project_name = daemon_client.project_name().to_string();
        let repo_root = daemon_client.project_root().map(|p| p.to_path_buf());
        Self {
            index: crate::live_index::LiveIndex::empty(),
            tool_router: Self::tool_router(),
            prompt_router: Self::prompt_router(),
            project_name,
            watcher_info: Arc::new(Mutex::new(WatcherInfo::default())),
            watcher_handle: Arc::new(Mutex::new(None)),
            repo_root: Arc::new(RwLock::new(repo_root)),
            token_stats: None,
            daemon_client: Some(Arc::new(tokio::sync::RwLock::new(daemon_client))),
            daemon_degraded: Arc::new(AtomicBool::new(false)),
            session_context: Arc::new(session::SessionContext::new()),
            worktree_misuse: Arc::new(crate::worktree::WorktreeMisuseCounter::new()),
        }
    }

    /// Accessor for tests.
    pub fn index(&self) -> &SharedIndex {
        &self.index
    }

    /// Return the MCP tool definitions advertised by this server.
    ///
    /// This is a read-only view over the generated router metadata so integration
    /// tests and diagnostics can validate client compatibility without spinning up
    /// a full stdio transport.
    pub fn tool_definitions() -> Vec<Tool> {
        Self::tool_router().list_all()
    }

    pub(crate) fn capture_repo_root(&self) -> Option<PathBuf> {
        self.repo_root.read().clone()
    }

    /// Record a frecency bump for the given paths against the bound workspace.
    ///
    /// No-op when no repo root is bound (the feature has nothing to anchor
    /// the per-workspace store to). Forwards to
    /// [`crate::live_index::frecency::bump`], which itself no-ops unless
    /// `SYMFORGE_FRECENCY=1`.
    pub(crate) fn bump_frecency(&self, paths: &[PathBuf]) {
        if let Some(root) = self.capture_repo_root() {
            crate::live_index::frecency::bump(&root, paths);
        }
    }

    pub(crate) fn effective_repo_root_for_git_tools(&self) -> Option<PathBuf> {
        self.capture_repo_root()
            .or_else(crate::discovery::find_project_root)
    }

    pub(crate) fn set_repo_root(&self, repo_root: Option<PathBuf>) {
        *self.repo_root.write() = repo_root;
    }

    /// Bump the worktree-awareness misuse counter when an edit handler
    /// was called without `working_directory` and the feature flag is
    /// on. No-op when the caller supplied the parameter or the flag is
    /// off. The counter surfaces in `health` output as a rolling
    /// "last hour" signal of feature-flag-on callers who haven't yet
    /// migrated to the worktree-aware call shape.
    pub(crate) fn note_worktree_misuse_if_flag_on(&self, working_directory: Option<&str>) {
        if working_directory.is_none() && crate::worktree::feature_flag_enabled() {
            self.worktree_misuse.record_missing_working_directory();
        }
    }

    /// Record token savings from an MCP tool response so the health counter accumulates.
    pub(crate) fn record_read_savings(&self, saved_tokens: u64) {
        if let Some(ref stats) = self.token_stats {
            stats
                .read_saved_tokens
                .fetch_add(saved_tokens, std::sync::atomic::Ordering::Relaxed);
            stats
                .read_fires
                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        }
    }

    /// Record token savings from any MCP tool that replaces raw file reads.
    pub(crate) fn record_tool_savings(&self, estimated_raw_tokens: u64, output_tokens: u64) {
        if let Some(ref stats) = self.token_stats {
            let saved = estimated_raw_tokens.saturating_sub(output_tokens);
            stats
                .read_saved_tokens
                .fetch_add(saved, std::sync::atomic::Ordering::Relaxed);
            stats
                .read_fires
                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        }
    }

    /// Record token savings from a named MCP tool, tracking per-tool breakdown.
    pub(crate) fn record_tool_savings_named(
        &self,
        tool_name: &str,
        estimated_raw_tokens: u64,
        output_tokens: u64,
    ) {
        if let Some(ref stats) = self.token_stats {
            let saved = estimated_raw_tokens.saturating_sub(output_tokens);
            stats
                .read_saved_tokens
                .fetch_add(saved, std::sync::atomic::Ordering::Relaxed);
            stats
                .read_fires
                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            stats.record_tool_tokens(tool_name, output_tokens, saved);
        }
    }

    /// Forward a tool call to the daemon. Returns:
    /// - `Some(result)` on success
    /// - `None` on connection failure (after one reconnect attempt), signalling
    ///   the caller to fall through to local execution
    pub(crate) async fn proxy_tool_call<T>(&self, tool_name: &str, params: &T) -> Option<String>
    where
        T: serde::Serialize,
    {
        let daemon_lock = self.daemon_client.as_ref()?;

        let was_degraded = self.daemon_degraded.load(Ordering::Relaxed);

        let value = match serde_json::to_value(params) {
            Ok(value) => value,
            Err(error) => return Some(format!("Daemon proxy serialization failed: {error}")),
        };

        // First attempt.
        // IMPORTANT: The read lock on daemon_lock is held for the duration of
        // the HTTP call. If this hangs, it blocks reconnect (which needs a
        // write lock). We wrap in a timeout so read locks release promptly
        // on failure — the reqwest client also has its own timeout as a
        // backstop, but this ensures lock release within a tighter window.
        {
            let client = daemon_lock.read().await;
            match tokio::time::timeout(
                std::time::Duration::from_secs(10),
                client.call_tool_value(tool_name, value.clone()),
            )
            .await
            {
                Ok(Ok(result)) => {
                    self.daemon_degraded.store(false, Ordering::Relaxed);
                    return Some(result);
                }
                Ok(Err(error)) => {
                    tracing::warn!(
                        tool = tool_name,
                        "daemon proxy call failed, attempting reconnect: {error}"
                    );
                }
                Err(_elapsed) => {
                    tracing::warn!(
                        tool = tool_name,
                        "daemon proxy call timed out after 10s, attempting reconnect"
                    );
                }
            }
        }

        if was_degraded {
            tracing::warn!(
                tool = tool_name,
                "daemon proxy probe failed while degraded, falling back to local execution"
            );
            self.ensure_local_index().await;
            return None;
        }

        // Reconnect attempt — take a write lock so only one caller does this.
        let reconnected = {
            let mut client = daemon_lock.write().await;
            match client.reconnect().await {
                Ok(new_client) => {
                    tracing::info!("daemon reconnected successfully");
                    *client = new_client;
                    true
                }
                Err(reconnect_error) => {
                    tracing::warn!(
                        "daemon reconnect failed, falling back to local execution: {reconnect_error}"
                    );
                    self.daemon_degraded.store(true, Ordering::Relaxed);
                    self.ensure_local_index().await;
                    false
                }
            }
        };

        if !reconnected {
            return None;
        }

        // Retry with the fresh client.
        {
            let client = daemon_lock.read().await;
            match tokio::time::timeout(
                std::time::Duration::from_secs(30),
                client.call_tool_value(tool_name, value),
            )
            .await
            {
                Ok(Ok(result)) => {
                    self.daemon_degraded.store(false, Ordering::Relaxed);
                    Some(result)
                }
                Ok(Err(error)) => {
                    tracing::warn!(
                        tool = tool_name,
                        "daemon proxy call failed after reconnect, falling back to local: {error}"
                    );
                    self.daemon_degraded.store(true, Ordering::Relaxed);
                    self.ensure_local_index().await;
                    None
                }
                Err(_elapsed) => {
                    tracing::warn!(
                        tool = tool_name,
                        "daemon proxy call timed out after reconnect, falling back to local"
                    );
                    self.daemon_degraded.store(true, Ordering::Relaxed);
                    self.ensure_local_index().await;
                    None
                }
            }
        }
    }

    pub(crate) async fn proxy_tool_call_without_params(&self, tool_name: &str) -> Option<String> {
        self.proxy_tool_call(tool_name, &serde_json::json!({}))
            .await
    }

    /// Lazily initialize the local index when the daemon is unreachable.
    /// Uses `SharedIndexHandle::reload` to populate the empty in-process index
    /// from disk, enabling graceful degradation to local-only mode.
    async fn ensure_local_index(&self) {
        {
            let mut watcher = self.watcher_info.lock();
            *watcher = WatcherInfo::detached_local_fallback();
        }

        // If the index already has files, nothing to do.
        if self.index.published_state().file_count > 0 {
            return;
        }

        let repo_root = self.capture_repo_root();
        if let Some(root) = repo_root {
            tracing::info!(
                root = %root.display(),
                "daemon unreachable — loading local index as fallback"
            );
            match self.index.reload(&root) {
                Ok(()) => {
                    let published = self.index.published_state();
                    tracing::info!(
                        files = published.file_count,
                        symbols = published.symbol_count,
                        "local fallback index loaded"
                    );

                    // Spawn git temporal computation so co-change queries work
                    // after daemon degradation (mirrors index_folder behaviour).
                    let expected_gen = self.index.current_project_generation();
                    crate::live_index::git_temporal::spawn_git_temporal_computation(
                        Arc::clone(&self.index),
                        root,
                        expected_gen,
                    );
                }
                Err(error) => {
                    tracing::error!("failed to load local fallback index: {error}");
                }
            }
        } else {
            tracing::warn!("daemon unreachable and no repo root available for local fallback");
        }
    }

    /// Test-only dispatcher that routes a tool call by name and JSON payload.
    ///
    /// Mirrors the tool-name match in `daemon::execute_tool_call`, but takes
    /// `&self` so parity tests can exercise handlers directly without a live
    /// daemon or MCP transport. Only the tools needed by the `tests/` suites
    /// are wired up here.
    #[doc(hidden)]
    pub async fn dispatch_tool_for_tests(
        &self,
        tool_name: &str,
        params: serde_json::Value,
    ) -> String {
        use rmcp::handler::server::wrapper::Parameters;
        fn decode<T: serde::de::DeserializeOwned>(params: serde_json::Value) -> Result<T, String> {
            serde_json::from_value(params).map_err(|e| format!("invalid tool parameters: {e}"))
        }
        macro_rules! call {
            ($method:ident, $input:ty) => {{
                match decode::<$input>(params) {
                    Ok(input) => self.$method(Parameters(input)).await,
                    Err(e) => e,
                }
            }};
        }
        match tool_name {
            "replace_symbol_body" => call!(replace_symbol_body, edit::ReplaceSymbolBodyInput),
            "insert_symbol" => call!(insert_symbol, edit::InsertSymbolInput),
            "delete_symbol" => call!(delete_symbol, edit::DeleteSymbolInput),
            "edit_within_symbol" => call!(edit_within_symbol, edit::EditWithinSymbolInput),
            "batch_edit" => call!(batch_edit, edit::BatchEditInput),
            "batch_rename" => call!(batch_rename, edit::BatchRenameInput),
            "batch_insert" => call!(batch_insert, edit::BatchInsertInput),
            "search_files" => call!(search_files, tools::SearchFilesInput),
            "search_text" => call!(search_text, tools::SearchTextInput),
            "search_symbols" => call!(search_symbols, tools::SearchSymbolsInput),
            "get_file_context" => call!(get_file_context, tools::GetFileContextInput),
            "get_file_content" => call!(get_file_content, tools::GetFileContentInput),
            "get_symbol" => call!(get_symbol, tools::GetSymbolInput),
            "get_symbol_context" => call!(get_symbol_context, tools::GetSymbolContextInput),
            "health" => self.health().await,
            "health_compact" => self.health_compact().await,
            "conventions" => self.conventions().await,
            other => format!("dispatch_tool_for_tests: unknown tool '{other}'"),
        }
    }
}

/// Wire `SymForgeServer` as an MCP `ServerHandler`.
///
/// The `#[tool_handler]` macro delegates tool dispatch to `self.tool_router`
/// and supplies the `call_tool` / `list_tools` implementations automatically.
#[tool_handler(router = self.tool_router)]
#[prompt_handler(router = self.prompt_router)]
impl ServerHandler for SymForgeServer {
    fn get_info(&self) -> ServerInfo {
        ServerInfo::new(
            ServerCapabilities::builder()
                .enable_tools()
                .enable_prompts()
                .enable_resources()
                .build(),
        )
    }

    fn list_resources(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: RequestContext<RoleServer>,
    ) -> impl std::future::Future<Output = Result<ListResourcesResult, rmcp::ErrorData>> + Send + '_
    {
        std::future::ready(Ok(ListResourcesResult {
            resources: self.resource_definitions(),
            ..Default::default()
        }))
    }

    fn list_resource_templates(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: RequestContext<RoleServer>,
    ) -> impl std::future::Future<Output = Result<ListResourceTemplatesResult, rmcp::ErrorData>>
    + Send
    + '_ {
        std::future::ready(Ok(ListResourceTemplatesResult {
            resource_templates: self.resource_template_definitions(),
            ..Default::default()
        }))
    }

    fn read_resource(
        &self,
        request: ReadResourceRequestParams,
        _context: RequestContext<RoleServer>,
    ) -> impl std::future::Future<Output = Result<ReadResourceResult, rmcp::ErrorData>> + Send + '_
    {
        let uri = request.uri;
        async move { self.read_resource_uri(&uri).await }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::Router;
    use axum::extract::State;
    use axum::routing::post;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use tokio::net::TcpListener;

    #[derive(Clone)]
    struct FakeToolState {
        calls: Arc<AtomicUsize>,
        body: Arc<String>,
    }

    async fn fake_tool_handler(State(state): State<FakeToolState>) -> String {
        state.calls.fetch_add(1, Ordering::Relaxed);
        state.body.as_ref().clone()
    }

    async fn spawn_fake_tool_server(
        body: &str,
    ) -> (String, tokio::sync::oneshot::Sender<()>, Arc<AtomicUsize>) {
        let listener = TcpListener::bind("127.0.0.1:0")
            .await
            .expect("bind fake daemon tool server");
        let base_url = format!("http://{}", listener.local_addr().expect("listener addr"));
        let calls = Arc::new(AtomicUsize::new(0));
        let state = FakeToolState {
            calls: Arc::clone(&calls),
            body: Arc::new(body.to_string()),
        };
        let app = Router::new()
            .route(
                "/v1/sessions/{session_id}/tools/{tool_name}",
                post(fake_tool_handler),
            )
            .with_state(state);
        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
        tokio::spawn(async move {
            let shutdown = async move {
                let _ = shutdown_rx.await;
            };
            let _ = axum::serve(listener, app)
                .with_graceful_shutdown(shutdown)
                .await;
        });
        (base_url, shutdown_tx, calls)
    }

    #[tokio::test]
    async fn daemon_degraded_clears_on_next_success() {
        let (base_url, shutdown, calls) = spawn_fake_tool_server("daemon-ok").await;
        let daemon_client = crate::daemon::DaemonSessionClient::new_for_test(
            base_url,
            "project-id".to_string(),
            "session-id".to_string(),
            "project-name".to_string(),
        );
        let server = SymForgeServer::new_daemon_proxy(daemon_client);
        server.daemon_degraded.store(true, Ordering::Relaxed);

        let result = server
            .proxy_tool_call("health", &serde_json::json!({}))
            .await;
        let _ = shutdown.send(());

        assert_eq!(result.as_deref(), Some("daemon-ok"));
        assert_eq!(
            calls.load(Ordering::Relaxed),
            1,
            "degraded proxy should still probe the daemon once"
        );
        assert!(
            !server.daemon_degraded.load(Ordering::Relaxed),
            "successful proxy call should clear daemon_degraded"
        );
    }
}
