import functools
import logging
import re
from typing import Any, Iterator

from .daemon_status import TaskStatus
from .logging import LogLevel
from .pv import PV, PVField

logger = logging.getLogger(__name__)


class BasePVInterface:
    """
    Base configuration class for EPICS PV interfaces.

    Subclasses define PV instances with template placeholders using Python
    format syntax (e.g. "{main}", "{p1}", "{p2}"). Prefix resolution runs
    automatically after the subclass __init__ completes; no need to call
    _format_pv_names().

    Example::

        class MyInterface(BasePVInterface):
            def __init__(self, prefixes: dict | None = None) -> None:
                super().__init__(prefixes=prefixes)
                self.sensor = PV("{main}Sensor:01", cast_type=float)

        iface = MyInterface(prefixes={"main": "DEV:01:"})
        assert iface.trigger.name == "DEV:01:Trigger"
        assert iface.sensor.name == "DEV:01:Sensor:01"
    """

    def __init_subclass__(cls, **kwargs: Any) -> None:
        super().__init_subclass__(**kwargs)
        original_init = cls.__init__

        @functools.wraps(original_init)
        def _wrapped(self: Any, *args: Any, **kw: Any) -> None:
            original_init(self, *args, **kw)
            if type(self) is cls:
                self._format_pv_names()
                logger.info("%s", self._format_for_log())

        cls.__init__ = _wrapped

    def __init__(self, prefixes: dict[str, str] | None = None) -> None:
        self._pv_attr_names: list[str] = []
        self.prefixes = self._validate_prefixes(prefixes)

        self.trigger = PV("{main}Trigger", cast_type=bool, tags=["control"])
        self.heartbeat = PV("{main}Heartbeat", cast_type=bool, tags=["control"])
        self.busy = PV("{main}Busy", cast_type=bool, tags=["status"])
        self.task_status = PV("{main}TaskStatus", cast_type=TaskStatus, tags=["status"])
        self.task_duration = PV("{main}TaskDuration", cast_type=float, tags=["status"])
        self.warning_log = PV("{main}WarningLog", tags=["status", "diagnostic"])
        self.error_log = PV("{main}ErrorLog", tags=["status", "diagnostic"])
        self.log_level = PV(
            "{main}LogLevel", cast_type=LogLevel, tags=["control", "diagnostic"]
        )
        self.silenced_loggers = PV(
            "{main}SilencedLoggers",
            tags=["control", "diagnostic"],
        )

        if type(self) is BasePVInterface:
            self._format_pv_names()

    @staticmethod
    def _validate_prefixes(prefixes: dict[str, str] | None) -> dict[str, str]:
        """
        Validate that prefixes is a non-None dictionary of string keys and values.

        Returns:
            The same dict for assignment to self.prefixes.

        Raises:
            ValueError: If prefixes is None (no input provided).
            TypeError: If prefixes is not a dict.
        """
        if prefixes is None:
            raise ValueError("prefixes is required and cannot be None")
        if not isinstance(prefixes, dict):
            raise TypeError(
                "prefixes must be a dictionary; got: {}".format(type(prefixes).__name__)
            )
        return prefixes

    def _format_pv_names(self) -> None:
        """Resolve {key} placeholders in PV names with self.prefixes; set _pv_attr_names."""
        self._validate_and_apply_prefix_templates()
        self._build_pv_attr_names()

    def _format_for_log(self) -> str:
        """Return a full, human-readable tree of the interface (prefixes, PVs, fields and types) for logging."""
        pv_list = list(self._iter_pv_attributes())
        lines = [
            f"{type(self).__name__} initialized",
            "prefixes: "
            + ", ".join(f"{k}={v!r}" for k, v in sorted(self.prefixes.items())),
            "PVs",
        ]
        for i, (attr_name, pv) in enumerate(pv_list):
            is_last_pv = i == len(pv_list) - 1
            branch = "└── " if is_last_pv else "├── "
            pv_type_str = (
                f" ({pv.cast_type.__name__})" if pv.cast_type is not None else ""
            )
            lines.append(f"  {branch}{attr_name}{pv_type_str} → {pv.name}")
            # Same width as "  │   " so branch symbols align for all PVs
            indent = "     " if is_last_pv else "│   "
            for j, field in enumerate(pv.fields):
                field_type_str = (
                    f" ({field.cast_type.__name__})"
                    if field.cast_type is not None
                    else ""
                )
                sub_branch = "└── " if j == len(pv.fields) - 1 else "├── "
                lines.append(
                    f"  {indent}{sub_branch}{field.name}{field_type_str} → {field.pv_name}"
                )
        return "\n".join(lines)

    def _iter_pv_attributes(self) -> Iterator[tuple[str, PV]]:
        """Yield (attribute_name, pv) for each PV attribute, skipping internals."""
        skip = ("prefixes", "_pv_attr_names")
        for attr_name, v in vars(self).items():
            if attr_name in skip or not isinstance(v, PV):
                continue
            yield attr_name, v

    def filter_pvs(self, tag: str) -> list[PV]:
        """
        Return top-level PVs that include ``tag``.

        Matching is against the PV's ``tags`` list (case-sensitive). PVs with
        ``tags is None`` are excluded.

        Args:
            tag: Tag string to match.

        Returns:
            List of PVs carrying the tag.
        """
        return [
            pv
            for _, pv in self._iter_pv_attributes()
            if pv.tags is not None and tag in pv.tags
        ]

    def _validate_and_apply_prefix_templates(self) -> None:
        """Resolve {key} in each PV name with self.prefixes; format field names. Raise if any key is missing."""
        placeholder_re = re.compile(r"\{(\w+)\}")
        missing_keys: list[str] = []
        for attr_name, pv in self._iter_pv_attributes():
            placeholders = set(placeholder_re.findall(pv.name))
            missing = placeholders - self.prefixes.keys()
            for key in sorted(missing):
                missing_keys.append(f"Field '{attr_name}' requires prefix key '{key}'")
            if not missing:
                pv.name = pv.name.format(**self.prefixes)
            for field in pv.fields:
                field.pv_name = f"{pv.name}.{field.name}"
        if missing_keys:
            raise ValueError(
                "Configuration Error: Missing prefixes for PV templates.\n"
                + "\n".join(missing_keys)
            )

    def _build_pv_attr_names(self) -> None:
        """Populate _pv_attr_names with top-level PV keys and dotted field keys (excluding synthetic val)."""
        self._pv_attr_names = []
        for attr_name, pv in self._iter_pv_attributes():
            self._pv_attr_names.append(attr_name)
            for field in pv.fields:
                if field.name.lower() != "val":
                    self._pv_attr_names.append(f"{attr_name}.{field.name}")

    def _resolve_pv(self, attr_path: str) -> PV | PVField:
        """
        Resolve dotted attribute path to the PV or PVField instance.

        Args:
            attr_path: Either a top-level attribute name (e.g. 'trigger') or a
                dotted path (e.g. 'waveform.NELM') for a field.

        Returns:
            The PV or PVField instance for that path.

        Raises:
            AttributeError: If the path does not exist.
        """
        if "." in attr_path:
            parent_attr, field_key = attr_path.split(".", 1)
            try:
                parent = getattr(self, parent_attr)
            except AttributeError as e:
                raise AttributeError(f"No attribute '{parent_attr}'") from e
            for f in parent.fields:
                if f.name == field_key:
                    return f
            raise AttributeError(f"No field '{field_key}' on '{parent_attr}'")
        return getattr(self, attr_path)

    @functools.cached_property
    def as_dict(self) -> dict[str, Any]:
        """
        Returns a dictionary of attribute name -> value.
        For PVs the value is the PV's .name (string). Dotted keys (e.g. waveform.NELM)
        are included for field sub-PVs; the synthetic 'val' field is excluded.
        Non-PV attributes use their raw value. Excludes 'prefixes' and '_pv_attr_names'.
        """
        data = {
            k: v
            for k, v in vars(self).items()
            if k not in ("prefixes", "_pv_attr_names")
        }
        for k, v in list(data.items()):
            if isinstance(v, PV):
                data[k] = v.name
                for field in v.fields:
                    if field.name.lower() != "val":
                        data[f"{k}.{field.name}"] = field.pv_name
        return data

    @functools.cached_property
    def pv_to_attr(self) -> dict[str, str]:
        """
        Returns a reverse mapping: {formatted_pv_name: attribute_name}.
        Example: {'VAC:01:Trigger': 'trigger', 'VAC:01:Waveform.NELM': 'waveform.NELM'}
        Only includes PV fields (keyed by PV name), including field sub-PVs.
        """
        result: dict[str, str] = {}
        for name in self._pv_attr_names:
            obj = self._resolve_pv(name)
            key = obj.pv_name if isinstance(obj, PVField) else obj.name
            result[key] = name
        return result

    def __repr__(self) -> str:
        pv_count = len(self._pv_attr_names)
        sample = ", ".join(
            f"{k}={self._resolve_pv(k).name!r}" for k in self._pv_attr_names[:3]
        )
        suffix = "..." if pv_count > 3 else ""
        return (
            f"{type(self).__name__}(prefixes={self.prefixes!r}, pvs=[{sample}{suffix}])"
        )

    def __iter__(self) -> Iterator[tuple[str, PV | PVField]]:
        for name in self._pv_attr_names:
            yield name, self._resolve_pv(name)

    def __contains__(self, item: object) -> bool:
        return item in self._pv_attr_names

    def __len__(self) -> int:
        return len(self._pv_attr_names)
