from collections.abc import (
    Callable,
    Hashable,
)

import numpy as np
from pandas.core.base import SelectionMixin
from pandas.core.frame import DataFrame
from pandas.core.generic import NDFrame
from pandas.core.groupby import ops
from pandas.core.indexes.api import Index
from pandas.core.series import Series

from pandas._typing import (
    Axis,
    KeysArgType,
    NDFrameT,
    npt,
)

class GroupByPlot:
    def __init__(self, groupby) -> None: ...
    def __call__(self, *args, **kwargs): ...
    def __getattr__(self, name: str): ...

class BaseGroupBy(SelectionMixin[NDFrameT]):
    level = ...
    as_index = ...
    keys = ...
    sort = ...
    group_keys = ...
    squeeze = ...
    observed = ...
    mutated = ...
    @property
    def obj(self) -> NDFrameT: ...
    axis = ...
    grouper = ...
    exclusions = ...
    def __len__(self) -> int: ...
    @property
    def groups(self) -> dict[Hashable, list[Hashable]]: ...
    @property
    def ngroups(self) -> int: ...
    @property
    def indices(self) -> dict[Hashable, Index | npt.NDArray[np.int_] | list[int]]: ...
    def pipe(self, func: Callable, *args, **kwargs): ...
    plot = ...
    def get_group(self, name, obj: NDFrameT | None = ...) -> NDFrameT: ...

class GroupBy(BaseGroupBy[NDFrameT]):
    def count(self) -> DataFrame | Series: ...
    def mean(self, **kwargs) -> DataFrame | Series: ...
    def median(self, **kwargs) -> DataFrame | Series: ...
    def std(self, ddof: int = ..., numeric_only: bool = ...) -> DataFrame | Series: ...
    def var(self, ddof: int = ..., numeric_only: bool = ...) -> DataFrame | Series: ...
    def sem(self, ddof: int = ..., numeric_only: bool = ...) -> DataFrame | Series: ...
    def ohlc(self) -> DataFrame: ...
    def describe(self, **kwargs) -> DataFrame | Series: ...
    def resample(self, rule, *args, **kwargs): ...
    def rolling(self, *args, **kwargs): ...
    def expanding(self, *args, **kwargs): ...
    def ffill(self, limit: int | None = ...) -> DataFrame | Series: ...
    def bfill(self, limit: int | None = ...) -> DataFrame | Series: ...
    def nth(
        self, n: int | list[int], dropna: str | None = ...
    ) -> DataFrame | Series: ...
    def quantile(self, q=..., interpolation: str = ...): ...
    def ngroup(self, ascending: bool = ...) -> Series: ...
    def cumcount(self, ascending: bool = ...) -> Series: ...
    def rank(
        self,
        method: str = ...,
        ascending: bool = ...,
        na_option: str = ...,
        pct: bool = ...,
        axis: int = ...,
    ) -> DataFrame: ...
    def cummax(self, axis: Axis = ..., **kwargs) -> DataFrame | Series: ...
    def cummin(self, axis: Axis = ..., **kwargs) -> DataFrame | Series: ...
    def cumprod(self, axis: Axis = ..., **kwargs) -> DataFrame | Series: ...
    def cumsum(self, axis: Axis = ..., **kwargs) -> DataFrame | Series: ...
    def shift(self, periods: int = ..., freq=..., axis: Axis = ..., fill_value=...): ...
    def pct_change(
        self,
        periods: int = ...,
        fill_method: str = ...,
        limit=...,
        freq=...,
        axis: Axis = ...,
    ) -> DataFrame | Series: ...
    def head(self, n: int = ...) -> DataFrame | Series: ...
    def tail(self, n: int = ...) -> DataFrame | Series: ...
    # Surplus methods from original pylance stubs; should they go away?
    def first(self, **kwargs) -> DataFrame | Series: ...
    def last(self, **kwargs) -> DataFrame | Series: ...
    def max(self, **kwargs) -> DataFrame | Series: ...
    def min(self, **kwargs) -> DataFrame | Series: ...
    def size(self) -> Series[int]: ...

def get_groupby(
    obj: NDFrame,
    by: KeysArgType | None = ...,
    axis: int = ...,
    level=...,
    grouper: ops.BaseGrouper | None = ...,
    exclusions=...,
    selection=...,
    as_index: bool = ...,
    sort: bool = ...,
    group_keys: bool = ...,
    squeeze: bool = ...,
    observed: bool = ...,
    mutated: bool = ...,
) -> GroupBy: ...
