# ruff: noqa: ARG002
from __future__ import annotations
import logging
import re
import socket
from abc import ABC, abstractmethod
from collections.abc import Callable
from typing import (
    TYPE_CHECKING,
    Any,
    AnyStr,
    ClassVar,
    Generic,
    Literal,
    TypeVar,
    cast,
)
from mopidy.config import validators
from mopidy.internal import log, path
if TYPE_CHECKING:
    from collections.abc import Iterable
T = TypeVar("T")
K = TypeVar("K", bound="ConfigValue")
V = TypeVar("V", bound="ConfigValue")
def decode(value: AnyStr) -> str:
    result = (
        value.decode(errors="surrogateescape") if isinstance(value, bytes) else value
    )
    for char in ("\\", "\n", "\t"):
        result = result.replace(char.encode(encoding="unicode-escape").decode(), char)
    return result
def encode(value: AnyStr) -> str:
    result = (
        value.decode(errors="surrogateescape") if isinstance(value, bytes) else value
    )
    for char in ("\\", "\n", "\t"):
        result = result.replace(char, char.encode(encoding="unicode-escape").decode())
    return result
class DeprecatedValue:
    pass
class _TransformedValue(str):
    __slots__ = ("original",)
    def __new__(cls, _original, transformed):
        return super().__new__(cls, transformed)
    def __init__(self, original, _transformed):
        self.original = original
[docs]
class ConfigValue(ABC, Generic[T]):
    """Represents a config key's value and how to handle it.
    Normally you will only be interacting with sub-classes for config values
    that encode either deserialization behavior and/or validation.
    Each config value should be used for the following actions:
    1. Deserializing from a raw string and validating, raising ValueError on
       failure.
    2. Serializing a value back to a string that can be stored in a config.
    3. Formatting a value to a printable form (useful for masking secrets).
    :class:`None` values should not be deserialized, serialized or formatted,
    the code interacting with the config should simply skip None config values.
    """
[docs]
    @abstractmethod
    def deserialize(self, value: AnyStr) -> T | None:
        """Cast raw string to appropriate type."""
        raise NotImplementedError 
[docs]
    def serialize(self, value: T, display: bool = False) -> str | DeprecatedValue:
        """Convert value back to string for saving."""
        if value is None:
            return ""
        return str(value) 
 
[docs]
class Deprecated(ConfigValue[Any]):
    """Deprecated value.
    Used for ignoring old config values that are no longer in use, but should
    not cause the config parser to crash.
    """
[docs]
    def deserialize(self, value: AnyStr) -> DeprecatedValue:
        return DeprecatedValue() 
[docs]
    def serialize(self, value: Any, display: bool = False) -> DeprecatedValue:
        return DeprecatedValue() 
 
[docs]
class String(ConfigValue[str]):
    r"""String value.
    Is decoded as utf-8, and \n and \t escapes should work and be preserved.
    """
    def __init__(
        self,
        optional: bool = False,
        choices: Iterable[str] | None = None,
        transformer: Callable[[str], str] | None = None,
    ) -> None:
        self._required = not optional
        self._choices = choices
        self._transformer = transformer
[docs]
    def deserialize(self, value: AnyStr) -> str | None:
        result = decode(value).strip()
        validators.validate_required(result, self._required)
        if not result:
            return None
        # This is necessary for backwards-compatibility, in case subclasses
        # aren't calling their parent constructor.
        transformer = getattr(self, "_transformer", None)
        if transformer:
            transformed_value = transformer(result)
            result = _TransformedValue(result, transformed_value)
        validators.validate_choice(result, self._choices)
        return result 
[docs]
    def serialize(self, value: str, display: bool = False) -> str:
        if value is None:
            return ""
        if isinstance(value, _TransformedValue):
            value = value.original
        return encode(value) 
 
[docs]
class Secret(String):
    r"""Secret string value.
    Is decoded as utf-8, and \n and \t escapes should work and be preserved.
    Should be used for passwords, auth tokens etc. Will mask value when being
    displayed.
    """
    def __init__(
        self,
        optional: bool = False,
        choices: None = None,
        transformer: Callable[[str], str] | None = None,
    ) -> None:
        super().__init__(
            optional=optional,
            choices=None,  # Choices doesn't make sense for secrets
            transformer=transformer,
        )
[docs]
    def serialize(self, value: str, display: bool = False) -> str:
        if value is not None and display:
            return "********"
        return super().serialize(value, display) 
 
[docs]
class Integer(ConfigValue[int]):
    """Integer value."""
    def __init__(
        self,
        minimum: int | None = None,
        maximum: int | None = None,
        choices: Iterable[int] | None = None,
        optional: bool = False,
    ) -> None:
        self._required = not optional
        self._minimum = minimum
        self._maximum = maximum
        self._choices = choices
[docs]
    def deserialize(self, value: AnyStr) -> int | None:
        result = decode(value)
        validators.validate_required(result, self._required)
        if not result:
            return None
        result = int(result)
        validators.validate_choice(result, self._choices)
        validators.validate_minimum(result, self._minimum)
        validators.validate_maximum(result, self._maximum)
        return result 
 
[docs]
class Float(ConfigValue[float]):
    """Float value."""
    def __init__(
        self,
        minimum: float | None = None,
        maximum: float | None = None,
        optional: bool = False,
    ) -> None:
        self._required = not optional
        self._minimum = minimum
        self._maximum = maximum
[docs]
    def deserialize(self, value: AnyStr) -> float | None:
        result = decode(value)
        validators.validate_required(result, self._required)
        if not value:
            return None
        result = float(result)
        validators.validate_minimum(result, self._minimum)
        validators.validate_maximum(result, self._maximum)
        return result 
 
[docs]
class Boolean(ConfigValue[bool]):
    """Boolean value.
    Accepts ``1``, ``yes``, ``true``, and ``on`` with any casing as
    :class:`True`.
    Accepts ``0``, ``no``, ``false``, and ``off`` with any casing as
    :class:`False`.
    """
    true_values = ("1", "yes", "true", "on")
    false_values = ("0", "no", "false", "off")
    def __init__(self, optional: bool = False) -> None:
        self._required = not optional
[docs]
    def deserialize(self, value: AnyStr) -> bool | None:
        result = decode(value)
        validators.validate_required(result, self._required)
        if not result:
            return None
        if result.lower() in self.true_values:
            return True
        if result.lower() in self.false_values:
            return False
        msg = f"invalid value for boolean: {result!r}"
        raise ValueError(msg) 
[docs]
    def serialize(
        self,
        value: bool,
        display: bool = False,
    ) -> Literal["true", "false"]:
        if value is True:
            return "true"
        if value in (False, None):
            return "false"
        msg = f"{value!r} is not a boolean"
        raise ValueError(msg) 
 
[docs]
class Pair(ConfigValue[tuple[K, V]]):
    """Pair value.
    The value is expected to be a pair of elements, separated by a specified delimiter.
    Values can optionally not be a pair, in which case the whole input is provided for
    both sides of the value.
    """
    _subtypes: tuple[K, V]
    def __init__(
        self,
        optional: bool = False,
        optional_pair: bool = False,
        separator: str = "|",
        subtypes: tuple[K, V] = (String(), String()),
    ) -> None:
        self._required = not optional
        self._optional_pair = optional_pair
        self._separator = separator
        self._subtypes = subtypes
[docs]
    def deserialize(self, value: AnyStr) -> tuple[K, V] | None:
        raw_value = decode(value).strip()
        validators.validate_required(raw_value, self._required)
        if not raw_value:
            return None
        if self._separator in raw_value:
            values = raw_value.split(self._separator, 1)
        elif self._optional_pair:
            values = (raw_value, raw_value)
        else:
            msg = (
                f"Config value must include {self._separator!r} separator: {raw_value}"
            )
            raise ValueError(msg)
        return cast(
            tuple[K, V],
            (
                self._subtypes[0].deserialize(encode(values[0])),
                self._subtypes[1].deserialize(encode(values[1])),
            ),
        ) 
[docs]
    def serialize(
        self,
        value: tuple[K, V],
        display: bool = False,
    ) -> str | DeprecatedValue:
        serialized_first_value = self._subtypes[0].serialize(value[0], display=display)
        serialized_second_value = self._subtypes[1].serialize(value[1], display=display)
        if isinstance(serialized_first_value, DeprecatedValue) or isinstance(
            serialized_second_value,
            DeprecatedValue,
        ):
            return DeprecatedValue()
        if (
            not display
            and self._optional_pair
            and serialized_first_value == serialized_second_value
        ):
            return serialized_first_value
        return f"{serialized_first_value}{self._separator}{serialized_second_value}" 
 
[docs]
class List(ConfigValue[tuple[V, ...] | frozenset[V]]):
    """List value.
    Supports elements split by commas or newlines. Newlines take precedence and
    empty list items will be filtered out.
    Enforcing unique entries in the list will result in a set data structure
    being used. This does not preserve ordering, which could result in the
    serialized output being unstable.
    """
    def __init__(
        self,
        optional: bool = False,
        unique: bool = False,
        subtype: V = String(),  # noqa: B008
    ) -> None:
        self._required = not optional
        self._unique = unique
        self._subtype = subtype
[docs]
    def deserialize(self, value: AnyStr) -> tuple[V, ...] | frozenset[V]:
        raw_value = decode(value)
        strings: list[str]
        if "\n" in raw_value:
            strings = re.split(r"\s*\n\s*", raw_value)
        else:
            strings = re.split(r"\s*,\s*", raw_value)
        # This is necessary for backwards-compatibility, in case subclasses
        # aren't calling their parent constructor.
        subtype: ConfigValue = getattr(self, "_subtype", String())
        values_iter = (subtype.deserialize(s.strip()) for s in strings if s.strip())
        values = frozenset(values_iter) if self._unique else tuple(values_iter)
        validators.validate_required(values, self._required)
        return cast(tuple[V, ...] | frozenset[V], values) 
[docs]
    def serialize(
        self,
        value: tuple[V, ...] | frozenset[V],
        display: bool = False,
    ) -> str:
        if not value:
            return ""
        # This is necessary for backwards-compatibility, in case subclasses
        # aren't calling their parent constructor.
        subtype: V = getattr(self, "_subtype", String())  # pyright: ignore[reportAssignmentType]
        serialized_values = []
        for item in value:
            serialized_value = subtype.serialize(item, display=display)
            if serialized_value:
                serialized_values.append(serialized_value)
        return "\n  " + "\n  ".join(serialized_values) 
 
[docs]
class LogColor(ConfigValue[log.LogColorName]):
[docs]
    def deserialize(self, value: AnyStr) -> log.LogColorName:
        raw_value = decode(value).lower()
        validators.validate_choice(raw_value, log.COLORS)
        raw_value = cast(log.LogColorName, raw_value)
        return raw_value 
[docs]
    def serialize(self, value: log.LogColorName, display: bool = False) -> str:
        if value.lower() in log.COLORS:
            return encode(value.lower())
        return "" 
 
[docs]
class LogLevel(ConfigValue[int]):
    """Log level value.
    Expects one of ``critical``, ``error``, ``warning``, ``info``, ``debug``,
    ``trace``, or ``all``, with any casing.
    """
    levels: ClassVar[dict[log.LogLevelName, int]] = {
        "critical": logging.CRITICAL,
        "error": logging.ERROR,
        "warning": logging.WARNING,
        "info": logging.INFO,
        "debug": logging.DEBUG,
        "trace": log.TRACE_LOG_LEVEL,
        "all": logging.NOTSET,
    }
[docs]
    def deserialize(self, value: AnyStr) -> int | None:
        raw_value = decode(value).lower()
        validators.validate_choice(raw_value, self.levels.keys())
        raw_value = cast(log.LogLevelName, raw_value)
        return self.levels.get(raw_value) 
[docs]
    def serialize(self, value: int, display: bool = False) -> str:
        lookup = {v: k for k, v in self.levels.items()}
        return encode(lookup.get(value, "")) 
 
[docs]
class Hostname(ConfigValue[str]):
    """Network hostname value."""
    def __init__(self, optional: bool = False) -> None:
        self._required = not optional
[docs]
    def deserialize(self, value: AnyStr, display: bool = False) -> str | None:
        raw_value = decode(value).strip()
        validators.validate_required(raw_value, self._required)
        if not raw_value:
            return None
        socket_path = path.get_unix_socket_path(raw_value)
        if socket_path is not None:
            path_str = Path(not self._required).deserialize(str(socket_path))
            return f"unix:{path_str}"
        try:
            socket.getaddrinfo(raw_value, None)
        except OSError as exc:
            msg = "must be a resolveable hostname or valid IP"
            raise ValueError(msg) from exc
        return raw_value 
 
[docs]
class Port(Integer):
    """Network port value.
    Expects integer in the range 0-65535, zero tells the kernel to simply
    allocate a port for us.
    """
    def __init__(self, choices=None, optional=False):
        super().__init__(
            minimum=0,
            maximum=2**16 - 1,
            choices=choices,
            optional=optional,
        ) 
# Keep this for backwards compatibility
class _ExpandedPath(_TransformedValue):
    pass
[docs]
class Path(ConfigValue[_ExpandedPath]):
    """File system path.
    The following expansions of the path will be done:
    - ``~`` to the current user's home directory
    - ``$XDG_CACHE_DIR`` according to the XDG spec
    - ``$XDG_CONFIG_DIR`` according to the XDG spec
    - ``$XDG_DATA_DIR`` according to the XDG spec
    - ``$XDG_MUSIC_DIR`` according to the XDG spec
    """
    def __init__(self, optional=False):
        self._required = not optional
[docs]
    def deserialize(self, value: AnyStr) -> _ExpandedPath | None:
        raw_value = decode(value).strip()
        expanded = path.expand_path(raw_value)
        validators.validate_required(raw_value, self._required)
        validators.validate_required(expanded, self._required)
        if not raw_value or expanded is None:
            return None
        return _ExpandedPath(raw_value, expanded) 
[docs]
    def serialize(
        self,
        value: None | (_ExpandedPath | bytes),
        display: bool = False,
    ) -> str:
        if value is None:
            return ""
        result = value
        if isinstance(result, _ExpandedPath):
            result = result.original
        if isinstance(result, bytes):
            result = result.decode(errors="surrogateescape")
        return str(result)