
from __future__ import annotations

import hashlib
import json
import logging
import os
import pathlib
import platform
import shutil
import tarfile
import threading
import time
import urllib.error
import urllib.request
import uuid
import zipfile

from solidlsp.language_servers._adapter_common import ensure_paths_exist
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, quote_windows_path
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings

log = logging.getLogger(__name__)

class PascalLanguageServer(SolidLanguageServer):

    PASLS_RELEASES_URL = "https://github.com/zen010101/pascal-language-server/releases/latest/download"
    PASLS_API_URL = "https://api.github.com/repos/zen010101/pascal-language-server/releases/latest"

    UPDATE_CHECK_INTERVAL = 86400  # 24 hours

    META_DIR = ".meta"

    NETWORK_TIMEOUT = 10

    def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
        pasls_executable_path = self._setup_runtime_dependencies(solidlsp_settings)

        proc_env: dict[str, str] = {}

        from solidlsp.ls_config import Language

        pascal_settings = solidlsp_settings.get_ls_specific_settings(Language.PASCAL)

        pp = pascal_settings.get("pp", "")
        if pp:
            proc_env["PP"] = pp
            log.info(f"Setting PP={pp} from ls_specific_settings")

        fpcdir = pascal_settings.get("fpcdir", "")
        if fpcdir:
            proc_env["FPCDIR"] = fpcdir
            log.info(f"Setting FPCDIR={fpcdir} from ls_specific_settings")

        lazarusdir = pascal_settings.get("lazarusdir", "")
        if lazarusdir:
            proc_env["LAZARUSDIR"] = lazarusdir
            log.info(f"Setting LAZARUSDIR={lazarusdir} from ls_specific_settings")

        fpc_target = pascal_settings.get("fpc_target", "")
        if fpc_target:
            proc_env["FPCTARGET"] = fpc_target
            log.info(f"Setting FPCTARGET={fpc_target} from ls_specific_settings")

        fpc_target_cpu = pascal_settings.get("fpc_target_cpu", "")
        if fpc_target_cpu:
            proc_env["FPCTARGETCPU"] = fpc_target_cpu
            log.info(f"Setting FPCTARGETCPU={fpc_target_cpu} from ls_specific_settings")

        super().__init__(
            config,
            repository_root_path,
            ProcessLaunchInfo(cmd=pasls_executable_path, cwd=repository_root_path, env=proc_env),
            "pascal",
            solidlsp_settings,
        )
        self.server_ready = threading.Event()

    @classmethod
    def _meta_dir(cls, pasls_dir: str) -> str:
        meta_path = os.path.join(pasls_dir, cls.META_DIR)
        os.makedirs(meta_path, exist_ok=True)
        return meta_path

    @classmethod
    def _meta_file(cls, pasls_dir: str, filename: str) -> str:
        return os.path.join(cls._meta_dir(pasls_dir), filename)

    @staticmethod
    def _normalize_version(version: str | None) -> str:
        if not version:
            return ""
        return version.strip().lstrip("vV")

    @classmethod
    def _is_newer_version(cls, latest: str | None, local: str | None) -> bool:
        if not latest:
            return False
        if not local:
            return True

        latest_norm = cls._normalize_version(latest)
        local_norm = cls._normalize_version(local)

        if not latest_norm:
            return False
        if not local_norm:
            return True

        try:

            def parse_version(v: str) -> list[int]:
                parts = []
                for part in v.split("."):
                    num = ""
                    for c in part:
                        if c.isdigit():
                            num += c
                        else:
                            break
                    parts.append(int(num) if num else 0)
                return parts

            latest_parts = parse_version(latest_norm)
            local_parts = parse_version(local_norm)

            max_len = max(len(latest_parts), len(local_parts))
            latest_parts.extend([0] * (max_len - len(latest_parts)))
            local_parts.extend([0] * (max_len - len(local_parts)))

            return latest_parts > local_parts
        except (OSError, RuntimeError, ValueError, TypeError):
            log.warning(f"Failed to parse versions for comparison: {latest_norm} vs {local_norm}")
            return False

    @classmethod
    def _get_latest_version(cls) -> str | None:
        try:
            headers = {"Accept": "application/vnd.github.v3+json", "User-Agent": "Serena-LSP"}
            github_token = os.environ.get("GITHUB_TOKEN")
            if github_token:
                headers["Authorization"] = f"token {github_token}"

            req = urllib.request.Request(cls.PASLS_API_URL, headers=headers)
            with urllib.request.urlopen(req, timeout=cls.NETWORK_TIMEOUT) as response:
                data = json.loads(response.read().decode())
                return data.get("tag_name")
        except (OSError, RuntimeError, ValueError, TypeError) as e:
            log.debug(f"Failed to get latest pasls version: {type(e).__name__}: {e}")
            return None

    @classmethod
    def _get_local_version(cls, pasls_dir: str) -> str | None:
        version_file = cls._meta_file(pasls_dir, "version")
        if os.path.exists(version_file):
            try:
                with open(version_file, encoding="utf-8") as f:
                    return f.read().strip()
            except OSError:
                return None
        return None

    @classmethod
    def _save_local_version(cls, pasls_dir: str, version: str) -> None:
        version_file = cls._meta_file(pasls_dir, "version")
        try:
            with open(version_file, "w", encoding="utf-8") as f:
                f.write(version)
        except OSError as e:
            log.warning(f"Failed to save version file: {e}")

    @classmethod
    def _should_check_update(cls, pasls_dir: str) -> bool:
        last_check_file = cls._meta_file(pasls_dir, "last_check")
        if not os.path.exists(last_check_file):
            return True
        try:
            with open(last_check_file, encoding="utf-8") as f:
                last_check = float(f.read().strip())
            return (time.time() - last_check) > cls.UPDATE_CHECK_INTERVAL
        except (OSError, ValueError):
            return True

    @classmethod
    def _update_last_check(cls, pasls_dir: str) -> None:
        last_check_file = cls._meta_file(pasls_dir, "last_check")
        try:
            with open(last_check_file, "w", encoding="utf-8") as f:
                f.write(str(time.time()))
        except OSError as e:
            log.warning(f"Failed to update last check time: {e}")

    @classmethod
    def _get_checksums(cls) -> dict[str, str] | None:
        checksums_url = f"{cls.PASLS_RELEASES_URL}/checksums.sha256"
        try:
            req = urllib.request.Request(checksums_url, headers={"User-Agent": "Serena-LSP"})
            with urllib.request.urlopen(req, timeout=cls.NETWORK_TIMEOUT) as response:
                content = response.read().decode("utf-8")
                checksums = {}
                for line in content.strip().split("\n"):
                    line = line.strip()
                    if not line or line.startswith("#"):
                        continue
                    parts = line.split()
                    if len(parts) >= 2:
                        sha256 = parts[0]
                        filename = parts[1].lstrip("*")  # Remove possible * prefix
                        checksums[filename] = sha256
                return checksums
        except (OSError, RuntimeError, ValueError, TypeError) as e:
            log.warning(f"Failed to get checksums: {type(e).__name__}: {e}")
            return None

    @staticmethod
    def _calculate_sha256(file_path: str) -> str:
        sha256_hash = hashlib.sha256()
        with open(file_path, "rb") as f:
            for chunk in iter(lambda: f.read(8192), b""):
                sha256_hash.update(chunk)
        return sha256_hash.hexdigest()

    @classmethod
    def _verify_checksum(cls, file_path: str, expected_sha256: str) -> bool:
        try:
            actual_sha256 = cls._calculate_sha256(file_path)
            if actual_sha256.lower() == expected_sha256.lower():
                log.debug(f"Checksum verified: {file_path}")
                return True
            else:
                log.error(f"Checksum mismatch for {file_path}: expected {expected_sha256}, got {actual_sha256}")
                return False
        except (OSError, RuntimeError, ValueError, TypeError) as e:
            log.error(f"Failed to verify checksum: {e}")
            return False

    @staticmethod
    def _is_file_locked(file_path: str) -> bool:
        if platform.system() != "Windows":
            return False

        if not os.path.exists(file_path):
            return False

        try:
            with open(file_path, "a"):
                pass
            return False
        except (OSError, PermissionError):
            return True

    @classmethod
    def _safe_remove(cls, file_path: str) -> bool:
        if not os.path.exists(file_path):
            return True

        if platform.system() == "Windows" and cls._is_file_locked(file_path):
            temp_name = f"{file_path}.old.{uuid.uuid4().hex[:8]}"
            try:
                os.rename(file_path, temp_name)
                log.info(f"File locked, renamed to: {temp_name}")
                cls._mark_for_cleanup(os.path.dirname(file_path), temp_name)
                return True
            except PermissionError:
                log.warning(f"Cannot remove/rename locked file: {file_path}")
                return False
        else:
            try:
                os.remove(file_path)
                return True
            except OSError as e:
                log.warning(f"Failed to remove file {file_path}: {e}")
                return False

    @classmethod
    def _mark_for_cleanup(cls, pasls_dir: str, file_path: str) -> None:
        cleanup_file = cls._meta_file(pasls_dir, "cleanup_list")
        try:
            with open(cleanup_file, "a", encoding="utf-8") as f:
                f.write(file_path + "\n")
        except OSError as exc:
            log.warning(f"Failed to mark file for cleanup '{file_path}': {exc}")

    @classmethod
    def _cleanup_old_files(cls, pasls_dir: str) -> None:
        cleanup_file = cls._meta_file(pasls_dir, "cleanup_list")
        if not os.path.exists(cleanup_file):
            return

        try:
            with open(cleanup_file, encoding="utf-8") as f:
                files = [line.strip() for line in f if line.strip()]

            remaining = []
            for file_path in files:
                if os.path.exists(file_path):
                    try:
                        os.remove(file_path)
                        log.debug(f"Cleaned up old file: {file_path}")
                    except OSError:
                        remaining.append(file_path)

            if remaining:
                with open(cleanup_file, "w", encoding="utf-8") as f:
                    f.write("\n".join(remaining) + "\n")
            else:
                os.remove(cleanup_file)
        except OSError as exc:
            log.warning(f"Failed to cleanup old pasls files: {exc}")

    @classmethod
    def _download_archive(cls, url: str, target_path: str) -> bool:
        try:
            os.makedirs(os.path.dirname(target_path), exist_ok=True)
            req = urllib.request.Request(url, headers={"User-Agent": "Serena-LSP"})
            with urllib.request.urlopen(req, timeout=60) as response:
                with open(target_path, "wb") as f:
                    while True:
                        chunk = response.read(8192)
                        if not chunk:
                            break
                        f.write(chunk)
            return True
        except (OSError, RuntimeError, ValueError, TypeError) as e:
            log.error(f"Failed to download {url}: {type(e).__name__}: {e}")
            return False

    @classmethod
    def _is_safe_tar_member(cls, member: tarfile.TarInfo, target_dir: str) -> bool:
        if ".." in member.name.split("/") or ".." in member.name.split("\\"):
            return False

        abs_target = os.path.abspath(target_dir)
        abs_member = os.path.abspath(os.path.join(target_dir, member.name))

        return abs_member.startswith(abs_target + os.sep) or abs_member == abs_target

    @classmethod
    def _extract_archive(cls, archive_path: str, target_dir: str, archive_type: str) -> bool:
        try:
            os.makedirs(target_dir, exist_ok=True)

            if archive_type == "gztar":
                with tarfile.open(archive_path, "r:gz") as tar:
                    for member in tar.getmembers():
                        if not cls._is_safe_tar_member(member, target_dir):
                            log.error(f"Unsafe tar member detected (path traversal): {member.name}")
                            return False
                    tar.extractall(target_dir)

            elif archive_type == "zip":
                with zipfile.ZipFile(archive_path, "r") as zip_ref:
                    for name in zip_ref.namelist():
                        if ".." in name.split("/") or ".." in name.split("\\"):
                            log.error(f"Unsafe zip member detected (path traversal): {name}")
                            return False
                        abs_target = os.path.abspath(target_dir)
                        abs_member = os.path.abspath(os.path.join(target_dir, name))
                        if not (abs_member.startswith(abs_target + os.sep) or abs_member == abs_target):
                            log.error(f"Unsafe zip member detected (path traversal): {name}")
                            return False
                    zip_ref.extractall(target_dir)

            else:
                log.error(f"Unsupported archive type: {archive_type}")
                return False

            cls._flatten_single_subdir(target_dir)

            return True
        except (OSError, RuntimeError, ValueError, TypeError) as e:
            log.error(f"Failed to extract archive: {type(e).__name__}: {e}")
            return False

    @classmethod
    def _flatten_single_subdir(cls, target_dir: str) -> None:
        entries = os.listdir(target_dir)
        if len(entries) == 1:
            subdir = os.path.join(target_dir, entries[0])
            if os.path.isdir(subdir):
                for item in os.listdir(subdir):
                    src = os.path.join(subdir, item)
                    dst = os.path.join(target_dir, item)
                    shutil.move(src, dst)
                os.rmdir(subdir)

    @classmethod
    def _get_archive_filename(cls, dep: RuntimeDependency) -> str:
        assert dep.url is not None, "RuntimeDependency.url must be set"
        return dep.url.split("/")[-1]

    @classmethod
    def _atomic_install(cls, pasls_dir: str, deps: RuntimeDependencyCollection, checksums: dict[str, str] | None) -> bool:
        temp_dir = pasls_dir + ".tmp"
        backup_dir = pasls_dir + ".backup"
        temp_archive_dir = os.path.join(os.path.expanduser("~"), "solidlsp_tmp")

        try:
            dep = deps.get_single_dep_for_current_platform()
            assert dep.url is not None, "RuntimeDependency.url must be set"
            assert dep.archive_type is not None, "RuntimeDependency.archive_type must be set"

            archive_filename = cls._get_archive_filename(dep)
            archive_path = os.path.join(temp_archive_dir, archive_filename)

            if os.path.exists(temp_dir):
                shutil.rmtree(temp_dir)
            os.makedirs(temp_archive_dir, exist_ok=True)

            log.info(f"Downloading pasls archive: {archive_filename}")
            if not cls._download_archive(dep.url, archive_path):
                log.error("Failed to download pasls archive")
                return False

            if checksums:
                expected_sha256 = checksums.get(archive_filename)
                if expected_sha256:
                    log.info(f"Verifying SHA256 checksum for {archive_filename}...")
                    if not cls._verify_checksum(archive_path, expected_sha256):
                        log.error(f"SHA256 checksum verification FAILED for {archive_filename}")
                        log.error("Aborting installation due to checksum mismatch - possible security issue!")
                        try:
                            os.remove(archive_path)
                        except OSError as exc:
                            log.warning(f"Failed to remove invalid archive '{archive_path}': {exc}")
                        return False
                    log.info("SHA256 checksum verified successfully")
                else:
                    log.warning(f"No checksum found for {archive_filename} in checksums file")
            else:
                log.warning("No checksums available - skipping verification (not recommended for production)")

            os.makedirs(temp_dir, exist_ok=True)
            log.info("Extracting archive to temporary directory...")
            if not cls._extract_archive(archive_path, temp_dir, dep.archive_type):
                log.error("Failed to extract archive")
                return False

            binary_path = deps.binary_path(temp_dir)
            if os.path.exists(binary_path):
                try:
                    os.chmod(binary_path, 0o755)
                except OSError as exc:
                    log.debug(f"chmod skipped for '{binary_path}': {exc}")  # May fail on Windows

            if os.path.exists(pasls_dir):
                if os.path.exists(backup_dir):
                    shutil.rmtree(backup_dir)
                shutil.move(pasls_dir, backup_dir)

            shutil.move(temp_dir, pasls_dir)

            if os.path.exists(backup_dir):
                backup_meta = os.path.join(backup_dir, cls.META_DIR)
                if os.path.exists(backup_meta):
                    target_meta = os.path.join(pasls_dir, cls.META_DIR)
                    if not os.path.exists(target_meta):
                        shutil.copytree(backup_meta, target_meta)

            try:
                os.remove(archive_path)
                os.rmdir(temp_archive_dir)
            except OSError as exc:
                log.debug(f"Archive cleanup skipped: {exc}")

            log.info("pasls installation completed successfully")
            return True

        except (OSError, RuntimeError, ValueError, TypeError) as e:
            log.error(f"Installation failed: {e}")

            if os.path.exists(backup_dir) and not os.path.exists(pasls_dir):
                try:
                    shutil.move(backup_dir, pasls_dir)
                    log.info("Rolled back to previous version")
                except (OSError, RuntimeError, ValueError, TypeError) as rollback_error:
                    log.error(f"Rollback failed: {rollback_error}")

            if os.path.exists(temp_dir):
                try:
                    shutil.rmtree(temp_dir)
                except (OSError, RuntimeError, ValueError, TypeError) as exc:
                    log.warning(f"Failed to cleanup temp directory '{temp_dir}': {exc}")

            return False

    @classmethod
    def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str:
        pasls_in_path = shutil.which("pasls")
        if pasls_in_path:
            log.info(f"Found pasls in PATH: {pasls_in_path}")
            return quote_windows_path(pasls_in_path)

        pasls_dir = cls.ls_resources_dir(solidlsp_settings)
        os.makedirs(pasls_dir, exist_ok=True)

        cls._cleanup_old_files(pasls_dir)

        deps = RuntimeDependencyCollection(
            [
                RuntimeDependency(
                    id="PascalLanguageServer",
                    description="Pascal Language Server for Linux (x64)",
                    url=f"{cls.PASLS_RELEASES_URL}/pasls-x86_64-linux.tar.gz",
                    platform_id="linux-x64",
                    archive_type="gztar",
                    binary_name="pasls",
                ),
                RuntimeDependency(
                    id="PascalLanguageServer",
                    description="Pascal Language Server for Linux (arm64)",
                    url=f"{cls.PASLS_RELEASES_URL}/pasls-aarch64-linux.tar.gz",
                    platform_id="linux-arm64",
                    archive_type="gztar",
                    binary_name="pasls",
                ),
                RuntimeDependency(
                    id="PascalLanguageServer",
                    description="Pascal Language Server for macOS (x64)",
                    url=f"{cls.PASLS_RELEASES_URL}/pasls-x86_64-darwin.zip",
                    platform_id="osx-x64",
                    archive_type="zip",
                    binary_name="pasls",
                ),
                RuntimeDependency(
                    id="PascalLanguageServer",
                    description="Pascal Language Server for macOS (arm64)",
                    url=f"{cls.PASLS_RELEASES_URL}/pasls-aarch64-darwin.zip",
                    platform_id="osx-arm64",
                    archive_type="zip",
                    binary_name="pasls",
                ),
                RuntimeDependency(
                    id="PascalLanguageServer",
                    description="Pascal Language Server for Windows (x64)",
                    url=f"{cls.PASLS_RELEASES_URL}/pasls-x86_64-win64.zip",
                    platform_id="win-x64",
                    archive_type="zip",
                    binary_name="pasls.exe",
                ),
            ]
        )

        pasls_executable_path = deps.binary_path(pasls_dir)

        need_download = False
        latest_version = None
        checksums = None

        if not os.path.exists(pasls_executable_path):
            log.info("pasls not found, will download...")
            need_download = True
            latest_version = cls._get_latest_version()
            checksums = cls._get_checksums()
        elif cls._should_check_update(pasls_dir):
            log.debug("Checking for pasls updates...")
            latest_version = cls._get_latest_version()
            local_version = cls._get_local_version(pasls_dir)

            if cls._is_newer_version(latest_version, local_version):
                log.info(f"New pasls version available: {latest_version} (current: {local_version})")

                if cls._is_file_locked(pasls_executable_path):
                    log.warning("Cannot update pasls: file is in use. Will retry next time.")
                else:
                    need_download = True
                    checksums = cls._get_checksums()
            else:
                log.debug(f"pasls is up to date: {local_version}")

        if need_download:
            if cls._atomic_install(pasls_dir, deps, checksums):
                if latest_version:
                    cls._save_local_version(pasls_dir, latest_version)
                else:
                    cls._save_local_version(pasls_dir, "unknown")
                cls._update_last_check(pasls_dir)
            else:
                if not os.path.exists(pasls_executable_path):
                    raise RuntimeError("Failed to install pasls and no local version available")
                log.warning("Update failed, using existing version")

        if not need_download and cls._should_check_update(pasls_dir):
            cls._update_last_check(pasls_dir)

        ensure_paths_exist([pasls_executable_path], context="pascal_language_server")

        try:
            os.chmod(pasls_executable_path, 0o755)
        except OSError as exc:
            log.debug(f"chmod skipped for '{pasls_executable_path}': {exc}")  # May fail on Windows, ignore

        log.info(f"Using pasls at: {pasls_executable_path}")
        return quote_windows_path(pasls_executable_path)

    @staticmethod
    def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
        root_uri = pathlib.Path(repository_absolute_path).as_uri()

        initialization_options: dict = {}

        env_vars = ["PP", "FPCDIR", "LAZARUSDIR", "FPCTARGET", "FPCTARGETCPU"]
        for var in env_vars:
            value = os.environ.get(var, "")
            if value:
                initialization_options[var] = value

        initialize_params = {
            "locale": "en",
            "capabilities": {
                "textDocument": {
                    "synchronization": {
                        "didSave": True,
                        "dynamicRegistration": True,
                        "willSave": True,
                        "willSaveWaitUntil": True,
                    },
                    "completion": {
                        "dynamicRegistration": True,
                        "completionItem": {
                            "snippetSupport": True,
                            "commitCharactersSupport": True,
                            "documentationFormat": ["markdown", "plaintext"],
                        },
                    },
                    "hover": {
                        "dynamicRegistration": True,
                        "contentFormat": ["markdown", "plaintext"],
                    },
                    "signatureHelp": {
                        "dynamicRegistration": True,
                        "signatureInformation": {
                            "documentationFormat": ["markdown", "plaintext"],
                        },
                    },
                    "definition": {"dynamicRegistration": True, "linkSupport": True},
                    "references": {"dynamicRegistration": True},
                    "documentHighlight": {"dynamicRegistration": True},
                    "documentSymbol": {
                        "dynamicRegistration": True,
                        "hierarchicalDocumentSymbolSupport": True,
                        "symbolKind": {"valueSet": list(range(1, 27))},
                    },
                    "codeAction": {
                        "dynamicRegistration": True,
                        "codeActionLiteralSupport": {
                            "codeActionKind": {
                                "valueSet": [
                                    "quickfix",
                                    "refactor",
                                    "refactor.extract",
                                    "refactor.inline",
                                    "refactor.rewrite",
                                    "source",
                                    "source.organizeImports",
                                ]
                            }
                        },
                    },
                    "formatting": {"dynamicRegistration": True},
                    "rangeFormatting": {"dynamicRegistration": True},
                },
                "workspace": {
                    "workspaceFolders": True,
                    "didChangeConfiguration": {"dynamicRegistration": True},
                    "symbol": {"dynamicRegistration": True},
                    "executeCommand": {"dynamicRegistration": True},
                    "configuration": True,
                    "workspaceEdit": {
                        "documentChanges": True,
                    },
                },
            },
            "initializationOptions": initialization_options,
            "processId": os.getpid(),
            "rootPath": repository_absolute_path,
            "rootUri": root_uri,
            "workspaceFolders": [
                {
                    "uri": root_uri,
                    "name": os.path.basename(repository_absolute_path),
                }
            ],
        }

        return initialize_params  # type: ignore

    def _start_server(self) -> None:

        def register_capability_handler(params: dict) -> None:
            log.debug(f"Capability registered: {params}")
            return

        def window_log_message(msg: dict) -> None:
            log.info(f"LSP: window/logMessage: {msg}")
            message_text = msg.get("message", "")
            if "initialized" in message_text.lower() or "ready" in message_text.lower():
                log.info("Pascal language server ready signal detected")
                self.server_ready.set()

        def publish_diagnostics(params: dict) -> None:
            log.debug(f"Diagnostics: {params}")
            return

        def do_nothing(params: dict) -> None:
            return

        self.server.on_request("client/registerCapability", register_capability_handler)
        self.server.on_notification("window/logMessage", window_log_message)
        self.server.on_notification("window/showMessage", window_log_message)
        self.server.on_notification("textDocument/publishDiagnostics", publish_diagnostics)
        self.server.on_notification("$/progress", do_nothing)

        log.info("Starting Pascal server process")
        self.server.start()
        initialize_params = self._get_initialize_params(self.repository_root_path)

        log.info("Sending initialize request from LSP client to LSP server and awaiting response")
        init_response = self.server.send.initialize(initialize_params)
        log.debug(f"Received initialize response from Pascal server: {init_response}")

        capabilities = init_response.get("capabilities", {})
        assert "textDocumentSync" in capabilities

        if "completionProvider" in capabilities:
            log.info("Pascal server supports code completion")
        if "definitionProvider" in capabilities:
            log.info("Pascal server supports go to definition")
        if "referencesProvider" in capabilities:
            log.info("Pascal server supports find references")
        if "documentSymbolProvider" in capabilities:
            log.info("Pascal server supports document symbols")

        self.server.notify.initialized({})

        log.info("Waiting for Pascal language server to be ready...")
        if not self.server_ready.wait(timeout=5.0):
            log.info("Timeout waiting for Pascal server ready signal, assuming server is ready")
            self.server_ready.set()
        else:
            log.info("Pascal server initialization complete")

    def is_ignored_dirname(self, dirname: str) -> bool:
        ignored_dirs = {
            "lib",
            "backup",
            "__history",
            "__recovery",
            "bin",
            ".git",
            ".svn",
            ".hg",
            "node_modules",
        }
        return dirname.lower() in ignored_dirs
