//! Daemon server implementation for persistent LSP connections.
//!
//! This module provides the main daemon server that listens on both a Unix
//! socket and a TCP port (`127.0.0.1`) simultaneously. The TCP listener serves
//! as a fallback for sandboxed environments (e.g., macOS Seatbelt) that block
//! `connect()` to Unix domain sockets.
//!
//! Both transports feed into the same request router — no handler logic is
//! duplicated. The transport is just an `AsyncRead + AsyncWrite` stream.

#![allow(dead_code)]

use anyhow::{Context, Result};
use serde_json::Value;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, UnixListener};
use tokio::sync::broadcast;

use crate::daemon::pidfile::{self, PidfileData};
use crate::daemon::pool::LspClientPool;
use crate::daemon::protocol::{
    BatchReferencesEntry, BatchReferencesParams, BatchReferencesResult, DaemonError, DaemonRequest,
    DaemonResponse, DefinitionParams, DefinitionResult, DiagnosticsResult, DocumentSymbolsParams,
    DocumentSymbolsResult, HoverParams, HoverResult, InspectParams, InspectResult, MemberInfo,
    MembersParams, MembersResult, Method, PingResult, ReferencesParams, ReferencesResult,
    ShutdownResult, WorkspaceSymbolsParams, WorkspaceSymbolsResult,
};
use crate::lsp::client::TyLspClient;
use crate::lsp::protocol::{DocumentSymbol, Hover, Location, SymbolKind};

/// Default warmup delays (ms) for LSP operations that may return empty on cold start.
/// Total: 100 + 200 + 400 + 800 = 1500ms.
const WARMUP_DELAYS: [u64; 4] = [100, 200, 400, 800];

/// The daemon server that handles client connections and LSP requests.
pub struct DaemonServer {
    /// Path to the Unix socket
    socket_path: PathBuf,

    /// Path to the pidfile
    pidfile_path: PathBuf,

    /// TCP port the daemon listens on (set after binding)
    tcp_port: u16,

    /// Pool of LSP clients (one per workspace).
    ///
    /// `LspClientPool` uses internal locking (`std::sync::Mutex`) so no outer
    /// async mutex is needed — this avoids holding a `MutexGuard` across the
    /// `.await` inside `get_or_create`.
    lsp_pool: Arc<LspClientPool>,

    /// Broadcast channel for shutdown signal
    shutdown_tx: broadcast::Sender<()>,

    /// Time when the daemon started
    start_time: Instant,
}

impl DaemonServer {
    /// Create a new daemon server with the specified socket path.
    pub fn new(socket_path: PathBuf) -> Self {
        let (shutdown_tx, _) = broadcast::channel(1);
        let pidfile_path =
            pidfile::get_pidfile_path().unwrap_or_else(|_| socket_path.with_extension("pid"));

        Self {
            socket_path,
            pidfile_path,
            tcp_port: 0,
            lsp_pool: Arc::new(LspClientPool::new()),
            shutdown_tx,
            start_time: Instant::now(),
        }
    }

    /// Get the socket path for the current user.
    ///
    /// Delegates to the canonical implementation in [`super::client::get_socket_path`].
    pub fn get_socket_path() -> Result<PathBuf> {
        super::client::get_socket_path()
    }

    /// Start the daemon server and listen for connections on both Unix socket
    /// and TCP `127.0.0.1`.
    pub async fn start(mut self) -> Result<()> {
        let (unix_listener, tcp_listener) = self.bind_listeners().await?;
        self.write_pidfile()?;

        let server = Arc::new(self);
        let local = tokio::task::LocalSet::new();

        Self::spawn_accept_loops(&server, &local, unix_listener, tcp_listener);

        // Wait for shutdown signal (this drives all spawned tasks)
        let server_clone = Arc::clone(&server);
        local
            .run_until(async move {
                let mut shutdown_rx = server_clone.shutdown_tx.subscribe();
                let _ = shutdown_rx.recv().await;
                tracing::info!("Shutdown signal received");
            })
            .await;

        server.cleanup().await?;
        Ok(())
    }

    /// Bind both Unix socket and TCP listeners.
    async fn bind_listeners(&mut self) -> Result<(UnixListener, TcpListener)> {
        // Remove existing socket file if it exists
        if self.socket_path.exists() {
            std::fs::remove_file(&self.socket_path)
                .context("Failed to remove existing socket file")?;
        }

        let unix_listener =
            UnixListener::bind(&self.socket_path).context("Failed to bind Unix socket")?;
        tracing::info!("Daemon listening on Unix socket {}", self.socket_path.display());

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let permissions = std::fs::Permissions::from_mode(0o600);
            std::fs::set_permissions(&self.socket_path, permissions)
                .context("Failed to set socket permissions")?;
        }

        let tcp_listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
            .await
            .context("Failed to bind TCP listener on 127.0.0.1")?;
        let tcp_addr = tcp_listener.local_addr().context("Failed to get TCP local address")?;
        self.tcp_port = tcp_addr.port();
        tracing::info!("Daemon listening on TCP {tcp_addr}");

        Ok((unix_listener, tcp_listener))
    }

    /// Write the pidfile with both transport addresses.
    fn write_pidfile(&self) -> Result<()> {
        let data = PidfileData {
            pid: std::process::id(),
            socket: self.socket_path.clone(),
            tcp_port: self.tcp_port,
            version: env!("CARGO_PKG_VERSION").to_string(),
        };
        data.write(&self.pidfile_path).context("Failed to write pidfile")?;
        tracing::info!("Wrote pidfile to {}", self.pidfile_path.display());
        Ok(())
    }

    /// Spawn idle timeout and both accept loops on the `LocalSet`.
    fn spawn_accept_loops(
        server: &Arc<Self>,
        local: &tokio::task::LocalSet,
        unix_listener: UnixListener,
        tcp_listener: TcpListener,
    ) {
        // NOTE: Using LocalSet because LspClientPool uses std::sync::Mutex
        // internally and spawn_local avoids Send requirements.

        // Idle timeout
        let s = Arc::clone(server);
        local.spawn_local(async move { s.idle_timeout_task().await });

        // Unix socket accept loop
        let s = Arc::clone(server);
        local.spawn_local(async move {
            let mut shutdown_rx = s.shutdown_tx.subscribe();
            loop {
                tokio::select! {
                    result = unix_listener.accept() => {
                        match result {
                            Ok((stream, _addr)) => {
                                let conn = Arc::clone(&s);
                                tokio::task::spawn_local(async move {
                                    if let Err(err) = conn.handle_connection(stream).await {
                                        tracing::error!("Unix connection error: {err}");
                                    }
                                });
                            }
                            Err(err) => tracing::error!("Unix accept error: {err}"),
                        }
                    }
                    _ = shutdown_rx.recv() => {
                        tracing::info!("Unix listener shutting down");
                        break;
                    }
                }
            }
        });

        // TCP accept loop
        let s = Arc::clone(server);
        local.spawn_local(async move {
            let mut shutdown_rx = s.shutdown_tx.subscribe();
            loop {
                tokio::select! {
                    result = tcp_listener.accept() => {
                        match result {
                            Ok((stream, addr)) => {
                                tracing::debug!("TCP connection from {addr}");
                                let conn = Arc::clone(&s);
                                tokio::task::spawn_local(async move {
                                    if let Err(err) = conn.handle_connection(stream).await {
                                        tracing::error!("TCP connection error: {err}");
                                    }
                                });
                            }
                            Err(err) => tracing::error!("TCP accept error: {err}"),
                        }
                    }
                    _ = shutdown_rx.recv() => {
                        tracing::info!("TCP listener shutting down");
                        break;
                    }
                }
            }
        });
    }

    /// Handle a single client connection.
    ///
    /// Generic over any stream implementing `AsyncRead + AsyncWrite`, allowing
    /// the same handler to serve both Unix socket and TCP connections.
    async fn handle_connection<S>(self: Arc<Self>, stream: S) -> Result<()>
    where
        S: tokio::io::AsyncRead + AsyncWrite + Unpin,
    {
        let (reader, writer) = tokio::io::split(stream);
        let mut reader = BufReader::new(reader);
        let mut writer = writer;
        let mut header_line = String::new();

        loop {
            header_line.clear();

            // Read Content-Length header
            let bytes_read = reader
                .read_line(&mut header_line)
                .await
                .context("Failed to read request header")?;
            if bytes_read == 0 {
                // EOF - client disconnected
                break;
            }

            // Parse content length
            let content_length =
                if let Some(len_str) = header_line.trim().strip_prefix("Content-Length: ") {
                    if let Ok(len) = len_str.parse::<usize>() {
                        len
                    } else {
                        send_error_response(&mut writer, DaemonError::parse_error()).await?;
                        continue;
                    }
                } else {
                    send_error_response(&mut writer, DaemonError::parse_error()).await?;
                    continue;
                };

            // Read empty separator line
            let mut empty_line = String::new();
            reader.read_line(&mut empty_line).await.context("Failed to read header separator")?;

            // Read request body
            let mut body = vec![0u8; content_length];
            reader.read_exact(&mut body).await.context("Failed to read request body")?;

            // Parse JSON-RPC request
            let Ok(request) = serde_json::from_slice::<DaemonRequest>(&body) else {
                send_error_response(&mut writer, DaemonError::parse_error()).await?;
                continue;
            };

            tracing::debug!("Received request: {:?}", request.method);

            // Process the request
            let response = self.handle_request(request).await;

            // Send response with Content-Length framing
            let response_json =
                serde_json::to_string(&response).context("Failed to serialize response")?;
            let framed = format!("Content-Length: {}\r\n\r\n{response_json}", response_json.len());
            writer.write_all(framed.as_bytes()).await.context("Failed to write response")?;
            writer.flush().await.context("Failed to flush response")?;

            tracing::debug!("Sent response for request ID {}", response.id);
        }

        Ok(())
    }

    /// Process a single JSON-RPC request and return a response.
    async fn handle_request(&self, request: DaemonRequest) -> DaemonResponse {
        let want_debug = request.debug;
        let lsp_method = Self::daemon_to_lsp_method(request.method);
        // Clone params for debug trace (only when debug is requested)
        let debug_params = if want_debug { Some(request.params.clone()) } else { None };

        let result = match request.method {
            Method::Hover => self.handle_hover(request.params).await,
            Method::Definition => self.handle_definition(request.params).await,
            Method::WorkspaceSymbols => self.handle_workspace_symbols(request.params).await,
            Method::DocumentSymbols => self.handle_document_symbols(request.params).await,
            Method::References => self.handle_references(request.params).await,
            Method::BatchReferences => self.handle_batch_references(request.params).await,
            Method::Inspect => self.handle_inspect(request.params).await,
            Method::Members => self.handle_members(request.params).await,
            Method::Diagnostics => self.handle_diagnostics(request.params).await,
            Method::Ping => self.handle_ping(request.params).await,
            Method::Shutdown => self.handle_shutdown(request.params).await,
        };

        let debug_trace = if want_debug {
            lsp_method.map(|method| {
                use crate::daemon::protocol::DebugTrace;
                DebugTrace {
                    method: method.to_string(),
                    params: debug_params.unwrap_or(Value::Null),
                    response: match &result {
                        Ok(v) => v.clone(),
                        Err(e) => serde_json::json!({"error": e.to_string()}),
                    },
                }
            })
        } else {
            None
        };

        let response = match result {
            Ok(value) => DaemonResponse::success(request.id, value),
            Err(e) => DaemonResponse::error(request.id, DaemonError::internal_error(e.to_string())),
        };
        response.with_debug_trace(debug_trace)
    }

    /// Resolve a file path against the workspace root.
    ///
    /// If the file path is relative, it is joined with the workspace root to
    /// produce an absolute path.  This ensures `file_uri()` (which calls
    /// `tokio::fs::canonicalize`) resolves relative to the workspace, not the
    /// daemon process's CWD.
    fn resolve_file(workspace: &std::path::Path, file: PathBuf) -> PathBuf {
        if file.is_absolute() {
            file
        } else {
            workspace.join(file)
        }
    }

    /// Map daemon method to the primary underlying LSP method.
    fn daemon_to_lsp_method(method: Method) -> Option<&'static str> {
        match method {
            Method::Hover => Some("textDocument/hover"),
            Method::Definition => Some("textDocument/definition"),
            Method::References | Method::BatchReferences => Some("textDocument/references"),
            Method::WorkspaceSymbols => Some("workspace/symbol"),
            Method::DocumentSymbols => Some("textDocument/documentSymbol"),
            Method::Inspect => Some("textDocument/hover + textDocument/references"),
            Method::Members => Some("textDocument/documentSymbol + textDocument/hover"),
            Method::Ping | Method::Shutdown | Method::Diagnostics => None,
        }
    }

    /// Handle a hover request.
    async fn handle_hover(&self, params: Value) -> Result<Value> {
        let params: HoverParams =
            serde_json::from_value(params).context("Invalid hover parameters")?;

        let client = self.lsp_pool.get_or_create(params.workspace.clone()).await?;

        let resolved = Self::resolve_file(&params.workspace, params.file);
        let file_str = resolved.to_string_lossy().to_string();
        client.open_document(&file_str).await?;

        let hover = Self::hover_with_warmup(&client, &file_str, params.line, params.column).await?;

        let result = HoverResult { hover };
        Ok(serde_json::to_value(result)?)
    }

    /// Handle a definition request.
    async fn handle_definition(&self, params: Value) -> Result<Value> {
        let params: DefinitionParams =
            serde_json::from_value(params).context("Invalid definition parameters")?;

        let client = self.lsp_pool.get_or_create(params.workspace.clone()).await?;

        let resolved = Self::resolve_file(&params.workspace, params.file);
        let file_str = resolved.to_string_lossy().to_string();
        client.open_document(&file_str).await?;
        let locations = with_warmup(
            "definition",
            &WARMUP_DELAYS,
            |locs: &Vec<Location>| !locs.is_empty(),
            || client.goto_definition(&file_str, params.line, params.column),
            None, // Definition lookups are position-based, rg check not applicable
        )
        .await?;

        let location = locations.into_iter().next();
        let result = DefinitionResult { location };
        Ok(serde_json::to_value(result)?)
    }

    /// Handle a workspace symbols request.
    async fn handle_workspace_symbols(&self, params: Value) -> Result<Value> {
        let params: WorkspaceSymbolsParams =
            serde_json::from_value(params).context("Invalid workspace symbols parameters")?;

        let workspace = params.workspace;
        let client = self.lsp_pool.get_or_create(workspace.clone()).await?;

        let mut symbols =
            Self::workspace_symbols_with_warmup(&client, &params.query, &workspace).await?;

        // Filter by exact name if specified (avoids serializing thousands of fuzzy matches)
        if let Some(ref exact_name) = params.exact_name {
            symbols.retain(|s| s.name == *exact_name);
        }

        // Apply limit if specified
        if let Some(limit) = params.limit {
            symbols.truncate(limit);
        }

        let result = WorkspaceSymbolsResult { symbols };
        Ok(serde_json::to_value(result)?)
    }

    /// Handle a document symbols request.
    async fn handle_document_symbols(&self, params: Value) -> Result<Value> {
        let params: DocumentSymbolsParams =
            serde_json::from_value(params).context("Invalid document symbols parameters")?;

        let client = self.lsp_pool.get_or_create(params.workspace.clone()).await?;

        let resolved = Self::resolve_file(&params.workspace, params.file);
        let file_str = resolved.to_string_lossy().to_string();
        client.open_document(&file_str).await?;
        let symbols = with_warmup(
            "document symbols",
            &WARMUP_DELAYS,
            |syms: &Vec<DocumentSymbol>| !syms.is_empty(),
            || client.document_symbols(&file_str),
            None, // Document symbols are file-based, rg check not applicable
        )
        .await?;

        let result = DocumentSymbolsResult { symbols };
        Ok(serde_json::to_value(result)?)
    }

    /// Handle a references request.
    async fn handle_references(&self, params: Value) -> Result<Value> {
        let params: ReferencesParams =
            serde_json::from_value(params).context("Invalid references parameters")?;

        let client = self.lsp_pool.get_or_create(params.workspace.clone()).await?;

        let resolved = Self::resolve_file(&params.workspace, params.file);
        let file_str = resolved.to_string_lossy().to_string();
        client.open_document(&file_str).await?;
        let locations = with_warmup(
            "references",
            &WARMUP_DELAYS,
            |locs: &Vec<Location>| !locs.is_empty(),
            || {
                client.find_references(
                    &file_str,
                    params.line,
                    params.column,
                    params.include_declaration,
                )
            },
            None, // References are position-based, rg check not applicable
        )
        .await?;

        let result = ReferencesResult { locations };
        Ok(serde_json::to_value(result)?)
    }

    /// Handle a batch references request (multiple queries, one connection).
    async fn handle_batch_references(&self, params: Value) -> Result<Value> {
        let params: BatchReferencesParams =
            serde_json::from_value(params).context("Invalid batch references parameters")?;

        let client = self.lsp_pool.get_or_create(params.workspace.clone()).await?;

        let mut entries = Vec::with_capacity(params.queries.len());
        for q in &params.queries {
            let resolved = Self::resolve_file(&params.workspace, q.file.clone());
            let file_str = resolved.to_string_lossy().to_string();
            client.open_document(&file_str).await?;
            let locations = with_warmup(
                "batch references",
                &WARMUP_DELAYS,
                |locs: &Vec<Location>| !locs.is_empty(),
                || client.find_references(&file_str, q.line, q.column, params.include_declaration),
                None, // Batch references are position-based, rg check not applicable
            )
            .await?;
            entries.push(BatchReferencesEntry { label: q.label.clone(), locations });
        }

        let result = BatchReferencesResult { entries };
        Ok(serde_json::to_value(result)?)
    }

    /// Handle an inspect request (hover, and optionally references).
    ///
    /// Requests are sequential because the LSP client communicates through a
    /// single stdin/stdout pipe — concurrent requests race on response routing.
    async fn handle_inspect(&self, params: Value) -> Result<Value> {
        let params: InspectParams =
            serde_json::from_value(params).context("Invalid inspect parameters")?;

        let client = self.lsp_pool.get_or_create(params.workspace.clone()).await?;

        let resolved = Self::resolve_file(&params.workspace, params.file);
        let file_str = resolved.to_string_lossy().to_string();
        client.open_document(&file_str).await?;

        let hover = Self::hover_with_warmup(&client, &file_str, params.line, params.column).await?;

        let references = if params.include_references {
            client.find_references(&file_str, params.line, params.column, false).await?
        } else {
            Vec::new()
        };

        let result = InspectResult { hover, references };
        Ok(serde_json::to_value(result)?)
    }

    /// Handle a members request.
    ///
    /// Retrieves document symbols for the file, finds the target class,
    /// extracts its children, and calls hover on each to get type signatures.
    /// This is N+1 LSP calls per class (1 documentSymbol + N hovers).
    async fn handle_members(&self, params: Value) -> Result<Value> {
        let params: MembersParams =
            serde_json::from_value(params).context("Invalid members parameters")?;

        let client = self.lsp_pool.get_or_create(params.workspace.clone()).await?;

        let resolved = Self::resolve_file(&params.workspace, params.file);
        let file_str = resolved.to_string_lossy().to_string();
        client.open_document(&file_str).await?;

        let doc_symbols = client.document_symbols(&file_str).await?;

        // Find the target class anywhere in the symbol tree (may be nested)
        let target = Self::find_symbol_recursive(&doc_symbols, &params.class_name);

        let Some(class_sym) = target else {
            // Symbol not found in file
            let result = MembersResult {
                class_name: params.class_name,
                file_uri: file_str,
                class_line: 0,
                class_column: 0,
                symbol_kind: None,
                members: Vec::new(),
            };
            return Ok(serde_json::to_value(result)?);
        };

        // Check that it's actually a class
        if !matches!(class_sym.kind, SymbolKind::Class) {
            let result = MembersResult {
                class_name: params.class_name,
                file_uri: file_str,
                class_line: class_sym.selection_range.start.line,
                class_column: class_sym.selection_range.start.character,
                symbol_kind: Some(class_sym.kind.clone()),
                members: Vec::new(),
            };
            return Ok(serde_json::to_value(result)?);
        }

        let children = class_sym.children.as_deref().unwrap_or(&[]);

        // Filter members based on include_all flag
        let filtered: Vec<_> = children
            .iter()
            .filter(|child| {
                if params.include_all {
                    return true;
                }
                // Exclude private (_prefixed) and dunder (__dunder__) members
                !child.name.starts_with('_')
            })
            .collect();

        // Get hover info for each member (N LSP calls — sequential, single pipe)
        let mut members = Vec::with_capacity(filtered.len());
        for child in &filtered {
            let hover_line = child.selection_range.start.line;
            let hover_col = child.selection_range.start.character;
            let hover = Self::hover_with_warmup(&client, &file_str, hover_line, hover_col).await?;

            let signature =
                hover.as_ref().map(|h| Self::extract_member_signature(&h.contents, &child.name));

            members.push(MemberInfo {
                name: child.name.clone(),
                kind: child.kind.clone(),
                signature,
                line: child.selection_range.start.line,
                column: child.selection_range.start.character,
            });
        }

        let result = MembersResult {
            class_name: params.class_name,
            file_uri: file_str,
            class_line: class_sym.selection_range.start.line,
            class_column: class_sym.selection_range.start.character,
            symbol_kind: Some(class_sym.kind.clone()),
            members,
        };
        Ok(serde_json::to_value(result)?)
    }

    /// Recursively search document symbols for a symbol with the given name.
    ///
    /// `document_symbols` returns a hierarchical tree — classes nested inside
    /// other classes or functions only appear as children, not at the top level.
    fn find_symbol_recursive<'a>(
        symbols: &'a [crate::lsp::protocol::DocumentSymbol],
        name: &str,
    ) -> Option<&'a crate::lsp::protocol::DocumentSymbol> {
        for s in symbols {
            if s.name == name {
                return Some(s);
            }
            if let Some(children) = &s.children {
                if let Some(found) = Self::find_symbol_recursive(children, name) {
                    return Some(found);
                }
            }
        }
        None
    }

    /// Extract a clean member signature from hover contents.
    ///
    /// ty's hover markdown looks like:
    ///   ```python\ndef method(self, x: int) -> str\n```\n---\nDocstring
    ///
    /// We want just the signature: `method(self, x: int) -> str`
    ///
    /// `member_name` is used to prefix bare type signatures (e.g. class
    /// variables where ty returns just the type like `int`).
    fn extract_member_signature(
        contents: &crate::lsp::protocol::HoverContents,
        member_name: &str,
    ) -> String {
        use crate::lsp::protocol::HoverContents;

        let full = match contents {
            HoverContents::Scalar(s) => s.clone(),
            HoverContents::Markup(markup) => markup.value.clone(),
            HoverContents::MarkedString(ms) => ms.value.clone(),
            HoverContents::Array(arr) => {
                use crate::lsp::protocol::MarkedStringOrString;
                arr.iter()
                    .map(|item| match item {
                        MarkedStringOrString::String(s) => s.clone(),
                        MarkedStringOrString::MarkedString(ms) => ms.value.clone(),
                    })
                    .collect::<Vec<_>>()
                    .join("\n")
            }
        };

        // Strip docstring (everything after "\n---")
        let type_part = match full.find("\n---") {
            Some(pos) => &full[..pos],
            None => &full,
        };

        // Strip markdown code fences
        let trimmed = type_part.trim();
        let cleaned = trimmed
            .strip_prefix("```python")
            .or_else(|| trimmed.strip_prefix("```xml"))
            .or_else(|| trimmed.strip_prefix("```text"))
            .or_else(|| trimmed.strip_prefix("```"))
            .unwrap_or(trimmed);

        let cleaned = cleaned.trim().strip_suffix("```").unwrap_or(cleaned).trim();

        // Strip leading `def ` for method signatures — show just `name(params) -> ret`
        let cleaned = cleaned.strip_prefix("def ").unwrap_or(cleaned);

        // Strip leading `(method) `, `(property) `, etc. prefixes ty may add
        let cleaned = if let Some(rest) = cleaned.strip_prefix('(') {
            if let Some(pos) = rest.find(") ") {
                let after = &rest[pos + 2..];
                after.strip_prefix("def ").unwrap_or(after)
            } else {
                cleaned
            }
        } else {
            cleaned
        };

        // Collapse multi-line signatures (ty wraps long parameter lists)
        // e.g. "assist(\n    self,\n    task: str\n) -> str" → "assist(self, task: str) -> str"
        let cleaned = Self::collapse_signature(cleaned);

        // If the signature is a bare type (no name prefix, no parens), prepend
        // the member name so it reads like `registry: dict[str, X]` instead of
        // just `dict[str, X]`.
        if !cleaned.contains('(') && !cleaned.contains(':') && cleaned != member_name {
            format!("{member_name}: {cleaned}")
        } else {
            cleaned
        }
    }

    /// Collapse a multi-line signature into a single line.
    ///
    /// ty formats long signatures like:
    /// ```text
    /// assist(
    ///     self,
    ///     task: str,
    ///     duration_minutes: int = 30
    /// ) -> str
    /// ```
    ///
    /// This collapses them to: `assist(self, task: str, duration_minutes: int = 30) -> str`
    fn collapse_signature(sig: &str) -> String {
        if !sig.contains('\n') {
            return sig.to_string();
        }
        // Replace newlines + surrounding whitespace with a single space,
        // then clean up spaces around parentheses and commas.
        let collapsed: String = sig.lines().map(str::trim).collect::<Vec<_>>().join(" ");
        // Normalize "( " → "(", " )" → ")", ",  " → ", "
        let collapsed = collapsed.replace("( ", "(").replace(" )", ")");
        // Collapse multiple spaces
        let mut result = String::with_capacity(collapsed.len());
        let mut prev_space = false;
        for ch in collapsed.chars() {
            if ch == ' ' {
                if !prev_space {
                    result.push(ch);
                }
                prev_space = true;
            } else {
                prev_space = false;
                result.push(ch);
            }
        }
        result
    }

    /// Handle a diagnostics request.
    #[allow(clippy::unused_async)] // Matches async handler interface
    async fn handle_diagnostics(&self, _params: Value) -> Result<Value> {
        // Diagnostics are not yet implemented in the LSP client
        // Return empty diagnostics for now
        let result = DiagnosticsResult { diagnostics: vec![] };
        Ok(serde_json::to_value(result)?)
    }

    /// Handle a ping request.
    #[allow(clippy::unused_async)] // Matches async handler interface
    async fn handle_ping(&self, _params: Value) -> Result<Value> {
        let workspace_paths: Vec<String> = self
            .lsp_pool
            .active_workspaces()
            .into_iter()
            .map(|p| p.to_string_lossy().to_string())
            .collect();

        let cwd = std::env::current_dir().ok().map(|p| p.to_string_lossy().to_string());

        let result = PingResult {
            status: "running".to_string(),
            version: env!("CARGO_PKG_VERSION").to_string(),
            uptime: self.start_time.elapsed().as_secs(),
            active_workspaces: workspace_paths.len(),
            cache_size: 0, // Cache not yet implemented
            socket_path: Some(self.socket_path.to_string_lossy().into_owned()),
            tcp_port: Some(self.tcp_port),
            workspace_paths,
            pid: std::process::id(),
            cwd,
        };
        Ok(serde_json::to_value(result)?)
    }

    /// Hover with retry on cold start.
    ///
    /// The ty LSP server may return null hover when a document was recently
    /// opened and analysis hasn't completed. Retry with back-off.
    async fn hover_with_warmup(
        client: &TyLspClient,
        file: &str,
        line: u32,
        column: u32,
    ) -> Result<Option<Hover>> {
        with_warmup(
            "hover",
            &WARMUP_DELAYS,
            |h: &Option<Hover>| h.is_some(),
            || client.hover(file, line, column),
            None, // No symbol name available for position-based hover
        )
        .await
    }

    /// Workspace symbols with retry on cold start.
    ///
    /// On cold start the ty LSP server may not have finished indexing the
    /// workspace yet, returning zero symbols. Retry with back-off.
    ///
    /// Uses ripgrep as a circuit-breaker: after the first empty result, if `rg`
    /// confirms the symbol doesn't exist in any `.py` file, skips retries.
    async fn workspace_symbols_with_warmup(
        client: &TyLspClient,
        query: &str,
        workspace_root: &std::path::Path,
    ) -> Result<Vec<crate::lsp::protocol::SymbolInformation>> {
        with_warmup(
            "workspace symbols",
            &WARMUP_DELAYS,
            |syms: &Vec<crate::lsp::protocol::SymbolInformation>| !syms.is_empty(),
            || client.workspace_symbols(query),
            Some(RgCheck { symbol: query, workspace_root }),
        )
        .await
    }

    /// Handle a shutdown request.
    #[allow(clippy::unused_async)] // Matches async handler interface
    async fn handle_shutdown(&self, _params: Value) -> Result<Value> {
        tracing::info!("Shutdown requested");

        // Send shutdown signal
        let _ = self.shutdown_tx.send(());

        let result = ShutdownResult { message: "Daemon shutting down".to_string() };
        Ok(serde_json::to_value(result)?)
    }

    /// Idle timeout task that shuts down the daemon after inactivity.
    async fn idle_timeout_task(&self) {
        let idle_timeout = Duration::from_secs(300); // 5 minutes
        let check_interval = Duration::from_secs(60); // 1 minute

        loop {
            tokio::time::sleep(check_interval).await;

            // Clean up idle LSP clients
            let removed = self.lsp_pool.cleanup_idle(idle_timeout);
            if removed > 0 {
                tracing::info!("Removed {removed} idle LSP clients");
            }

            // Check if daemon should shut down (all clients idle)
            if self.lsp_pool.is_empty() && self.start_time.elapsed() > idle_timeout {
                tracing::info!("Daemon idle timeout, shutting down");
                let _ = self.shutdown_tx.send(());
                break;
            }
        }
    }

    /// Graceful shutdown cleanup.
    #[allow(clippy::unused_async)] // Called from async context
    async fn cleanup(&self) -> Result<()> {
        tracing::info!("Cleaning up daemon resources");

        // Remove socket file
        if self.socket_path.exists() {
            std::fs::remove_file(&self.socket_path).context("Failed to remove socket file")?;
        }

        // Remove pidfile
        pidfile::remove_pidfile(&self.pidfile_path);

        Ok(())
    }
}

/// Context for the optional ripgrep-based early termination check.
///
/// When provided, `with_warmup` will run `rg` after the first empty result
/// to check whether the symbol text exists in any `.py` file. If it doesn't,
/// retries are skipped — the symbol provably does not exist.
struct RgCheck<'a> {
    symbol: &'a str,
    workspace_root: &'a std::path::Path,
}

/// Retry an LSP operation with exponential back-off when it returns an "empty" result.
///
/// On cold start the ty LSP server may not have finished indexing a document
/// or the workspace, causing operations to return empty/null results. This
/// helper retries with the given delays until `is_ready` returns `true`.
///
/// When `rg_check` is provided, uses ripgrep as a fast negative filter after the
/// first empty result: if the symbol doesn't appear in any `.py` file, bail
/// immediately instead of retrying.
async fn with_warmup<T, F, Fut>(
    description: &str,
    delays: &[u64],
    is_ready: impl Fn(&T) -> bool,
    mut operation: F,
    rg_check: Option<RgCheck<'_>>,
) -> Result<T>
where
    F: FnMut() -> Fut,
    Fut: std::future::Future<Output = Result<T>>,
{
    let result = operation().await?;
    if is_ready(&result) {
        return Ok(result);
    }

    // After the first empty result, check if the symbol exists in the workspace
    // using ripgrep. If rg confirms it doesn't exist, skip all retries.
    // Run in spawn_blocking to avoid blocking the tokio runtime.
    if let Some(ref check) = rg_check {
        let symbol = check.symbol.to_owned();
        let workspace = check.workspace_root.to_owned();
        let exists = tokio::task::spawn_blocking(move || {
            crate::ripgrep::symbol_might_exist_in_workspace(&symbol, &workspace)
        })
        .await
        .unwrap_or(true); // If spawn_blocking panics, conservatively continue retries

        if !exists {
            tracing::debug!(
                "{description}: rg confirms symbol '{}' not found, skipping retries",
                check.symbol
            );
            return Ok(result);
        }
    }

    for delay_ms in delays {
        tracing::debug!("{description} not ready, retrying in {delay_ms}ms...");
        tokio::time::sleep(Duration::from_millis(*delay_ms)).await;
        let result = operation().await?;
        if is_ready(&result) {
            return Ok(result);
        }
    }

    tracing::debug!("{description} still not ready after retries");
    operation().await
}

/// Send a framed error response to the client.
async fn send_error_response<W: AsyncWrite + Unpin>(
    writer: &mut W,
    error: DaemonError,
) -> Result<()> {
    let error_response = DaemonResponse::error(0, error);
    let response_json = serde_json::to_string(&error_response)?;
    let framed = format!("Content-Length: {}\r\n\r\n{response_json}", response_json.len());
    writer.write_all(framed.as_bytes()).await?;
    writer.flush().await?;
    Ok(())
}

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

    #[test]
    fn test_get_socket_path() {
        let path = DaemonServer::get_socket_path().expect("should return a valid socket path");
        assert!(path.to_string_lossy().contains("ty-find"));
    }

    #[test]
    fn test_server_creation() {
        let socket_path = PathBuf::from("/tmp/test-ty-find.sock");
        let server = DaemonServer::new(socket_path.clone());
        assert_eq!(server.socket_path, socket_path);
    }

    #[tokio::test]
    async fn test_ping_handler() {
        let socket_path = PathBuf::from("/tmp/test-ty-find.sock");
        let server = DaemonServer::new(socket_path);

        let params = serde_json::json!({});
        let value = server.handle_ping(params).await.expect("ping should succeed");

        assert_eq!(value["status"], "running");
        assert_eq!(value["active_workspaces"], 0);
        assert_eq!(value["cache_size"], 0);
        // Uptime should be a small number since the server was just created
        assert!(value["uptime"].as_u64().unwrap() < 5);
    }

    #[tokio::test]
    async fn test_ping_includes_transport_info() {
        let socket_path = PathBuf::from("/tmp/test-ty-find-transport.sock");
        let server = DaemonServer::new(socket_path.clone());

        let params = serde_json::json!({});
        let value = server.handle_ping(params).await.expect("ping should succeed");

        // Socket path should be included
        assert_eq!(value["socket_path"].as_str().unwrap(), socket_path.to_string_lossy().as_ref());
        // TCP port defaults to 0 before binding
        assert!(value["tcp_port"].is_number());
    }

    #[test]
    fn test_find_symbol_recursive_top_level() {
        use crate::lsp::protocol::{DocumentSymbol, Position, Range, SymbolKind};

        let range = Range {
            start: Position { line: 0, character: 0 },
            end: Position { line: 5, character: 0 },
        };
        let symbols = vec![DocumentSymbol {
            name: "Animal".to_string(),
            detail: None,
            kind: SymbolKind::Class,
            tags: None,
            deprecated: None,
            range: range.clone(),
            selection_range: range,
            children: None,
        }];

        let found = DaemonServer::find_symbol_recursive(&symbols, "Animal");
        assert!(found.is_some());
        assert_eq!(found.unwrap().name, "Animal");

        let not_found = DaemonServer::find_symbol_recursive(&symbols, "Dog");
        assert!(not_found.is_none());
    }

    #[test]
    fn test_find_symbol_recursive_nested() {
        use crate::lsp::protocol::{DocumentSymbol, Position, Range, SymbolKind};

        let range = Range {
            start: Position { line: 0, character: 0 },
            end: Position { line: 20, character: 0 },
        };
        let inner_range = Range {
            start: Position { line: 10, character: 4 },
            end: Position { line: 15, character: 0 },
        };

        let nested_class = DocumentSymbol {
            name: "InnerWidget".to_string(),
            detail: None,
            kind: SymbolKind::Class,
            tags: None,
            deprecated: None,
            range: inner_range.clone(),
            selection_range: inner_range,
            children: None,
        };

        let outer_class = DocumentSymbol {
            name: "OuterPanel".to_string(),
            detail: None,
            kind: SymbolKind::Class,
            tags: None,
            deprecated: None,
            range: range.clone(),
            selection_range: range,
            children: Some(vec![nested_class]),
        };

        let symbols = vec![outer_class];

        // Should find the nested class
        let found = DaemonServer::find_symbol_recursive(&symbols, "InnerWidget");
        assert!(found.is_some());
        assert_eq!(found.unwrap().name, "InnerWidget");

        // Should still find the outer class
        let found = DaemonServer::find_symbol_recursive(&symbols, "OuterPanel");
        assert!(found.is_some());
    }

    #[test]
    fn test_extract_member_signature_method() {
        use crate::lsp::protocol::{HoverContents, MarkupContent, MarkupKind};

        let contents = HoverContents::Markup(MarkupContent {
            kind: MarkupKind::Markdown,
            value: "```python\ndef speak(self) -> str\n```".to_string(),
        });
        let sig = DaemonServer::extract_member_signature(&contents, "speak");
        assert_eq!(sig, "speak(self) -> str");
    }

    #[test]
    fn test_extract_member_signature_property() {
        use crate::lsp::protocol::{HoverContents, MarkupContent, MarkupKind};

        let contents = HoverContents::Markup(MarkupContent {
            kind: MarkupKind::Markdown,
            value: "```python\n(property) name: str\n```".to_string(),
        });
        let sig = DaemonServer::extract_member_signature(&contents, "name");
        assert_eq!(sig, "name: str");
    }

    #[test]
    fn test_extract_member_signature_with_docstring() {
        use crate::lsp::protocol::{HoverContents, MarkupContent, MarkupKind};

        let contents = HoverContents::Markup(MarkupContent {
            kind: MarkupKind::Markdown,
            value: "```python\ndef describe(self) -> str\n```\n---\nDescribe the animal."
                .to_string(),
        });
        let sig = DaemonServer::extract_member_signature(&contents, "describe");
        assert_eq!(sig, "describe(self) -> str");
        assert!(!sig.contains("Describe"));
    }

    #[test]
    fn test_extract_member_signature_class_variable() {
        use crate::lsp::protocol::{HoverContents, MarkupContent, MarkupKind};

        let contents = HoverContents::Markup(MarkupContent {
            kind: MarkupKind::Markdown,
            value: "```python\nMAX_LEGS: int\n```".to_string(),
        });
        let sig = DaemonServer::extract_member_signature(&contents, "MAX_LEGS");
        assert_eq!(sig, "MAX_LEGS: int");
    }

    #[test]
    fn test_extract_member_signature_scalar() {
        use crate::lsp::protocol::HoverContents;

        let contents = HoverContents::Scalar("int".to_string());
        let sig = DaemonServer::extract_member_signature(&contents, "count");
        assert_eq!(sig, "count: int");
    }

    #[test]
    fn test_extract_member_signature_multiline() {
        use crate::lsp::protocol::{HoverContents, MarkupContent, MarkupKind};

        let contents = HoverContents::Markup(MarkupContent {
            kind: MarkupKind::Markdown,
            value: "```python\ndef assist(\n    self,\n    task: str,\n    duration_minutes: int = 30\n) -> str\n```".to_string(),
        });
        let sig = DaemonServer::extract_member_signature(&contents, "assist");
        assert_eq!(sig, "assist(self, task: str, duration_minutes: int = 30) -> str");
    }

    #[test]
    fn test_extract_member_signature_bare_type() {
        use crate::lsp::protocol::{HoverContents, MarkupContent, MarkupKind};

        let contents = HoverContents::Markup(MarkupContent {
            kind: MarkupKind::Markdown,
            value: "```python\ndict[str, ServiceDog]\n```".to_string(),
        });
        let sig = DaemonServer::extract_member_signature(&contents, "registry");
        assert_eq!(sig, "registry: dict[str, ServiceDog]");
    }

    #[test]
    fn test_extract_member_signature_bare_property() {
        use crate::lsp::protocol::{HoverContents, MarkupContent, MarkupKind};

        let contents = HoverContents::Markup(MarkupContent {
            kind: MarkupKind::Markdown,
            value: "```python\nproperty\n```".to_string(),
        });
        let sig = DaemonServer::extract_member_signature(&contents, "is_certified");
        assert_eq!(sig, "is_certified: property");
    }
}
