"""
Bitrix API client implementations.

This module provides the main SDK client classes used to interact with
the Bitrix REST API. It includes:

- BaseClient — common functionality shared by all client versions
- ClientV1 / ClientV2 / ClientV3 — version-specific API clients
- Client — factory function that returns the appropriate client version

Each client exposes Bitrix API scopes as attributes (e.g. `crm`, `user`,
`task`) which provide access to the corresponding API methods.
"""

import inspect
from abc import ABC
from typing import TYPE_CHECKING, ClassVar, List, Literal, Mapping, Optional, Sequence, Text, Union, overload

from . import scopes
from .api.requests import BitrixAPIBatchesRequest, BitrixAPIBatchRequest
from .constants.version import B24APIVersion
from .protocols import BitrixTokenFullProtocol
from .scopes import v3 as scopes_v3

# noinspection PyProtectedMember
from .scopes._base_context import BaseContext
from .utils.types import B24APIVersionLiteral, JSONDict, Key, Number, Timeout

if TYPE_CHECKING:
    from .api.requests import BitrixAPIRequest

__all__ = [
    "BaseClient",
    "Client",
    "ClientV1",
    "ClientV2",
    "ClientV3",
]


class BaseClient(ABC):
    """
    Base class for Bitrix API clients.

    This class provides common functionality shared by all API client
    versions, including:

    - scope initialization
    - batch request execution
    - API method discovery
    - configuration forwarding to API requests

    Concrete client implementations (ClientV1, ClientV2, ClientV3)
    specify the Bitrix REST API version used for requests.
    """

    VERSION: ClassVar[Union[B24APIVersion, B24APIVersionLiteral]] = NotImplemented

    __slots__ = (
        "_bitrix_token",
        "_kwargs",
        "access",
        "ai",
        "app",
        "biconnector",
        "bizproc",
        "booking",
        "calendar",
        "catalog",
        "crm",
        "department",
        "disk",
        "documentation",
        "documentgenerator",
        "entity",
        "event",
        "events",
        "feature",
        "im",
        "imbot",
        "imconnector",
        "imopenlines",
        "landing",
        "lists",
        "mailservice",
        "messageservice",
        "method",
        "placement",
        "profile",
        "pull",
        "rpa",
        "sale",
        "salescenter",
        "scope",
        "server",
        "sign",
        "socialnetwork",
        "sonet_group",
        "task",
        "telephony",
        "timeman",
        "user",
        "userconsent",
        "userfieldconfig",
        "vote",
        "voximplant",
    )

    _bitrix_token: BitrixTokenFullProtocol
    _kwargs: JSONDict

    access: scopes.Access
    ai: scopes.Ai
    app: scopes.App
    booking: scopes.Booking
    biconnector: scopes.Biconnector
    bizproc: scopes.Bizproc
    calendar: scopes.Calendar
    catalog: scopes.Catalog
    crm: scopes.CRM
    department: scopes.Department
    disk: scopes.Disk
    documentgenerator: scopes.Documentgenerator
    entity: scopes.Entity
    event: scopes.Event
    events: scopes.Events
    feature: scopes.Feature
    im: scopes.Im
    imbot: scopes.Imbot
    imconnector: scopes.Imconnector
    imopenlines: scopes.Imopenlines
    landing: scopes.Landing
    lists: scopes.Lists
    mailservice: scopes.Mailservice
    messageservice: scopes.Messageservice
    method: scopes.Method
    placement: scopes.Placement
    profile: scopes.Profile
    pull: scopes.Pull
    rpa: scopes.Rpa
    salescenter: scopes.Salescenter
    sale: scopes.Sale
    scope: scopes.Scope
    server: scopes.Server
    sign: scopes.Sign
    socialnetwork: scopes.Socialnetwork
    sonet_group: scopes.SonetGroup
    task: scopes.Task
    telephony: scopes.Telephony
    timeman: scopes.Timeman
    user: scopes.User
    userfieldconfig: scopes.Userfieldconfig
    userconsent: scopes.Userconsent
    vote: scopes.Vote
    voximplant: scopes.Voximplant

    def __init__(  # noqa: PLR0915
            self,
            bitrix_token: BitrixTokenFullProtocol,
            *,
            timeout: Timeout = None,
            max_retries: Optional[int] = None,
            initial_retry_delay: Optional[Number] = None,
            retry_delay_increment: Optional[Number] = None,
            **kwargs,
    ):
        """
        Initialize the Bitrix API client.

        Parameters
        ----------
        bitrix_token : BitrixTokenFullProtocol
            Authentication token used to access the Bitrix REST API.

        timeout : Timeout, optional
            Default request timeout.

        max_retries : int, optional
            Maximum number of retry attempts for failed requests.

        initial_retry_delay : Number, optional
            Delay before the first retry attempt.

        retry_delay_increment : Number, optional
            Increment added to retry delay after each retry.

        **kwargs
            Additional options passed to API request objects.
        """

        self._bitrix_token = bitrix_token

        self.access = scopes.Access(self)
        self.ai = scopes.Ai(self)
        self.app = scopes.App(self)
        self.booking = scopes.Booking(self)
        self.biconnector = scopes.Biconnector(self)
        self.bizproc = scopes.Bizproc(self)
        self.calendar = scopes.Calendar(self)
        self.catalog = scopes.Catalog(self)
        self.documentation = scopes_v3.Documentation(self)
        self.crm = scopes.CRM(self)
        self.department = scopes.Department(self)
        self.disk = scopes.Disk(self)
        self.documentgenerator = scopes.Documentgenerator(self)
        self.entity = scopes.Entity(self)
        self.event = scopes.Event(self)
        self.events = scopes.Events(self)
        self.feature = scopes.Feature(self)
        self.im = scopes.Im(self)
        self.imbot = scopes.Imbot(self)
        self.imconnector = scopes.Imconnector(self)
        self.imopenlines = scopes.Imopenlines(self)
        self.landing = scopes.Landing(self)
        self.lists = scopes.Lists(self)
        self.mailservice = scopes.Mailservice(self)
        self.messageservice = scopes.Messageservice(self)
        self.method = scopes.Method(self)
        self.placement = scopes.Placement(self)
        self.profile = scopes.Profile(self)
        self.pull = scopes.Pull(self)
        self.rpa = scopes.Rpa(self)
        self.salescenter = scopes.Salescenter(self)
        self.sale = scopes.Sale(self)
        self.scope = scopes.Scope(self)
        self.server = scopes.Server(self)
        self.sign = scopes.Sign(self)
        self.socialnetwork = scopes.Socialnetwork(self)
        self.sonet_group = scopes.SonetGroup(self)
        self.task = scopes.Task(self)
        self.telephony = scopes.Telephony(self)
        self.timeman = scopes.Timeman(self)
        self.user = scopes.User(self)
        self.userfieldconfig = scopes.Userfieldconfig(self)
        self.userconsent = scopes.Userconsent(self)
        self.vote = scopes.Vote(self)
        self.voximplant = scopes.Voximplant(self)

        self._kwargs = kwargs | {"prefer_version": self.VERSION}

        if timeout is not None:
            self._kwargs["timeout"] = timeout

        if max_retries is not None:
            self._kwargs["max_retries"] = max_retries

        if initial_retry_delay is not None:
            self._kwargs["initial_retry_delay"] = initial_retry_delay

        if retry_delay_increment is not None:
            self._kwargs["retry_delay_increment"] = retry_delay_increment

    def __str__(self):
        if hasattr(self._bitrix_token, "domain"):
            return f"<{self.__class__.__name__} of portal {self._bitrix_token.domain}>"
        else:
            return repr(self)

    def __repr__(self):
        return f"{self.__class__.__name__}(bitrix_token={self._bitrix_token})"

    @overload
    def call_batch(
            self,
            bitrix_api_requests: Mapping[Key, "BitrixAPIRequest"],
            *,
            halt: bool = False,
            ignore_size_limit: bool = False,
            timeout: Timeout = None,
            **kwargs,
    ) -> BitrixAPIBatchRequest[Mapping[Key, "BitrixAPIRequest"]]: ...

    @overload
    def call_batch(
            self,
            bitrix_api_requests: Sequence["BitrixAPIRequest"],
            *,
            halt: bool = False,
            ignore_size_limit: bool = False,
            timeout: Timeout = None,
            **kwargs,
    ) -> BitrixAPIBatchRequest[Sequence["BitrixAPIRequest"]]: ...

    def call_batch(
            self,
            bitrix_api_requests: Union[Mapping[Key, "BitrixAPIRequest"], Sequence["BitrixAPIRequest"]],
            *,
            halt: bool = False,
            ignore_size_limit: bool = False,
            timeout: Timeout = None,
            **kwargs,
    ) -> BitrixAPIBatchRequest:
        """
        Execute multiple API requests in a single batch call.

        Args:
            bitrix_api_requests: Collection of BitrixAPIRequest objects to execute.
            halt: If True, stops processing on first error. Returns results up to that point.
            ignore_size_limit: Ignore batch size limitations if True.
            timeout: Timeout for the batch request in seconds.

        Returns:
            BitrixAPIBatchRequest instance for executing the batch.
        """

        kwargs = self._kwargs | kwargs

        if timeout is not None:
            kwargs["timeout"] = timeout

        return BitrixAPIBatchRequest(
            bitrix_token=self._bitrix_token,
            bitrix_api_requests=bitrix_api_requests,
            halt=halt,
            ignore_size_limit=ignore_size_limit,
            **kwargs,
        )

    @overload
    def call_batches(
            self,
            bitrix_api_requests: Mapping[Key, "BitrixAPIRequest"],
            *,
            halt: bool = False,
            timeout: Timeout = None,
            **kwargs,
    ) -> BitrixAPIBatchRequest[Mapping[Key, "BitrixAPIRequest"]]: ...

    @overload
    def call_batches(
            self,
            bitrix_api_requests: Sequence["BitrixAPIRequest"],
            *,
            halt: bool = False,
            timeout: Timeout = None,
            **kwargs,
    ) -> BitrixAPIBatchRequest[Sequence["BitrixAPIRequest"]]: ...

    def call_batches(
            self,
            bitrix_api_requests: Union[Mapping[Key, "BitrixAPIRequest"], Sequence["BitrixAPIRequest"]],
            halt: bool = False,
            timeout: Timeout = None,
            **kwargs,
    ) -> BitrixAPIBatchesRequest:
        """
        Execute multiple API requests in parallel batches.

        Args:
            bitrix_api_requests: Collection of BitrixAPIRequest objects to execute.
            halt: If True, stops processing on first error. Returns results up to that point.
            timeout: Timeout for the batches request in seconds.

        Returns:
            BitrixAPIBatchesRequest instance for executing parallel batches.
        """

        kwargs = self._kwargs | kwargs

        if timeout is not None:
            kwargs["timeout"] = timeout

        return BitrixAPIBatchesRequest(
            bitrix_token=self._bitrix_token,
            bitrix_api_requests=bitrix_api_requests,
            halt=halt,
            **kwargs,
        )

    def __get_context_by_path(self, path: Text) -> BaseContext:
        """
        Resolve a context object using a dot-separated path.

        Examples
        --------
        "crm.lead"
        "user"
        "tasks.task"

        Parameters
        ----------
        path : Text
            Dot-separated path to the context.

        Returns
        -------
        BaseContext
            Resolved context object.

        Raises
        ------
        ValueError
            If the path cannot be resolved.

        TypeError
            If the resolved object is not a BaseContext instance.
        """

        if not path:
            raise ValueError("Path cannot be empty")

        try:
            context = self

            for part in path.split("."):
                context = getattr(context, part)

        except AttributeError as error:
            raise ValueError(f"Path {path!r} not found: {error}") from error

        else:
            if not isinstance(context, BaseContext):
                raise TypeError(
                    f"Path {path!r} points to an object of type {type(context).__name__!r}, "
                    f"which is not a BaseContext descendant",
                )

            return context

    def __collect_api_methods(self, context: Union[BaseContext, "BaseClient"]) -> List[Text]:
        """
        Recursively collect API method names from a context.

        Parameters
        ----------
        context : BaseContext or BaseClient
            Context to inspect.

        Returns
        -------
        List[Text]
            List of available API method names.
        """

        api_methods: List[Text] = []

        for attr_name, value in inspect.getmembers(context):
            if attr_name == "__call__":
                api_methods.append(str(context))

            elif attr_name.startswith("_"):
                continue

            elif isinstance(value, BaseContext):
                api_methods.extend(self.__collect_api_methods(value))

            elif not isinstance(context, self.__class__):
                api_methods.append(f"{context}.{attr_name}")

        return api_methods

    def get_supported_api_methods(self, context: Optional[Union[BaseContext, Text]] = None) -> List[Text]:
        """
        Get a list of all supported API methods.

        Args:
            context: Optional context object or path to filter methods.
                     If None, returns all available methods.

        Returns:
            List of available API method names.
        """

        if context is None:
            context_object = self

        elif isinstance(context, BaseContext):
            context_object = context

        elif isinstance(context, str):
            context_object = self.__get_context_by_path(context)

        else:
            raise TypeError(
                f"Invalid argument type {type(context).__name__!r}. "
                f"The method accepts only 'str' (API path), 'None', or an instance of BaseContext.",
            )

        return self.__collect_api_methods(context_object)

    def print_supported_api_methods(self, context: Optional[Union[BaseContext, Text]] = None):
        """
        Print supported API methods.

        This is a convenience wrapper around :meth:`get_supported_api_methods`
        that prints the discovered API methods to stdout in sorted order,
        followed by the total number of methods.

        Parameters
        ----------
        context : BaseContext or str, optional
            Context object or dot-separated API path used to filter methods.
            If omitted, methods from all available scopes are printed.
        """

        supported_api_methods = self.get_supported_api_methods(context)

        print("\n".join(sorted(supported_api_methods)))
        print(f"\nTotal supported API methods: {len(supported_api_methods)}")


class ClientV1(BaseClient):
    """
    Bitrix REST API v1 client.

    Provides access to API scopes supported by the Bitrix API.
    """

    VERSION = B24APIVersion.V1

    __slots__ = (
        "tasks",
    )

    tasks: scopes.Tasks

    def __init__(
            self,
            bitrix_token: BitrixTokenFullProtocol,
            *,
            timeout: Timeout = None,
            max_retries: Optional[int] = None,
            initial_retry_delay: Optional[Number] = None,
            retry_delay_increment: Optional[Number] = None,
            **kwargs,
    ):
        super().__init__(
            bitrix_token,
            timeout=timeout,
            max_retries=max_retries,
            initial_retry_delay=initial_retry_delay,
            retry_delay_increment=retry_delay_increment,
            **kwargs,
        )
        self.tasks = scopes.Tasks(self)


class ClientV2(ClientV1):
    """
    Bitrix REST API v2 client.

    This client extends the v1 client with additional API methods
    introduced in Bitrix REST API v2.
    """

    VERSION = B24APIVersion.V2

    __slots__ = ()


class ClientV3(BaseClient):
    """
    Bitrix REST API v3 client.

    This client exposes the modern Bitrix REST API v3 which uses a
    structured request/response model and different scope definitions.
    """

    VERSION = B24APIVersion.V3

    __slots__ = (
        "documentation",
        "main",
        "rest",
        "tasks",
    )

    documentation: scopes_v3.Documentation
    main: scopes_v3.Main
    rest: scopes_v3.Rest
    tasks: scopes_v3.Tasks

    def __init__(
            self,
            bitrix_token: BitrixTokenFullProtocol,
            *,
            timeout: Timeout = None,
            max_retries: Optional[int] = None,
            initial_retry_delay: Optional[Number] = None,
            retry_delay_increment: Optional[Number] = None,
            **kwargs,
    ):
        super().__init__(
            bitrix_token,
            timeout=timeout,
            max_retries=max_retries,
            initial_retry_delay=initial_retry_delay,
            retry_delay_increment=retry_delay_increment,
            **kwargs,
        )
        self.documentation = scopes_v3.Documentation(self)
        self.main = scopes_v3.Main(self)
        self.rest = scopes_v3.Rest(self)
        self.tasks = scopes_v3.Tasks(self)


# noinspection PyPep8Naming
@overload
def Client(
        bitrix_token: BitrixTokenFullProtocol,
        *,
        prefer_version: Literal[2, B24APIVersion.V2] = B24APIVersion.V2,
        timeout: Timeout = None,
        max_retries: Optional[int] = None,
        initial_retry_delay: Optional[Number] = None,
        retry_delay_increment: Optional[Number] = None,
        **kwargs,
) -> ClientV2: ...


# noinspection PyPep8Naming
@overload
def Client(
        bitrix_token: BitrixTokenFullProtocol,
        *,
        prefer_version: Literal[1, B24APIVersion.V1],
        timeout: Timeout = None,
        max_retries: Optional[int] = None,
        initial_retry_delay: Optional[Number] = None,
        retry_delay_increment: Optional[Number] = None,
        **kwargs,
) -> ClientV1: ...


# noinspection PyPep8Naming
@overload
def Client(
        bitrix_token: BitrixTokenFullProtocol,
        *,
        prefer_version: Literal[3, B24APIVersion.V3],
        timeout: Timeout = None,
        max_retries: Optional[int] = None,
        initial_retry_delay: Optional[Number] = None,
        retry_delay_increment: Optional[Number] = None,
        **kwargs,
) -> ClientV3: ...


def Client(  # noqa: N802
        bitrix_token: BitrixTokenFullProtocol,
        *,
        prefer_version: Union[B24APIVersionLiteral, B24APIVersion] = B24APIVersion.V2,
        timeout: Timeout = None,
        max_retries: Optional[int] = None,
        initial_retry_delay: Optional[Number] = None,
        retry_delay_increment: Optional[Number] = None,
        **kwargs,
) -> BaseClient:
    """
    Client factory function.

    Creates a Bitrix API client instance for the requested API version.

    Parameters
    ----------
    bitrix_token : BitrixTokenFullProtocol
        Authentication token used for API access.

    prefer_version : int or B24APIVersion, default V2
        Preferred Bitrix API version.

    timeout : Timeout, optional
        Default request timeout.

    max_retries : int, optional
        Maximum retry attempts.

    initial_retry_delay : Number, optional
        Initial delay between retries.

    retry_delay_increment : Number, optional
        Retry delay increment.

    Returns
    -------
    BaseClient
        Client instance corresponding to the requested API version.

    Raises
    ------
    ValueError
        If an unsupported API version is specified.
    """

    if prefer_version == ClientV1.VERSION:
        client_class = ClientV1

    elif prefer_version == ClientV2.VERSION:
        client_class = ClientV2

    elif prefer_version == ClientV3.VERSION:
        client_class = ClientV3

    else:
        raise ValueError("Invalid prefer_version, must be 1, 2 or 3")

    return client_class(
        bitrix_token=bitrix_token,
        timeout=timeout,
        max_retries=max_retries,
        initial_retry_delay=initial_retry_delay,
        retry_delay_increment=retry_delay_increment,
        **kwargs,
    )
