"""Internal validation helpers for coercing config values.

These functions validate and coerce configuration values (from YAML, env,
or user overrides) into their expected Python types. They are shared by
the catalog and pricing modules to avoid duplication.

All functions accept a ``source`` and ``field`` parameter for actionable
error messages that point users to the misconfigured entry.
"""

from __future__ import annotations

from typing import Any


class CoercionError(ValueError):
    """Raised when a configuration value cannot be coerced to the expected type."""

    def __init__(
        self,
        *,
        source: str,
        field: str,
        value: object,
        detail: str,
    ) -> None:
        self.source = source
        self.field = field
        self.value = value
        super().__init__(f"Invalid {field} in {source}: {detail} (got {value!r})")


def coerce_positive_int(
    value: Any,
    *,
    source: str,
    field: str,
) -> int:
    """Coerce a value to a positive integer.

    Args:
        value: Raw value from configuration.
        source: Human-readable config source for error messages.
        field: Field name for error messages.

    Returns:
        Validated positive integer.

    Raises:
        CoercionError: If value is None, non-numeric, fractional, or non-positive.
    """
    if value is None:
        raise CoercionError(
            source=source, field=field, value=value,
            detail="must be a positive integer",
        )
    if isinstance(value, bool):
        raise CoercionError(
            source=source, field=field, value=value,
            detail="must be a positive integer (not boolean)",
        )

    if isinstance(value, int):
        parsed = value
    elif isinstance(value, float):
        if not value.is_integer():
            raise CoercionError(
                source=source, field=field, value=value,
                detail="must be an integer (no fractional values)",
            )
        parsed = int(value)
    elif isinstance(value, str):
        cleaned = value.strip()
        if not cleaned:
            raise CoercionError(
                source=source, field=field, value=value,
                detail="must be a positive integer",
            )
        try:
            parsed = int(cleaned)
        except ValueError as exc:
            raise CoercionError(
                source=source, field=field, value=value,
                detail="must be a positive integer",
            ) from exc
    else:
        raise CoercionError(
            source=source, field=field, value=value,
            detail="must be a positive integer",
        )

    if parsed <= 0:
        raise CoercionError(
            source=source, field=field, value=value,
            detail="must be positive",
        )
    return parsed


def coerce_nonnegative_float(
    value: Any,
    *,
    source: str,
    field: str,
) -> float:
    """Coerce a value to a non-negative float.

    Args:
        value: Raw value from configuration.
        source: Human-readable config source for error messages.
        field: Field name for error messages.

    Returns:
        Validated non-negative float.

    Raises:
        CoercionError: If value is None, non-numeric, or negative.
    """
    if value is None:
        raise CoercionError(
            source=source, field=field, value=value,
            detail="must be a non-negative number",
        )
    if isinstance(value, bool):
        raise CoercionError(
            source=source, field=field, value=value,
            detail="must be a non-negative number (not boolean)",
        )
    try:
        parsed = float(value)
    except (TypeError, ValueError) as exc:
        raise CoercionError(
            source=source, field=field, value=value,
            detail="must be a non-negative number",
        ) from exc
    if parsed < 0:
        raise CoercionError(
            source=source, field=field, value=value,
            detail="must be non-negative",
        )
    return parsed


def coerce_string_tuple(
    value: Any,
    *,
    source: str,
    field: str,
) -> tuple[str, ...]:
    """Coerce a value to a tuple of non-empty strings.

    Args:
        value: Raw value — may be a single string, list, tuple, or set.
        source: Human-readable config source for error messages.
        field: Field name for error messages.

    Returns:
        Tuple of validated non-empty strings.

    Raises:
        CoercionError: If value contains non-string or empty items.
    """
    if value is None:
        return ()

    if isinstance(value, str):
        cleaned = value.strip()
        if not cleaned:
            raise CoercionError(
                source=source, field=field, value=value,
                detail="must be a non-empty string",
            )
        return (cleaned,)

    if not isinstance(value, (list, tuple, set)):
        raise CoercionError(
            source=source, field=field, value=value,
            detail="must be a string or sequence of strings",
        )

    cleaned_items: list[str] = []
    for idx, item in enumerate(value):
        if not isinstance(item, str):
            raise CoercionError(
                source=source, field=field, value=item,
                detail=f"entry at index {idx} must be a string",
            )
        cleaned = item.strip()
        if not cleaned:
            raise CoercionError(
                source=source, field=field, value=item,
                detail=f"entry at index {idx} must be non-empty",
            )
        cleaned_items.append(cleaned)

    return tuple(cleaned_items)
