from __future__ import annotations

from enum import IntEnum
from pathlib import Path
from typing import ClassVar, Literal, Self

import networkx as nx
from pydantic import BaseModel, Field, model_validator

from ..tools.base import (
    DepGraph,
    NodeMixin,
    nx_graph_from_adjacency_list,
)

# from ..tools.filesystem import Dir, File, FileSystem


class SymbolKind(IntEnum):
    """A symbol kind.
    Imported from multilspy.lsp_protocol_handler.lsp_types.SymbolKind.
    """

    File = 1
    Module = 2
    Namespace = 3
    Package = 4
    Class = 5
    Method = 6
    Property = 7
    Field = 8
    Constructor = 9
    Enum = 10
    Interface = 11
    Function = 12
    Variable = 13
    Constant = 14
    String = 15
    Number = 16
    Boolean = 17
    Array = 18
    Object = 19
    Key = 20
    Null = 21
    EnumMember = 22
    Struct = 23
    Event = 24
    Operator = 25
    TypeParameter = 26


Range = tuple[tuple[int, int], tuple[int, int]]


class Symbol(BaseModel, NodeMixin):
    """A mix of LSP's SymbolInformation and DocumentSymbol."""

    name: str
    relative_path: Path = Field()
    kind: SymbolKind
    range: Range | None = Field(
        default=None,
        description='The range of the symbol. None represents a file-level symbol.',
    )
    detail: str | None = Field(default=None)
    container_name: str | None = Field(default=None, alias='containerName')
    children: list[Symbol] = Field(default_factory=list)

    @property
    def path_key(self) -> str:
        return str(self.relative_path)

    def __repr__(self) -> str:
        return str(self.relative_path)


class Module(Symbol):
    """A module."""

    name: str | None = Field(
        default=None,
        description='The name of the module. Automatically generated from the relative '
        'path for Python and IML modules.',
    )
    kind: Literal[SymbolKind.Module] = Field(default=SymbolKind.Module)
    content: str
    src_lang: str

    def __hash__(self) -> int:
        return hash((self.relative_path, self.content))

    @model_validator(mode='after')
    def validate_path(self) -> Self:
        if self.src_lang == 'python' and not self.relative_path.suffix == '.py':
            raise ValueError('Python module must have a .py extension')
        elif self.src_lang == 'iml' and not self.relative_path.suffix == '.iml':
            raise ValueError('IML module must have a .iml extension')
        return self


class ModuleDep[T: Module](DepGraph[T]):
    FILE_EXTENSION_MAP: ClassVar[dict[str, str]] = {
        'python': '.py',
        'iml': '.iml',
    }

    def __init__(self, nx_graph: nx.DiGraph):
        # Allow empty module graphs (e.g. repo has no matching source files)
        if nx_graph.number_of_nodes() == 0:
            # Choose a neutral default; callers must handle empty graphs anyway.
            self.src_lang = 'python'
            super().__init__(nx_graph)
            return

        # Check if all nodes have the same source language
        src_langs = {node.src_lang for node in nx_graph.nodes}
        if len(src_langs) != 1:
            raise ValueError(
                f'All nodes must have the same source language, but got {src_langs}'
            )

        # Check if the source language is supported
        src_lang = next(iter(src_langs))
        if src_lang not in self.FILE_EXTENSION_MAP:
            raise ValueError(
                f'Unsupported source language: {src_lang}. Supported languages: '
                f'{list(self.FILE_EXTENSION_MAP.keys())}'
            )

        self.src_lang = src_lang
        super().__init__(nx_graph)

    @classmethod
    def from_adjacency_list(
        cls,
        nodes: list[T],
        adjacency_list: dict[str, set[str]],
    ) -> Self:
        nx_graph = nx_graph_from_adjacency_list(
            nodes=nodes, adjacency_list=adjacency_list
        )
        return cls(nx_graph)
