# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
# See LICENSE for details.
"""Pure decoders: str -> T.

Each decoder accepts a non-empty, stripped string and returns a typed value.
On invalid input, raises DecodeError with a clear message.
The caller is responsible for handling None/blank values before calling.
"""

import json
import os
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Any

from .errors import DecodeError

TRUE_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on", "enabled"})
FALSE_VALUES: frozenset[str] = frozenset({"0", "false", "no", "off", "disabled"})


def decode_bool(value: str) -> bool:
    """Decode a boolean from a string.

    Args:
        value: Non-empty, stripped string to decode.

    Returns:
        True or False.

    Raises:
        DecodeError: If value is not in the recognized true/false sets.
    """
    lower = value.lower()
    if lower in TRUE_VALUES:
        return True
    if lower in FALSE_VALUES:
        return False
    raise DecodeError(
        f"Cannot decode '{value}' as bool. Valid values: {sorted(TRUE_VALUES | FALSE_VALUES)}",
        raw_value=value,
    )


def decode_int(value: str) -> int:
    """Decode an integer from a string.

    Args:
        value: Non-empty, stripped string to decode.

    Returns:
        Parsed integer.

    Raises:
        DecodeError: If value cannot be parsed as int.
    """
    try:
        return int(value)
    except ValueError as exc:
        raise DecodeError(
            f"Cannot decode '{value}' as int.",
            raw_value=value,
        ) from exc


def decode_float(value: str) -> float:
    """Decode a float from a string.

    Args:
        value: Non-empty, stripped string to decode.

    Returns:
        Parsed float.

    Raises:
        DecodeError: If value cannot be parsed as float.
    """
    try:
        return float(value)
    except ValueError as exc:
        raise DecodeError(
            f"Cannot decode '{value}' as float.",
            raw_value=value,
        ) from exc


def decode_str(value: str) -> str:
    """Decode a string (strip whitespace).

    Args:
        value: Non-empty, stripped string to decode.

    Returns:
        The stripped string value.
    """
    return value.strip()


def decode_path(value: str) -> Path:
    """Decode a filesystem path from a string.

    Args:
        value: Non-empty, stripped string to decode.

    Returns:
        Path object.
    """
    return Path(value.strip())


def decode_json(value: str) -> Any:
    """Decode a JSON value from a string.

    Args:
        value: Non-empty, stripped string containing valid JSON.

    Returns:
        Parsed Python object.

    Raises:
        DecodeError: If value is not valid JSON.
    """
    try:
        return json.loads(value)
    except (json.JSONDecodeError, TypeError) as exc:
        raise DecodeError(
            f"Cannot decode '{value}' as JSON: {exc}",
            raw_value=value,
        ) from exc


def read_env(var: str, env: Mapping[str, str] | None = None) -> str | None:
    """Read a raw environment variable value, stripped.

    Args:
        var: Environment variable name.
        env: String-keyed mapping to read from. Defaults to os.environ.

    Returns:
        Stripped string value, or None if unset or blank.
    """
    source = env if env is not None else os.environ
    raw = source.get(var, "").strip()
    return raw or None


def make_bool_decoder(true_values: set[str], false_values: set[str]) -> Callable[[str], bool]:
    """Create a boolean decoder with custom true/false value sets.

    Args:
        true_values: Set of string values to treat as True.
        false_values: Set of string values to treat as False.

    Returns:
        A decoder function: str -> bool.
    """

    def _decode(value: str) -> bool:
        lower = value.lower()
        if lower in true_values:
            return True
        if lower in false_values:
            return False
        raise DecodeError(
            f"Cannot decode '{value}' as bool. Valid values: {sorted(true_values | false_values)}",
            raw_value=value,
        )

    return _decode


__all__ = [
    "TRUE_VALUES",
    "FALSE_VALUES",
    "read_env",
    "decode_bool",
    "decode_int",
    "decode_float",
    "decode_str",
    "decode_path",
    "decode_json",
    "make_bool_decoder",
]
