# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
# See LICENSE for details.
"""Declarative configuration schema with error collection.

ConfigSchema is the top-level abstraction. Subclasses declare fields as
class attributes, then call .load() to resolve them from environment
variables and overrides.
"""

import os
from collections.abc import Mapping
from typing import Any, ClassVar, Self

from .errors import ConfigError, ConfigErrors, DecodeError, MissingFieldError, ValidationError
from .field import _MISSING_TYPE, MISSING, Field
from .resolve import resolve_value


class ConfigSchema:
    """Base class for declarative configuration.

    Subclasses declare Field attributes and an optional ENV_PREFIX.
    Call .load() to resolve all fields and return a populated instance.

    Examples:
        >>> class AppConfig(ConfigSchema):
        ...     ENV_PREFIX = "MYAPP_"
        ...     debug = Field.bool(default=False)
        ...     port = Field.int(default=8080, validator=lambda p: 1 <= p <= 65535)
        ...
        >>> config = AppConfig.load()
        >>> config.debug   # bool
        >>> config.port    # int
    """

    ENV_PREFIX: ClassVar[str] = ""

    @classmethod
    def _collect_fields(cls) -> dict[str, Field]:
        """Scan class hierarchy for Field instances."""
        fields: dict[str, Field] = {}
        for klass in reversed(cls.__mro__):
            for attr, val in vars(klass).items():
                if isinstance(val, Field):
                    fields[attr] = val
        return fields

    @classmethod
    def load(
        cls,
        *,
        overrides: dict[str, Any] | None = None,
        env: Mapping[str, str] | None = None,
    ) -> Self:
        """Resolve all fields and return a populated instance.

        Resolution order for each field:
            1. overrides[field_name] — programmatic override (skip decoding).
            2. env[ENV_PREFIX + FIELD_NAME] — environment variable.
            3. field.default — declared default.
            4. MISSING → MissingFieldError.

        Args:
            overrides: Dict of field_name -> already-typed values.
                A value of None is treated as "not provided" (falls through to env/default).
            env: String-keyed mapping to read env vars from. Defaults to os.environ.

        Returns:
            Instance with resolved field values set as attributes.

        Raises:
            ConfigErrors: If one or more fields fail resolution, decoding,
                or validation. Contains all errors, not just the first.
        """
        overrides = overrides or {}
        env = env if env is not None else os.environ
        fields = cls._collect_fields()
        errors: list[ConfigError] = []
        instance = object.__new__(cls)

        for attr, field in fields.items():
            env_name = field.env_name or f"{cls.ENV_PREFIX}{attr.upper()}"

            # Concept 2: extract + decode from environment
            try:
                env_val = field.decode_env(env, env_name)
            except DecodeError as exc:
                exc.field_name = attr
                if field.sensitive:
                    exc.raw_value = "***"
                    exc.args = (
                        f"Field '{attr}' (env: {env_name}) failed to decode (value masked).",
                    )
                errors.append(exc)
                continue

            # Concept 1: priority resolution (override > env > default)
            value = resolve_value(overrides.get(attr), env_val, default=field.default)

            # Missing: no value from any source
            if isinstance(value, _MISSING_TYPE):
                errors.append(
                    MissingFieldError(
                        f"Field '{attr}' (env: {env_name}) has no value and no default.",
                        field_name=attr,
                        env_name=env_name,
                    )
                )
                continue

            # Validate
            if field.validator is not None:
                try:
                    if not field.validator(value):
                        display = "***" if field.sensitive else repr(value)
                        errors.append(
                            ValidationError(
                                f"Field '{attr}' failed validation with value {display}.",
                                field_name=attr,
                                value="***" if field.sensitive else value,
                            )
                        )
                        continue
                except Exception as exc:
                    errors.append(
                        ValidationError(
                            f"Field '{attr}' validator raised {type(exc).__name__}: {exc}",
                            field_name=attr,
                            value="***" if field.sensitive else value,
                        )
                    )
                    continue

            setattr(instance, attr, value)

        if errors:
            raise ConfigErrors(errors)

        return instance

    def __repr__(self) -> str:
        fields = self.__class__._collect_fields()
        parts = []
        for attr, field in fields.items():
            value = getattr(self, attr, MISSING)
            display = (
                "'***'"
                if field.sensitive and not isinstance(value, _MISSING_TYPE)
                else repr(value)
            )
            parts.append(f"{attr}={display}")
        return f"{self.__class__.__name__}({', '.join(parts)})"


__all__ = ["ConfigSchema"]
