from collections.abc import Callable, Collection, Hashable, Iterable, Iterator, Mapping, MutableMapping
from decimal import Decimal
from functools import cached_property
from typing import Any, ClassVar, Generic, TypeAlias, TypeVar, overload
from typing_extensions import Self

import numpy
from networkx.classes.coreviews import AdjacencyView, AtlasView
from networkx.classes.digraph import DiGraph
from networkx.classes.reportviews import DegreeView, DiDegreeView, EdgeView, NodeView, OutEdgeView

_DataBound: TypeAlias = Mapping[str, Any]

_Node = TypeVar("_Node", bound=Hashable)
_NodeData = TypeVar("_NodeData", bound=_DataBound, default=dict[str, Any])
_EdgeData = TypeVar("_EdgeData", bound=_DataBound, default=dict[str, Any])

_NodeWithData: TypeAlias = tuple[_Node, _NodeData]
_NodePlus: TypeAlias = _Node | _NodeWithData[_Node, _NodeData]
_Edge: TypeAlias = tuple[_Node, _Node]
_EdgeWithData: TypeAlias = tuple[_Node, _Node, _EdgeData]
_EdgePlus: TypeAlias = _Edge[_Node] | _EdgeWithData[_Node, _EdgeData]
_MapFactory: TypeAlias = Callable[[], MutableMapping[str, Any]]
_NBunch: TypeAlias = _Node | Iterable[_Node] | None
_Data: TypeAlias = (
    Graph[_Node, _NodeData, _EdgeData]
    | dict[_Node, dict[_Node, _NodeData]]
    | dict[_Node, Iterable[_Node]]
    | Iterable[_EdgePlus[_Node, _EdgeData]]
    | numpy.ndarray[Any, Any]
    # | scipy.sparse.base.spmatrix
)

__all__ = ["Graph"]

class Graph(Collection[_Node], Generic[_Node, _NodeData, _EdgeData]):
    __networkx_backend__: ClassVar[str]
    node_dict_factory: ClassVar[_MapFactory]
    node_attr_dict_factory: ClassVar[_MapFactory]
    adjlist_outer_dict_factory: ClassVar[_MapFactory]
    adjlist_inner_dict_factory: ClassVar[_MapFactory]
    edge_attr_dict_factory: ClassVar[_MapFactory]
    graph_attr_dict_factory: ClassVar[_MapFactory]

    graph: dict[str, Any]
    __networkx_cache__: dict[str, Any]

    def to_directed_class(self) -> type[DiGraph[_Node, _NodeData, _EdgeData]]: ...
    def to_undirected_class(self) -> type[Graph[_Node, _NodeData, _EdgeData]]: ...
    # @_dispatchable adds `backend` argument, but this decorated is unsupported constructor type here
    # and __init__() ignores this argument
    def __new__(cls, *args, backend=None, **kwargs) -> Self: ...
    def __init__(
        self, incoming_graph_data: _Data[_Node, _NodeData, _EdgeData] | None = None, **attr: Any
    ) -> None: ...  # attr: key=value pairs
    @cached_property
    def adj(self) -> AdjacencyView[_Node, _Node, _EdgeData]: ...

    # This object is a read-only dict-like structure
    @property
    def name(self) -> str: ...
    @name.setter
    def name(self, s: str) -> None: ...

    def __iter__(self) -> Iterator[_Node]: ...
    def __contains__(self, n: object) -> bool: ...
    def __len__(self) -> int: ...
    def __getitem__(self, n: _Node) -> AtlasView[_Node, str, Any]: ...
    def add_node(self, node_for_adding: _Node, **attr: Any) -> None: ...  # attr: Set or change node attributes using key=value
    def add_nodes_from(
        self, nodes_for_adding: Iterable[_NodePlus[_Node, _NodeData]], **attr: Any
    ) -> None: ...  # attr: key=value pairs
    def remove_node(self, n: _Node) -> None: ...
    def remove_nodes_from(self, nodes: Iterable[_Node]) -> None: ...
    @cached_property
    def nodes(self) -> NodeView[_Node, _NodeData, _EdgeData]: ...
    def number_of_nodes(self) -> int: ...
    def order(self) -> int: ...
    def has_node(self, n: _Node) -> bool: ...
    # Including subtypes' possible return types for LSP
    def add_edge(self, u_of_edge: _Node, v_of_edge: _Node, **attr: Any) -> Hashable | None: ...
    # attr: Edge data (or labels or objects) can be assigned using keyword arguments
    def add_edges_from(self, ebunch_to_add: Iterable[_EdgePlus[_Node, _EdgeData]], **attr: Any) -> None: ...
    # attr: Edge data (or labels or objects) can be assigned using keyword arguments
    def add_weighted_edges_from(
        self, ebunch_to_add: Iterable[tuple[_Node, _Node, float | Decimal | None]], weight: str = "weight", **attr: Any
    ) -> None: ...
    # attr: Edge attributes to add/update for all edges.
    def remove_edge(self, u: _Node, v: _Node) -> None: ...
    def remove_edges_from(self, ebunch: Iterable[_EdgePlus[_Node, _EdgeData]]) -> None: ...

    @overload
    def update(self, edges: Graph[_Node, _NodeData, _EdgeData], nodes: None = None) -> None: ...
    @overload
    def update(
        self,
        edges: Graph[_Node, _NodeData, _EdgeData] | Iterable[_EdgePlus[_Node, _EdgeData]] | None = None,
        nodes: Iterable[_Node] | None = None,
    ) -> None: ...

    def has_edge(self, u: _Node, v: _Node) -> bool: ...
    def neighbors(self, n: _Node) -> Iterator[_Node]: ...
    @cached_property
    # Including subtypes' possible return types for LSP
    def edges(self) -> EdgeView[_Node, _NodeData, _EdgeData] | OutEdgeView[_Node, _NodeData, _EdgeData]: ...
    def get_edge_data(self, u: _Node, v: _Node, default: Any = None) -> _EdgeData: ...
    # default:  any Python object
    def adjacency(self) -> Iterator[tuple[_Node, dict[_Node, _EdgeData]]]: ...
    @cached_property
    # Including subtypes' possible return types for LSP
    def degree(self) -> DegreeView[_Node, _NodeData, _EdgeData] | DiDegreeView[_Node, _NodeData, _EdgeData]: ...
    def clear(self) -> None: ...
    def clear_edges(self) -> None: ...
    def is_multigraph(self) -> bool: ...
    def is_directed(self) -> bool: ...
    def copy(self, as_view: bool = False) -> Self: ...
    def to_directed(self, as_view: bool = False) -> DiGraph[_Node, _NodeData, _EdgeData]: ...
    def to_undirected(self, as_view: bool = False) -> Graph[_Node, _NodeData, _EdgeData]: ...
    def subgraph(self, nodes: _NBunch[_Node]) -> Self: ...
    def edge_subgraph(self, edges: Iterable[_Edge[_Node]]) -> Self: ...

    @overload
    def size(self, weight: None = None) -> int: ...
    @overload
    def size(self, weight: str) -> float: ...

    def number_of_edges(self, u: _Node | None = None, v: _Node | None = None) -> int: ...
    def nbunch_iter(self, nbunch: _NBunch[_Node] = None) -> Iterator[_Node]: ...
