"""LSP Client for communicating with language servers."""

from __future__ import annotations

import asyncio
import json
import subprocess
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable
from concurrent.futures import Future, ThreadPoolExecutor

from ieum.analysis.lsp.protocol import (
    LSPMessage,
    Position,
    Range,
    Location,
    TextDocumentIdentifier,
    TextDocumentPositionParams,
    DocumentSymbol,
    SymbolInformation,
    Hover,
    CompletionItem,
    Diagnostic,
)
from ieum.analysis.lsp.servers import (
    LSPServerManager,
    Language,
    EXTENSION_TO_LANGUAGE,
)


@dataclass
class LSPResponse:
    """Response from an LSP request."""

    success: bool
    result: Any
    error: str | None = None


class LSPClient:
    """Client for communicating with LSP servers."""

    def __init__(self, workspace_path: str | Path | None = None) -> None:
        """Initialize the LSP client."""
        self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
        self.server_manager = LSPServerManager()
        self._processes: dict[Language, subprocess.Popen] = {}
        self._pending_requests: dict[int, Future] = {}
        self._initialized: set[Language] = set()
        self._executor = ThreadPoolExecutor(max_workers=4)
        self._read_threads: dict[Language, threading.Thread] = {}
        self._lock = threading.Lock()

    def _get_language(self, file_path: str | Path) -> Language | None:
        """Get language for a file."""
        ext = Path(file_path).suffix.lower()
        return EXTENSION_TO_LANGUAGE.get(ext)

    def _ensure_server(self, language: Language) -> subprocess.Popen | None:
        """Ensure a server is running for the language."""
        if language in self._processes:
            proc = self._processes[language]
            if proc.poll() is None:
                return proc

        # Start new server
        proc = self.server_manager.start_server(language, self.workspace_path)
        if proc:
            self._processes[language] = proc
            # Start reader thread
            self._start_reader_thread(language, proc)
            # Initialize the server
            self._initialize_server(language)
        return proc

    def _start_reader_thread(self, language: Language, proc: subprocess.Popen) -> None:
        """Start a thread to read server responses."""
        def reader():
            while proc.poll() is None:
                try:
                    # Read Content-Length header
                    header = b""
                    while b"\r\n\r\n" not in header:
                        byte = proc.stdout.read(1)
                        if not byte:
                            break
                        header += byte

                    if not header:
                        continue

                    # Parse Content-Length
                    header_str = header.decode("utf-8")
                    content_length = 0
                    for line in header_str.split("\r\n"):
                        if line.startswith("Content-Length:"):
                            content_length = int(line.split(":")[1].strip())
                            break

                    if content_length == 0:
                        continue

                    # Read content
                    content = proc.stdout.read(content_length)
                    if content:
                        self._handle_message(language, json.loads(content.decode("utf-8")))

                except Exception:
                    continue

        thread = threading.Thread(target=reader, daemon=True)
        thread.start()
        self._read_threads[language] = thread

    def _handle_message(self, language: Language, message: dict) -> None:
        """Handle an incoming message from the server."""
        msg_id = message.get("id")

        if msg_id is not None and msg_id in self._pending_requests:
            future = self._pending_requests.pop(msg_id)
            if "error" in message:
                future.set_result(LSPResponse(
                    success=False,
                    result=None,
                    error=message["error"].get("message", str(message["error"])),
                ))
            else:
                future.set_result(LSPResponse(
                    success=True,
                    result=message.get("result"),
                ))

    def _send_request(
        self,
        language: Language,
        method: str,
        params: dict | None = None,
        timeout: float = 30.0,
    ) -> LSPResponse:
        """Send a request to the server and wait for response."""
        proc = self._ensure_server(language)
        if not proc:
            return LSPResponse(
                success=False,
                result=None,
                error=f"No server available for {language.value}",
            )

        msg_id, message = LSPMessage.request(method, params)

        # Create future for response
        future: Future = Future()
        with self._lock:
            self._pending_requests[msg_id] = future

        try:
            # Send message
            proc.stdin.write(message.encode("utf-8"))
            proc.stdin.flush()

            # Wait for response
            return future.result(timeout=timeout)
        except TimeoutError:
            with self._lock:
                self._pending_requests.pop(msg_id, None)
            return LSPResponse(
                success=False,
                result=None,
                error="Request timed out",
            )
        except Exception as e:
            return LSPResponse(
                success=False,
                result=None,
                error=str(e),
            )

    def _send_notification(self, language: Language, method: str, params: dict | None = None) -> None:
        """Send a notification to the server (no response expected)."""
        proc = self._ensure_server(language)
        if not proc:
            return

        message = LSPMessage.notification(method, params)
        try:
            proc.stdin.write(message.encode("utf-8"))
            proc.stdin.flush()
        except Exception:
            pass

    def _initialize_server(self, language: Language) -> bool:
        """Initialize the LSP server."""
        if language in self._initialized:
            return True

        workspace_uri = f"file://{self.workspace_path.as_posix()}"

        params = {
            "processId": None,
            "rootUri": workspace_uri,
            "capabilities": {
                "textDocument": {
                    "completion": {"completionItem": {"snippetSupport": True}},
                    "hover": {"contentFormat": ["markdown", "plaintext"]},
                    "definition": {"linkSupport": True},
                    "references": {},
                    "documentSymbol": {"hierarchicalDocumentSymbolSupport": True},
                },
                "workspace": {
                    "workspaceFolders": True,
                },
            },
            "workspaceFolders": [{"uri": workspace_uri, "name": self.workspace_path.name}],
        }

        response = self._send_request(language, "initialize", params)
        if response.success:
            # Send initialized notification
            self._send_notification(language, "initialized", {})
            self._initialized.add(language)
            return True
        return False

    def _open_document(self, file_path: str | Path) -> bool:
        """Notify server that a document is opened."""
        language = self._get_language(file_path)
        if not language:
            return False

        path = Path(file_path)
        try:
            content = path.read_text(encoding="utf-8", errors="replace")
        except Exception:
            return False

        uri = f"file://{path.resolve().as_posix()}"

        # Detect language ID
        lang_id = language.value
        if path.suffix in (".tsx", ".jsx"):
            lang_id = "typescriptreact" if path.suffix == ".tsx" else "javascriptreact"

        params = {
            "textDocument": {
                "uri": uri,
                "languageId": lang_id,
                "version": 1,
                "text": content,
            }
        }

        self._send_notification(language, "textDocument/didOpen", params)
        return True

    # Public API methods

    def get_definition(self, file_path: str | Path, line: int, character: int) -> list[Location]:
        """Get definition location for a symbol at the given position."""
        language = self._get_language(file_path)
        if not language:
            return []

        self._open_document(file_path)

        path = Path(file_path).resolve()
        uri = f"file://{path.as_posix()}"

        params = {
            "textDocument": {"uri": uri},
            "position": {"line": line, "character": character},
        }

        response = self._send_request(language, "textDocument/definition", params)
        if not response.success or not response.result:
            return []

        result = response.result
        if isinstance(result, dict):
            result = [result]

        locations = []
        for loc in result:
            if "uri" in loc and "range" in loc:
                locations.append(Location.from_dict(loc))
            elif "targetUri" in loc:
                # LocationLink format
                locations.append(Location(
                    uri=loc["targetUri"],
                    range=Range.from_dict(loc["targetRange"]),
                ))

        return locations

    def get_references(
        self,
        file_path: str | Path,
        line: int,
        character: int,
        include_declaration: bool = True,
    ) -> list[Location]:
        """Get all references to a symbol at the given position."""
        language = self._get_language(file_path)
        if not language:
            return []

        self._open_document(file_path)

        path = Path(file_path).resolve()
        uri = f"file://{path.as_posix()}"

        params = {
            "textDocument": {"uri": uri},
            "position": {"line": line, "character": character},
            "context": {"includeDeclaration": include_declaration},
        }

        response = self._send_request(language, "textDocument/references", params)
        if not response.success or not response.result:
            return []

        return [Location.from_dict(loc) for loc in response.result]

    def get_hover(self, file_path: str | Path, line: int, character: int) -> Hover | None:
        """Get hover information for a symbol at the given position."""
        language = self._get_language(file_path)
        if not language:
            return None

        self._open_document(file_path)

        path = Path(file_path).resolve()
        uri = f"file://{path.as_posix()}"

        params = {
            "textDocument": {"uri": uri},
            "position": {"line": line, "character": character},
        }

        response = self._send_request(language, "textDocument/hover", params)
        if not response.success or not response.result:
            return None

        return Hover.from_dict(response.result)

    def get_document_symbols(self, file_path: str | Path) -> list[DocumentSymbol | SymbolInformation]:
        """Get all symbols in a document."""
        language = self._get_language(file_path)
        if not language:
            return []

        self._open_document(file_path)

        path = Path(file_path).resolve()
        uri = f"file://{path.as_posix()}"

        params = {"textDocument": {"uri": uri}}

        response = self._send_request(language, "textDocument/documentSymbol", params)
        if not response.success or not response.result:
            return []

        symbols = []
        for item in response.result:
            if "range" in item:
                # DocumentSymbol format (hierarchical)
                symbols.append(DocumentSymbol.from_dict(item))
            else:
                # SymbolInformation format (flat)
                symbols.append(SymbolInformation.from_dict(item))

        return symbols

    def get_completions(
        self,
        file_path: str | Path,
        line: int,
        character: int,
    ) -> list[CompletionItem]:
        """Get completion suggestions at the given position."""
        language = self._get_language(file_path)
        if not language:
            return []

        self._open_document(file_path)

        path = Path(file_path).resolve()
        uri = f"file://{path.as_posix()}"

        params = {
            "textDocument": {"uri": uri},
            "position": {"line": line, "character": character},
        }

        response = self._send_request(language, "textDocument/completion", params)
        if not response.success or not response.result:
            return []

        result = response.result
        if isinstance(result, dict):
            items = result.get("items", [])
        else:
            items = result

        return [CompletionItem.from_dict(item) for item in items]

    def get_diagnostics(self, file_path: str | Path) -> list[Diagnostic]:
        """Get diagnostics (errors, warnings) for a document."""
        # Note: Diagnostics are typically pushed by the server via notifications
        # This is a placeholder for manual diagnostic requests if supported
        language = self._get_language(file_path)
        if not language:
            return []

        self._open_document(file_path)
        # Most servers don't support pull diagnostics, they push via notifications
        return []

    def get_workspace_symbols(self, query: str) -> list[SymbolInformation]:
        """Search for symbols across the workspace."""
        # Try all initialized servers
        all_symbols = []

        for language in self._initialized:
            params = {"query": query}
            response = self._send_request(language, "workspace/symbol", params)
            if response.success and response.result:
                for item in response.result:
                    all_symbols.append(SymbolInformation.from_dict(item))

        return all_symbols

    def rename_symbol(
        self,
        file_path: str | Path,
        line: int,
        character: int,
        new_name: str,
    ) -> dict[str, list[dict]]:
        """Rename a symbol across the workspace."""
        language = self._get_language(file_path)
        if not language:
            return {}

        self._open_document(file_path)

        path = Path(file_path).resolve()
        uri = f"file://{path.as_posix()}"

        params = {
            "textDocument": {"uri": uri},
            "position": {"line": line, "character": character},
            "newName": new_name,
        }

        response = self._send_request(language, "textDocument/rename", params)
        if not response.success or not response.result:
            return {}

        # Parse workspace edit
        changes = response.result.get("changes", {})
        document_changes = response.result.get("documentChanges", [])

        if document_changes:
            # Convert documentChanges to changes format
            for doc_change in document_changes:
                if "textDocument" in doc_change:
                    uri = doc_change["textDocument"]["uri"]
                    edits = doc_change.get("edits", [])
                    changes[uri] = edits

        return changes

    def shutdown(self) -> None:
        """Shutdown all servers."""
        for language in list(self._initialized):
            self._send_request(language, "shutdown", timeout=5.0)
            self._send_notification(language, "exit")

        self._initialized.clear()
        self.server_manager.stop_all_servers()
        self._processes.clear()
        self._executor.shutdown(wait=False)

    def __del__(self) -> None:
        """Cleanup on destruction."""
        try:
            self.shutdown()
        except Exception:
            pass
