import logging
from typing import Any, List, Type

logger = logging.getLogger(__name__)


class PVField:
    """
    Record-field descriptor for an EPICS PV (e.g. NELM, NORD).

    Always has ``val`` and ``raw``; full ``name`` is set by the parent PV
    during prefix formatting. When ``cast_type`` is set, assigning to ``val``
    casts to that type.
    """

    def __init__(
        self,
        name: str,
        cast_type: Type[Any] | None = None,
    ) -> None:
        self.name = name
        self.cast_type = cast_type

        self.pv_name = None
        self._val: Any = None
        self._raw: Any = None

    @property
    def val(self) -> Any:
        return self._val

    @val.setter
    def val(self, v: Any) -> None:
        if v is None or self.cast_type is None:
            self._val = v
        else:
            self._val = self.cast_type(v)

    @property
    def raw(self) -> Any:
        return self._raw

    @raw.setter
    def raw(self, v: Any) -> None:
        self._raw = v

    def __str__(self) -> str:
        return (
            f"PVField(name={self.name!r}, cast_type={self.cast_type}, "
            f"val={self.val}, raw={self.raw})"
        )

    def __repr__(self) -> str:
        return f"PVField({self.name!r}, cast_type={self.cast_type})"


class PV:
    """Process Variable descriptor: name, metadata, and live value/raw from IOC.

    Every PV has a built-in ``VAL`` field (``PVField`` with ``val`` / ``raw`` and
    ``cast_type`` taken from the PV's ``cast_type`` argument). It is appended
    after any user ``fields`` and must not be passed in ``fields``.

    When ``cast_type`` is set (e.g. int, float), assigning to the VAL field's
    ``val`` casts the value to that type; invalid casts raise (e.g.
    int("hello")).

    Optional ``fields`` is a list of extra ``PVField`` instances (e.g. NELM);
    each field's full name becomes ``parent.name + "." + name`` after
    formatting. Each field is exposed as a lowercase attribute (e.g.
    ``pv.nelm``).
    """

    _PV_RESERVED_ATTRS = frozenset(
        {"name", "cast_type", "fields", "tags", "val", "raw", "val_field"}
    )

    def _build_fields(self, fields: List[PVField] | None) -> List[PVField]:
        """Return user fields plus the required built-in VAL field."""
        user_fields = list(fields) if fields is not None else []
        for field in user_fields:
            if field.name.lower() == "val":
                raise ValueError(
                    "The VAL field is created by PV; omit it from `fields`."
                )
        return user_fields + [self._val_field]

    def __init__(
        self,
        name: str,
        cast_type: Type[Any] | None = None,
        fields: List[PVField] | None = None,
        tags: List[str] | None = None,
    ) -> None:
        self.name = name
        self.cast_type = cast_type
        self._val_field = PVField("VAL", cast_type=cast_type)
        self.fields = self._build_fields(fields)
        self._setup_fields()
        self.tags = tags
        logger.debug("PV initialized: %s", self)
        logger.debug("Attributes: %s", self.__dict__)

    @property
    def val_field(self) -> PVField:
        """Return the built-in VAL field descriptor."""
        return self._val_field

    @property
    def val(self) -> Any:
        """Return current typed value for the built-in VAL field."""
        return self._val_field.val

    @val.setter
    def val(self, v: Any) -> None:
        self._val_field.val = v

    @property
    def raw(self) -> Any:
        """Return current raw value for the built-in VAL field."""
        return self._val_field.raw

    @raw.setter
    def raw(self, v: Any) -> None:
        self._val_field.raw = v

    def _setup_fields(self) -> None:
        """Validate, format, and bind fields as lowercase attributes."""
        for field in self.fields:
            if not isinstance(field, PVField):
                raise TypeError(
                    f"fields must be a list of PVField instances; got {type(field).__name__}."
                )
            field.pv_name = f"{self.name}.{field.name.upper()}"
            attr = field.name.lower()
            if attr == "val":
                continue
            if attr in self._PV_RESERVED_ATTRS:
                raise ValueError(
                    f"PV field name '{field.name}' (attribute '{attr}') shadows a reserved PV attribute."
                )
            object.__setattr__(self, attr, field)

    def _field_vals_for_display(self) -> list[dict[str, Any]]:
        """Field name, val, and raw for string/repr display."""
        return [{"name": f.name, "val": f.val, "raw": f.raw} for f in self.fields]

    def __str__(self) -> str:
        return (
            f"PV(name={self.name!r}, cast_type={self.cast_type}, "
            f"fields={self._field_vals_for_display()})"
        )

    def __repr__(self) -> str:
        return (
            f"PV({self.name!r}, cast_type={self.cast_type}, "
            f"fields={self._field_vals_for_display()})"
        )

    def all_fields(self) -> List[PVField]:
        """Return PVField entries for this PV, preserving declaration order."""
        return [field for field in self.fields if isinstance(field, PVField)]
