"""LSP language/server data layer.

Maps file extensions to LSP language IDs and defines server configurations.
Ported from opencode's language.ts / server.ts registry.
No async, no subprocess — pure data + path scanning.
"""

from dataclasses import dataclass, field
from pathlib import Path

# ~90 file extensions → LSP language IDs (opencode language.ts)
EXTENSION_MAP: dict[str, str] = {
    # Python
    ".py": "python",
    ".pyi": "python",
    ".pyw": "python",
    # JavaScript
    ".js": "javascript",
    ".jsx": "javascriptreact",
    ".mjs": "javascript",
    ".cjs": "javascript",
    # TypeScript
    ".ts": "typescript",
    ".tsx": "typescriptreact",
    ".mts": "typescript",
    ".cts": "typescript",
    # Go
    ".go": "go",
    ".mod": "gomod",
    ".sum": "gosum",
    ".work": "gowork",
    # Rust
    ".rs": "rust",
    # Java
    ".java": "java",
    # Kotlin
    ".kt": "kotlin",
    ".kts": "kotlin",
    # C / C++
    ".c": "c",
    ".h": "c",
    ".cpp": "cpp",
    ".cxx": "cpp",
    ".cc": "cpp",
    ".hpp": "cpp",
    ".hxx": "cpp",
    ".hh": "cpp",
    # C#
    ".cs": "csharp",
    # Ruby
    ".rb": "ruby",
    ".rake": "ruby",
    ".gemspec": "ruby",
    # PHP
    ".php": "php",
    # Swift
    ".swift": "swift",
    # Objective-C
    ".m": "objective-c",
    ".mm": "objective-cpp",
    # Lua
    ".lua": "lua",
    # Zig
    ".zig": "zig",
    # Elixir
    ".ex": "elixir",
    ".exs": "elixir",
    # Erlang
    ".erl": "erlang",
    ".hrl": "erlang",
    # Haskell
    ".hs": "haskell",
    ".lhs": "haskell",
    # OCaml
    ".ml": "ocaml",
    ".mli": "ocaml",
    # Scala
    ".scala": "scala",
    ".sc": "scala",
    # Dart
    ".dart": "dart",
    # R
    ".r": "r",
    ".R": "r",
    # Julia
    ".jl": "julia",
    # Nim
    ".nim": "nim",
    # Crystal
    ".cr": "crystal",
    # V
    ".v": "v",
    # Shell
    ".sh": "shellscript",
    ".bash": "shellscript",
    ".zsh": "shellscript",
    ".fish": "shellscript",
    # Perl
    ".pl": "perl",
    ".pm": "perl",
    # HTML
    ".html": "html",
    ".htm": "html",
    # CSS
    ".css": "css",
    ".scss": "scss",
    ".sass": "sass",
    ".less": "less",
    # Vue / Svelte / Astro
    ".vue": "vue",
    ".svelte": "svelte",
    ".astro": "astro",
    # Config / Data
    ".json": "json",
    ".jsonc": "jsonc",
    ".yaml": "yaml",
    ".yml": "yaml",
    ".toml": "toml",
    ".xml": "xml",
    ".graphql": "graphql",
    ".gql": "graphql",
    ".proto": "protobuf",
    # Markup
    ".md": "markdown",
    ".mdx": "mdx",
    ".tex": "latex",
    ".typ": "typst",
    # SQL
    ".sql": "sql",
    # Docker
    ".dockerfile": "dockerfile",
    # Terraform
    ".tf": "terraform",
    ".tfvars": "terraform",
    # Nix
    ".nix": "nix",
    # WASM
    ".wat": "wat",
    ".wast": "wat",
    # Clojure
    ".clj": "clojure",
    ".cljs": "clojurescript",
    ".cljc": "clojure",
    ".edn": "clojure",
    # F#
    ".fs": "fsharp",
    ".fsi": "fsharp",
    ".fsx": "fsharp",
}


@dataclass(frozen=True)
class ServerDef:
    """Definition of an LSP server."""

    id: str
    extensions: tuple[str, ...]
    command: tuple[str, ...]
    root_markers: tuple[str, ...] = ()
    exclude_markers: tuple[str, ...] = ()
    install_cmd: tuple[str, ...] | None = None
    settings: dict = field(default_factory=dict)


# 30+ server definitions ported from opencode's server.ts
SERVERS: list[ServerDef] = [
    ServerDef(
        id="pyright",
        extensions=(".py", ".pyi", ".pyw"),
        command=("pyright-langserver", "--stdio"),
        root_markers=("pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"),
        install_cmd=("pip", "install", "pyright"),
    ),
    ServerDef(
        id="typescript-language-server",
        extensions=(".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"),
        command=("typescript-language-server", "--stdio"),
        root_markers=("package.json", "tsconfig.json", "jsconfig.json"),
        install_cmd=("npm", "install", "-g", "typescript-language-server", "typescript"),
    ),
    ServerDef(
        id="gopls",
        extensions=(".go", ".mod", ".sum", ".work"),
        command=("gopls", "serve"),
        root_markers=("go.mod", "go.sum"),
        install_cmd=("go", "install", "golang.org/x/tools/gopls@latest"),
    ),
    ServerDef(
        id="rust-analyzer",
        extensions=(".rs",),
        command=("rust-analyzer",),
        root_markers=("Cargo.toml",),
    ),
    ServerDef(
        id="clangd",
        extensions=(".c", ".h", ".cpp", ".cxx", ".cc", ".hpp", ".hxx", ".hh", ".m", ".mm"),
        command=("clangd",),
        root_markers=("compile_commands.json", "CMakeLists.txt", "Makefile", ".clangd"),
    ),
    ServerDef(
        id="java-language-server",
        extensions=(".java",),
        command=("jdtls",),
        root_markers=("pom.xml", "build.gradle", "build.gradle.kts", ".project"),
    ),
    ServerDef(
        id="kotlin-language-server",
        extensions=(".kt", ".kts"),
        command=("kotlin-language-server",),
        root_markers=("build.gradle", "build.gradle.kts", "pom.xml"),
    ),
    ServerDef(
        id="ruby-lsp",
        extensions=(".rb", ".rake", ".gemspec"),
        command=("ruby-lsp",),
        root_markers=("Gemfile", ".ruby-version"),
        install_cmd=("gem", "install", "ruby-lsp"),
    ),
    ServerDef(
        id="phpactor",
        extensions=(".php",),
        command=("phpactor", "language-server"),
        root_markers=("composer.json",),
    ),
    ServerDef(
        id="sourcekit-lsp",
        extensions=(".swift",),
        command=("sourcekit-lsp",),
        root_markers=("Package.swift",),
    ),
    ServerDef(
        id="lua-language-server",
        extensions=(".lua",),
        command=("lua-language-server",),
        root_markers=(".luarc.json", ".luarc.jsonc", ".stylua.toml"),
    ),
    ServerDef(
        id="zls",
        extensions=(".zig",),
        command=("zls",),
        root_markers=("build.zig", "build.zig.zon"),
        install_cmd=None,
    ),
    ServerDef(
        id="elixir-ls",
        extensions=(".ex", ".exs"),
        command=("elixir-ls",),
        root_markers=("mix.exs",),
    ),
    ServerDef(
        id="erlang-ls",
        extensions=(".erl", ".hrl"),
        command=("erlang_ls",),
        root_markers=("rebar.config", "erlang.mk"),
    ),
    ServerDef(
        id="haskell-language-server",
        extensions=(".hs", ".lhs"),
        command=("haskell-language-server-wrapper", "--lsp"),
        root_markers=("*.cabal", "stack.yaml", "cabal.project"),
    ),
    ServerDef(
        id="ocamllsp",
        extensions=(".ml", ".mli"),
        command=("ocamllsp",),
        root_markers=("dune-project", "*.opam"),
    ),
    ServerDef(
        id="metals",
        extensions=(".scala", ".sc"),
        command=("metals",),
        root_markers=("build.sbt", "build.sc", ".bloop"),
    ),
    ServerDef(
        id="dart-language-server",
        extensions=(".dart",),
        command=("dart", "language-server", "--protocol=lsp"),
        root_markers=("pubspec.yaml",),
    ),
    ServerDef(
        id="r-languageserver",
        extensions=(".r", ".R"),
        command=("R", "--slave", "-e", "languageserver::run()"),
        root_markers=("DESCRIPTION", ".Rproj"),
    ),
    ServerDef(
        id="julia-language-server",
        extensions=(".jl",),
        command=("julia", "--startup-file=no", "-e", "using LanguageServer; runserver()"),
        root_markers=("Project.toml",),
    ),
    ServerDef(
        id="nimlsp",
        extensions=(".nim",),
        command=("nimlsp",),
        root_markers=("*.nimble",),
    ),
    ServerDef(
        id="crystalline",
        extensions=(".cr",),
        command=("crystalline",),
        root_markers=("shard.yml",),
    ),
    ServerDef(
        id="bash-language-server",
        extensions=(".sh", ".bash", ".zsh"),
        command=("bash-language-server", "start"),
        root_markers=(),
        install_cmd=("npm", "install", "-g", "bash-language-server"),
    ),
    ServerDef(
        id="yaml-language-server",
        extensions=(".yaml", ".yml"),
        command=("yaml-language-server", "--stdio"),
        root_markers=(),
        install_cmd=("npm", "install", "-g", "yaml-language-server"),
    ),
    ServerDef(
        id="vscode-json-languageserver",
        extensions=(".json", ".jsonc"),
        command=("vscode-json-language-server", "--stdio"),
        root_markers=(),
        install_cmd=("npm", "install", "-g", "vscode-langservers-extracted"),
    ),
    ServerDef(
        id="html-language-server",
        extensions=(".html", ".htm"),
        command=("vscode-html-language-server", "--stdio"),
        root_markers=(),
        install_cmd=("npm", "install", "-g", "vscode-langservers-extracted"),
    ),
    ServerDef(
        id="css-language-server",
        extensions=(".css", ".scss", ".less"),
        command=("vscode-css-language-server", "--stdio"),
        root_markers=(),
        install_cmd=("npm", "install", "-g", "vscode-langservers-extracted"),
    ),
    ServerDef(
        id="vue-language-server",
        extensions=(".vue",),
        command=("vue-language-server", "--stdio"),
        root_markers=("package.json",),
        exclude_markers=(),
        install_cmd=("npm", "install", "-g", "@vue/language-server"),
    ),
    ServerDef(
        id="svelte-language-server",
        extensions=(".svelte",),
        command=("svelteserver", "--stdio"),
        root_markers=("package.json",),
        install_cmd=("npm", "install", "-g", "svelte-language-server"),
    ),
    ServerDef(
        id="terraform-ls",
        extensions=(".tf", ".tfvars"),
        command=("terraform-ls", "serve"),
        root_markers=(".terraform", "*.tf"),
    ),
    ServerDef(
        id="nil",
        extensions=(".nix",),
        command=("nil",),
        root_markers=("flake.nix", "default.nix", "shell.nix"),
    ),
    ServerDef(
        id="clojure-lsp",
        extensions=(".clj", ".cljs", ".cljc", ".edn"),
        command=("clojure-lsp",),
        root_markers=("deps.edn", "project.clj", "shadow-cljs.edn"),
    ),
    ServerDef(
        id="csharp-ls",
        extensions=(".cs",),
        command=("csharp-ls",),
        root_markers=("*.csproj", "*.sln"),
    ),
    ServerDef(
        id="fsautocomplete",
        extensions=(".fs", ".fsi", ".fsx"),
        command=("fsautocomplete", "--adaptive-lsp-server-enabled"),
        root_markers=("*.fsproj", "*.sln"),
    ),
    ServerDef(
        id="graphql-language-server",
        extensions=(".graphql", ".gql"),
        command=("graphql-lsp", "server", "-m", "stream"),
        root_markers=(".graphqlrc", ".graphqlrc.yml", ".graphqlrc.json", "graphql.config.js"),
        install_cmd=("npm", "install", "-g", "graphql-language-service-cli"),
    ),
]

# Index servers by extension for fast lookup.
_SERVERS_BY_EXT: dict[str, list[ServerDef]] = {}
for _srv in SERVERS:
    for _ext in _srv.extensions:
        _SERVERS_BY_EXT.setdefault(_ext, []).append(_srv)


def language_id(path: str | Path) -> str | None:
    """Return the LSP language ID for a file, or None if unknown."""
    ext = Path(path).suffix.lower()
    return EXTENSION_MAP.get(ext)


def find_servers(path: str | Path) -> list[tuple[ServerDef, Path]]:
    """Find matching LSP servers for a file path.

    Returns (server_def, project_root) pairs. Walks up from the file
    looking for root markers to determine the project root.
    """
    path = Path(path)
    ext = path.suffix.lower()
    candidates = _SERVERS_BY_EXT.get(ext, [])
    if not candidates:
        return []

    results: list[tuple[ServerDef, Path]] = []
    for server in candidates:
        root = _find_project_root(path, server.root_markers, server.exclude_markers)
        results.append((server, root))
    return results


def _find_project_root(
    path: Path,
    root_markers: tuple[str, ...],
    exclude_markers: tuple[str, ...] = (),
) -> Path:
    """Walk up from *path* looking for root markers.

    Returns the first directory containing a root marker, or the file's
    parent directory if no marker is found.
    """
    import fnmatch as _fnmatch

    start = path.parent if path.is_file() else path
    current = start.resolve()

    for _ in range(20):  # Max depth to avoid scanning entire FS
        # Check exclude markers first
        for marker in exclude_markers:
            for child in current.iterdir() if current.exists() else []:
                if _fnmatch.fnmatch(child.name, marker):
                    return start.resolve()

        # Check root markers
        for marker in root_markers:
            if "*" in marker:
                # Glob pattern — check if any file matches
                try:
                    if any(current.glob(marker)):
                        return current
                except OSError:
                    pass
            else:
                if (current / marker).exists():
                    return current

        parent = current.parent
        if parent == current:
            break
        current = parent

    return start.resolve()
