from __future__ import annotations
from abc import ABC
from dataclasses import dataclass
from typing import Any, Optional, ClassVar
from zempy.zosapi.core.interop import run_native, ensure_not_none
from zempy.bridge import zemax_exceptions as _exc
from zempy.zosapi.analysis.adapters.Message import Message
from zempy.zosapi.analysis.enums.error_types import ErrorType


@dataclass(frozen=True, slots=True)
class _BaseIASSelector(ABC):
    """
    Base for IAS selectors (Field / Surface / Wavelength) that expose
    a single integer-valued 'get' and a 'set' returning IMessage.
    Subclasses must define:
      - _iface_attr: name of the dataclass field holding the native iface
      - _GET_NAME  : member name to read current selection (property or 0-arg method)
      - _SET_NAME  : method name that sets selection and returns IMessage
      - _LABEL     : for logging tags (optional)
    """
    zosapi: object

    # Subclasses add the actual native interface field name used by _iface_attr
    # e.g. IAS_Field declares: _iface_attr = "_ias_field" and dataclass field: _ias_field: Any
    _iface_attr: ClassVar[str] = ""
    _GET_NAME:   ClassVar[str] = ""
    _SET_NAME:   ClassVar[str] = ""
    _LABEL:      ClassVar[str] = "IAS_Selector"

    @classmethod
    def from_native(cls, zosapi: object, iface: Any) -> "_BaseIASSelector":
        if iface is None:
            raise ValueError(f"{cls.__name__}.from_native: iface is None")
        if not cls._iface_attr:
            raise ValueError(f"{cls.__name__}._iface_attr is not set")
        # Construct by injecting the subclass' iface field
        return cls(zosapi=zosapi, **{cls._iface_attr: iface})  # type: ignore[arg-type]

    # ---- Standardized native access ----
    @property
    def native(self) -> Any:
        return self._iface()

    def _iface(self) -> Any:
        return getattr(self, self.__class__._iface_attr)

    def ensure_native(self) -> None:
        ensure_not_none(self._iface(), what=f"{self.__class__.__name__}.native", exc_type=_exc.ZemaxObjectGone)

    # ---- Helpers ----
    @staticmethod
    def _get_attr_maybe_call(obj: Any, name: str) -> Any:
        attr = getattr(obj, name)
        return attr() if callable(attr) else attr

    def _wrap_message(self, native_msg: Any) -> Message:
        # Prefer using from_native with zosapi context
        return Message.from_native(self.zosapi, native_msg)

    # ---- API ----
    def get(self) -> Optional[int]:
        """Read current selection; supports property or 0-arg method."""
        try:
            val = run_native(
                f"{self._LABEL}.{self._GET_NAME} get",
                lambda: self._get_attr_maybe_call(self._iface(), self.__class__._GET_NAME),
                ensure=self.ensure_native,
            )
            return None if val is None else int(val)
        except Exception:
            return None

    def set(self, n: int) -> Message:
        """Set selection; returns IMessage wrapped as Message."""
        try:
            native_msg = run_native(
                f"{self._LABEL}.{self._SET_NAME}({n})",
                lambda: getattr(self._iface(), self.__class__._SET_NAME)(int(n)),
                ensure=self.ensure_native,
            )
            return self._wrap_message(native_msg)
        except Exception as ex:
            # Fallback synthetic Message if native path failed
            # (adjust if your Message adapter has a better factory)
            return Message(code=ErrorType.Unknown, text=str(ex))  # type: ignore[arg-type]

    def __str__(self) -> str:
        n = self.get()
        return f"{n if n is not None else 'UNKNOWN'}"
