from __future__ import annotations

from dataclasses import dataclass, field
from typing import List, Optional

from redis.utils import SENTINEL

_BRACES = {"(", ")", "[", "]", "{", "}"}


def _validate_no_invalid_chars(value: str, field_name: str) -> None:
    """Ensure value contains only printable ASCII without spaces or braces.

    This mirrors the constraints enforced by other Redis clients for values that
    will appear in CLIENT LIST / CLIENT INFO output.
    """

    for ch in value:
        # printable ASCII without space: '!' (0x21) to '~' (0x7E)
        if ord(ch) < 0x21 or ord(ch) > 0x7E or ch in _BRACES:
            raise ValueError(
                f"{field_name} must not contain spaces, newlines, non-printable characters, or braces"
            )


def _validate_driver_name(name: str) -> None:
    """Validate an upstream driver name.

    The name should look like a typical Python distribution or package name,
    following a simplified form of PEP 503 normalisation rules:

    * start with a lowercase ASCII letter
    * contain only lowercase letters, digits, hyphens and underscores

    Examples of valid names: ``"django-redis"``, ``"celery"``, ``"rq"``.
    """

    import re

    _validate_no_invalid_chars(name, "Driver name")
    if not re.match(r"^[a-z][a-z0-9_-]*$", name):
        raise ValueError(
            "Upstream driver name must use a Python package-style name: "
            "start with a lowercase letter and contain only lowercase letters, "
            "digits, hyphens, and underscores (e.g., 'django-redis')."
        )


def _validate_driver_version(version: str) -> None:
    _validate_no_invalid_chars(version, "Driver version")


def _format_driver_entry(driver_name: str, driver_version: str) -> str:
    return f"{driver_name}_v{driver_version}"


@dataclass
class DriverInfo:
    """Driver information used to build the CLIENT SETINFO LIB-NAME and LIB-VER values.

    This class consolidates all driver metadata (redis-py version and upstream drivers)
    into a single object that is propagated through connection pools and connections.

    The formatted name follows the pattern::

        name(driver1_vVersion1;driver2_vVersion2)

    Parameters
    ----------
    name : str, optional
        The base library name. If omitted, defaults to "redis-py". If None,
        LIB-NAME will not be sent.
    lib_version : str, optional
        The redis-py library version. If omitted, the version will be determined
        automatically from the installed package. If None, LIB-VER will not be sent.

    Examples
    --------
    >>> info = DriverInfo()
    >>> info.formatted_name
    'redis-py'

    >>> info = DriverInfo().add_upstream_driver("django-redis", "5.4.0")
    >>> info.formatted_name
    'redis-py(django-redis_v5.4.0)'

    >>> info = DriverInfo(lib_version="5.0.0")
    >>> info.lib_version
    '5.0.0'
    """

    name: Optional[str] | object = SENTINEL
    lib_version: Optional[str] | object = SENTINEL
    _upstream: List[str] = field(default_factory=list)

    def __post_init__(self):
        """Initialize default metadata if not explicitly provided."""
        if self.name is SENTINEL:
            self.name = "redis-py"
        if self.lib_version is SENTINEL:
            from redis.utils import get_lib_version

            self.lib_version = get_lib_version()

    @property
    def upstream_drivers(self) -> List[str]:
        """Return a copy of the upstream driver entries.

        Each entry is in the form ``"driver-name_vversion"``.
        """

        return list(self._upstream)

    def add_upstream_driver(
        self, driver_name: str, driver_version: str
    ) -> "DriverInfo":
        """Add an upstream driver to this instance and return self.

        The most recently added driver appears first in :pyattr:`formatted_name`.
        """

        if driver_name is None:
            raise ValueError("Driver name must not be None")
        if driver_version is None:
            raise ValueError("Driver version must not be None")

        _validate_driver_name(driver_name)
        _validate_driver_version(driver_version)

        entry = _format_driver_entry(driver_name, driver_version)
        # insert at the beginning so latest is first
        self._upstream.insert(0, entry)
        return self

    @property
    def formatted_name(self) -> Optional[str]:
        """Return the base name with upstream drivers encoded, if any.

        With no upstream drivers, this is just :pyattr:`name`. Otherwise::

            name(driver1_vX;driver2_vY)
        """

        name = self.name
        if not isinstance(name, str) or not name:
            return None
        if not self._upstream:
            return name
        return f"{name}({';'.join(self._upstream)})"


def resolve_driver_info(
    driver_info: Optional[DriverInfo] | object = SENTINEL,
    lib_name: Optional[str] | object = SENTINEL,
    lib_version: Optional[str] | object = SENTINEL,
) -> Optional[DriverInfo]:
    """Resolve driver_info from parameters.

    If driver_info is provided, use it. Otherwise, create DriverInfo from
    lib_name and lib_version (using defaults only for sentinel values).

    Parameters
    ----------
    driver_info : DriverInfo, optional
        The DriverInfo instance to use
    lib_name : str, optional
        The library name (default: "redis-py")
    lib_version : str, optional
        The library version (default: auto-detected)

    Returns
    -------
    DriverInfo, optional
        The resolved DriverInfo instance
    """
    if driver_info is SENTINEL:
        if lib_name is None and lib_version is None:
            return None
        return DriverInfo(name=lib_name, lib_version=lib_version)

    if driver_info is None or isinstance(driver_info, DriverInfo):
        return driver_info

    raise TypeError("driver_info must be a DriverInfo instance or None")
