from collections.abc import Callable
from enum import (
    Enum,
)
from pathlib import Path
from typing import TYPE_CHECKING, NamedTuple

from networkx import DiGraph

from blends.syntax.node_attributes import VALID_SYNTAX_ATTRIBUTES


class Language(Enum):
    CSHARP = "c_sharp"
    DART = "dart"
    GO = "go"
    HCL = "hcl"
    JAVA = "java"
    JAVASCRIPT = "javascript"
    JSON = "json"
    KOTLIN = "kotlin"
    NOT_SUPPORTED = "not_supported"
    PHP = "php"
    PYTHON = "python"
    RUBY = "ruby"
    SCALA = "scala"
    SWIFT = "swift"
    TYPESCRIPT = "tsx"
    YAML = "yaml"


LANGUAGE_TO_EXTENSIONS: dict[Language, tuple[str, ...]] = {
    Language.CSHARP: (".cs",),
    Language.DART: (".dart",),
    Language.GO: (".go",),
    Language.HCL: (".hcl", ".tf"),
    Language.JAVA: (".java",),
    Language.JAVASCRIPT: (".js", ".jsx"),
    Language.JSON: (".json",),
    Language.KOTLIN: (".kt", ".ktm", ".kts"),
    Language.PHP: (".php",),
    Language.PYTHON: (".py",),
    Language.RUBY: (".rb",),
    Language.SCALA: (".scala",),
    Language.SWIFT: (".swift",),
    Language.TYPESCRIPT: (".ts", ".tsx"),
    Language.YAML: (".yaml", ".yml"),
}

EXTENSION_TO_LANGUAGE: dict[str, Language] = {
    ext: lang for lang, exts in LANGUAGE_TO_EXTENSIONS.items() for ext in exts
}


def decide_language(path: str) -> Language:
    file_extension = "." + path.split(".")[-1]
    return EXTENSION_TO_LANGUAGE.get(file_extension, Language.NOT_SUPPORTED)


SUPPORTED_MULTIFILE = {
    Language.JAVA,
}


class Content(NamedTuple):
    content_bytes: bytes
    content_str: str
    language: Language
    path: Path


class CustomParser(NamedTuple):
    validator: Callable[[Content], bool]
    parser: Callable[[Content], Content | None]


NId = str

if TYPE_CHECKING:

    class BaseGraph(DiGraph[NId]): ...
else:

    class BaseGraph(DiGraph): ...


class Graph(BaseGraph):
    def __init__(self) -> None:  # noqa: D107
        super().__init__()
        self.nodes_by_type: dict[str, list[NId]] = {}

    def add_node(self, node_for_adding: NId, **attr: object) -> None:
        super().add_node(node_for_adding, **attr)
        if label_type := attr.get("label_type"):
            self.nodes_by_type.setdefault(str(label_type), []).append(node_for_adding)

    def update_node(self, node_id: NId, attributes: dict[str, object]) -> None:
        self.nodes[node_id].update(attributes)


class SyntaxGraph(Graph):
    def add_node(self, node_for_adding: NId, **attr: object) -> None:
        invalid_keys = attr.keys() - VALID_SYNTAX_ATTRIBUTES
        if invalid_keys:
            msg = f"Invalid node attribute keys: {invalid_keys!r}"
            raise ValueError(msg)
        super().add_node(node_for_adding, **attr)

    def update_node(self, node_id: NId, attributes: dict[str, object]) -> None:
        invalid_keys = attributes.keys() - VALID_SYNTAX_ATTRIBUTES
        if invalid_keys:
            msg = f"Invalid node attribute keys: {invalid_keys!r}"
            raise ValueError(msg)
        super().update_node(node_id, attributes)

    def update_node_attribute(self, node_id: NId, key: str, value: object) -> None:
        if key not in VALID_SYNTAX_ATTRIBUTES:
            msg = f"Invalid node attribute key: {key!r}"
            raise ValueError(msg)
        self.nodes[node_id][key] = value


class GraphPair(NamedTuple):
    ast_graph: Graph | None
    syntax_graph: SyntaxGraph | None


class GraphShard(NamedTuple):
    path: str
    graph: Graph
    syntax_graph: SyntaxGraph
    content_as_str: str
    file_size: int


class GraphDB(NamedTuple):
    context: dict[Language, dict[str, dict]]  # type: ignore[type-arg]
    shards: dict[str, GraphShard]
    properties: dict[str, dict[str, str]] = {}  # noqa: RUF012

    def get_path_shard(self, path: str) -> GraphShard | None:
        return self.shards.get(path)
