# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
# See LICENSE for details.
"""Field descriptor for typed configuration values.

A Field declares what a config value looks like: its decoder, default,
and optional validation. Fields are used as class attributes on
ConfigSchema subclasses and resolved during .load().
"""

from __future__ import annotations

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

from .decode import (
    decode_bool,
    decode_float,
    decode_int,
    decode_json,
    decode_path,
    decode_str,
    read_env,
)


class _MISSING_TYPE:
    """Sentinel for 'no default provided', distinct from None."""

    _instance: _MISSING_TYPE | None = None

    def __new__(cls) -> _MISSING_TYPE:
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __repr__(self) -> str:
        return "MISSING"

    def __bool__(self) -> bool:
        return False


MISSING = _MISSING_TYPE()
"""Sentinel indicating no default was provided."""


class Field[T]:
    """Descriptor for a typed configuration field.

    Attributes:
        decoder: Pure function that converts a non-empty string to T.
        default: Default value, or MISSING if none was provided.
        validator: Optional function that returns True if the decoded value is valid.
        env_name: Explicit env var name override (bypasses auto-generated prefix+attr).
        description: Human-readable description of this field.
        sensitive: If True, raw values are masked in error messages.
    """

    def __init__(
        self,
        decoder: Callable[[str], T],
        *,
        default: T | _MISSING_TYPE = MISSING,
        validator: Callable[[T], bool] | None = None,
        env_name: str | None = None,
        description: str | None = None,
        sensitive: bool = False,
    ) -> None:
        self.decoder = decoder
        self.default = default
        self.validator = validator
        self.env_name = env_name
        self.description = description
        self.sensitive = sensitive

    @classmethod
    def bool(
        cls,
        *,
        default: bool | _MISSING_TYPE = MISSING,
        validator: Callable[[bool], bool] | None = None,
        env_name: str | None = None,
        description: str | None = None,
        sensitive: bool = False,
    ) -> Field[bool]:
        """Create a boolean field."""
        return cls(
            decode_bool,
            default=default,
            validator=validator,
            env_name=env_name,
            description=description,
            sensitive=sensitive,
        )

    @classmethod
    def int(
        cls,
        *,
        default: int | _MISSING_TYPE = MISSING,
        validator: Callable[[int], bool] | None = None,
        env_name: str | None = None,
        description: str | None = None,
        sensitive: bool = False,
    ) -> Field[int]:
        """Create an integer field."""
        return cls(
            decode_int,
            default=default,
            validator=validator,
            env_name=env_name,
            description=description,
            sensitive=sensitive,
        )

    @classmethod
    def float(
        cls,
        *,
        default: float | _MISSING_TYPE = MISSING,
        validator: Callable[[float], bool] | None = None,
        env_name: str | None = None,
        description: str | None = None,
        sensitive: bool = False,
    ) -> Field[float]:
        """Create a float field."""
        return cls(
            decode_float,
            default=default,
            validator=validator,
            env_name=env_name,
            description=description,
            sensitive=sensitive,
        )

    @classmethod
    def str(
        cls,
        *,
        default: str | _MISSING_TYPE = MISSING,
        validator: Callable[[str], bool] | None = None,
        env_name: str | None = None,
        description: str | None = None,
        sensitive: bool = False,
    ) -> Field[str]:
        """Create a string field."""
        return cls(
            decode_str,
            default=default,
            validator=validator,
            env_name=env_name,
            description=description,
            sensitive=sensitive,
        )

    @classmethod
    def path(
        cls,
        *,
        default: Path | _MISSING_TYPE = MISSING,
        validator: Callable[[Path], bool] | None = None,
        env_name: str | None = None,
        description: str | None = None,
        sensitive: bool = False,
    ) -> Field[Path]:
        """Create a path field."""
        return cls(
            decode_path,
            default=default,
            validator=validator,
            env_name=env_name,
            description=description,
            sensitive=sensitive,
        )

    @classmethod
    def json(
        cls,
        *,
        default: Any | _MISSING_TYPE = MISSING,
        validator: Callable[[Any], bool] | None = None,
        env_name: str | None = None,
        description: str | None = None,
        sensitive: bool = False,
    ) -> Field[Any]:
        """Create a JSON field."""
        return cls(
            decode_json,
            default=default,
            validator=validator,
            env_name=env_name,
            description=description,
            sensitive=sensitive,
        )

    def decode_env(self, env: Mapping[str, str], env_name: str) -> T | None:
        """Read an env var and decode it using this field's decoder.

        Args:
            env: String-keyed mapping (e.g. os.environ).
            env_name: The environment variable name to look up.

        Returns:
            Decoded value, or None if the env var is unset or blank.

        Raises:
            DecodeError: If the raw value cannot be decoded.
        """
        raw = read_env(env_name, env)
        if raw is None:
            return None
        return self.decoder(raw)

    def __repr__(self) -> str:
        default_str = (
            f", default={self.default!r}" if not isinstance(self.default, _MISSING_TYPE) else ""
        )
        return f"<Field(decoder={self.decoder.__name__}{default_str})>"


__all__ = [
    "MISSING",
    "_MISSING_TYPE",
    "Field",
]
