"""
Base configuration classes without dependency on generated code.

This module contains the base Config implementation that can be combined
with generated ConfigAccessor via multiple inheritance.
"""

from __future__ import annotations

import abc
import inspect
import textwrap
from dataclasses import dataclass
from typing import Optional, Any, Union, Tuple, Iterator, Literal, Type, Protocol, List, Dict, ClassVar

from .generator_interface import ConfigBase, ConfigValue, FrameProcessor
from ._public.err import Sys_ErrCfgNotFound, Sys_ErrCfgAccessDenied
from ._public import PublicCommands

# Configuration reset wildcard value (0xFF indicates "all" in mk/sk/vk positions)
_RESET_WILDCARD = 0xFF


def _format_value(value: Any) -> str:
    if isinstance(value, (bytes, bytearray)):
        return f'bytes.fromhex("{value.hex(" ").upper()}")'
    return repr(value)


class ConfDict(Protocol):
    """
    Protocol for configuration dictionary interface.

    Allows treating configuration as a dictionary with (mk_sk, vk) keys
    and optional bytes values (None = deleted).
    """

    def __getitem__(self, item: Tuple[int, Optional[int]]) -> Optional[bytes]:
        """Get configuration value."""
        ...

    def __setitem__(self, key: Tuple[int, Optional[int]], content: Optional[bytes]) -> None:
        """Set or delete configuration value (None = delete)."""
        ...

    def __iter__(self) -> Iterator[Tuple[int, Optional[int]]]:
        """Iterate over all configuration keys."""
        ...


class _CommandsConfDictWrapper(ConfDict):
    """
    Wraps Commands object as ConfDict for Config class.

    Implements the ConfDict protocol by delegating to Sys_CfgGetValue,
    Sys_CfgSetValue, and Sys_CfgDelValues commands.
    """

    def __init__(self, cmds: Any) -> None:
        """
        Initialize wrapper.

        Args:
            cmds: Commands instance (typed as Any to avoid circular import)
        """
        self.cmds = cmds

    def __getitem__(self, item: Tuple[int, Optional[int]]) -> Optional[bytes]:
        """Get configuration value from reader."""
        key, value = item
        if value is None:
            raise KeyError(item)
        try:
            result = self.cmds.Sys_CfgGetValue(Key=key, Value=value)
            return result if isinstance(result, bytes) else None
        except Sys_ErrCfgNotFound:
            raise KeyError(item)

    def __setitem__(self, item: Tuple[int, Optional[int]], content: Optional[bytes]) -> None:
        """Set or delete configuration value on reader."""
        key, value = item

        if content is None:
            # Delete operation: content=None triggers deletion
            try:
                self.cmds.Sys_CfgDelValues(Key=key, Value=value if value is not None else _RESET_WILDCARD)
            except Sys_ErrCfgNotFound:
                pass
        else:
            # Set operation: content is not None
            # Constraint: when vk is None, data must be None (so vk=None with data is invalid)
            if value is None:
                raise ValueError(f"When ValueKey is None, content must be None. Got content={content!r}")
            try:
                self.cmds.Sys_CfgSetValue(Key=key, Value=value, Content=content)
            except Sys_ErrCfgNotFound:
                raise KeyError(item)

    def __iter__(self) -> Iterator[Tuple[int, Optional[int]]]:
        """Iterate over all configuration keys on reader."""
        yield from (
            (key, value)
            for key in self.cmds.Sys_CfgGetKeyList()
            for value in self.cmds.Sys_CfgGetValueList(Key=key)
        )


AsCodeMode = Literal["strict", "accept-unknown", "ignore-access-denied"]


class BaseConfig:
    """
    Base configuration class with as_code() support.

    This class provides the core configuration functionality and is meant to
    be combined with a generated ConfigAccessor class via multiple inheritance.

    Example:
        class Config(_BaseConfig, ConfigAccessor):
            pass
    """
    ValueRegistry: ClassVar[RegistryType] = {}

    def __init__(self, cfg_src: Union[ConfDict, Any]) -> None:
        """
        Initialize Config wrapper.

        Args:
            cfg_src: Either a ConfDict implementation or a Commands object
                    (Commands will be wrapped in CommandsConfDictWrapper)
        """
        self.confdict = (
            _CommandsConfDictWrapper(cfg_src)
            if isinstance(cfg_src, PublicCommands)
            else cfg_src
        )

    def process_frame(self, frame: bytes) -> bytes:
        """
        Process configuration frame through confdict.

        Args:
            frame: Configuration frame (mode + mk + sk + vk + content)

        Returns:
            Response bytes (for get operations)

        Raises:
            KeyError: If configuration key doesn't exist
            ValueError: For invalid mode or invalid operations
        """
        mode = frame[0]
        masterkey = frame[1]
        subkey = frame[2] if len(frame) > 2 else _RESET_WILDCARD
        value = frame[3] if len(frame) > 3 else None
        content = frame[4:] if len(frame) > 4 else b""
        key = (masterkey << 8) | subkey

        if mode == 0:  # get
            result = self.confdict[(key, value)]
            if result is None:
                raise KeyError(f"(0x{key:04x}, {value}) already deleted")
            return result

        if mode == 1:  # set
            self.confdict[(key, value)] = content
            return b""

        if mode == 2:  # delete
            self.confdict[(key, value)] = None
            return b""

        raise ValueError(f"unknown mode '{mode}' in configuration frame {frame!r}")

    def reset(self) -> None:
        self.confdict[(0xFFFF, 0xFF)] = None

    def as_code(
        self,
        *,
        config_obj_name: Optional[str] = "cfg",
        indentation: Union[str, int, None] = None,
        positional_args: Optional[bool] = None,
        show_defaults: bool = False,
        mode: AsCodeMode = "strict",
        format: Optional[None] = None,
    ) -> str:
        """
        Convert configuration to Python SDK code.

        Generates Python code that recreates this configuration state using
        the SDK's configuration API.

        Args:
            config_obj_name: Prefix for generated code (e.g., "cfg." prefix).
                                    When None, no prefix is added. Default: "cfg"
            indentation: Indentation prefix for generated code.
                         str: use as prefix directly, int: use " "*indentation,
                         None: no indentation (same as ""). Default: None
            positional_args: Argument style for generated code.
                             True: only positional arguments,
                             False: only keyword arguments,
                             None: positional if single arg, else keyword. Default: None
            show_defaults: Include parameters with default values in the generated code.
                           When False, content fields with default values are hidden.
                           Index parameters (*_ndx) are always shown. Ignored when using positional args.
                           Default: False
            mode: Behavior when encountering unknown keys or parsing errors.
                  "strict": Raise KeyError/ValueError (default)
                  "accept-unknown": Fallback to Sys_CfgSetValue/Sys_CfgDelValues for unknown keys

        Yields:
            String representation of each Python SDK call or reset operation

        Notes:
            Items are sorted by name (mk_sk, vk) with special rules for resets:
            - Masterkey resets (sk=0xFF, vk=0xFF) come before all entries with that mk
            - Subkey resets (vk=0xFF) come before all entries with that mk_sk
            - Valuekey resets (value=None) come before that specific value key
        """
        indent = ""
        if isinstance(indentation, int):
            indent += " " * indentation
        elif indentation is not None:
            indent += indentation

        def black(code: str) -> str:
            import black
            return black.format_str(
                code,
                mode=black.Mode(
                    magic_trailing_comma=True,
                    target_versions={black.mode.TargetVersion.PY39}
                )
            )

        def fmt(code: str) -> str:
            if format:
                return black(code)
            if format is False:
                return code

            try:
                return black(code)
            except ModuleNotFoundError:
                return code

        if not self.ValueRegistry:
            BaseConfig.ValueRegistry = _build_registry(ConfigBase)

        return textwrap.indent(fmt("\n".join(
            mod.format(
                config_obj_name=config_obj_name,
                positional_args=positional_args,
                show_defaults=show_defaults,
            )
            for mod in sorted(
                (
                    _config_modification(
                        registry=self.ValueRegistry,
                        confdict=self.confdict,
                        mk_sk=mk_sk,
                        maybe_vk=vk,
                        mode=mode
                    )
                    for (mk_sk, vk) in self.confdict
                ),
                key=lambda mod: mod.sort_indizes()
            )
        )), prefix=indent)


@dataclass
class _ConfigModification(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def sort_indizes(self) -> tuple[object, ...]: ...

    @abc.abstractmethod
    def _format(
        self,
        positional_args: Optional[bool] = None,
        show_defaults: bool = False,
    ) -> str: ...

    def format(
        self,
        config_obj_name: Optional[str] = "cfg",
        positional_args: Optional[bool] = None,
        show_defaults: bool = False,
    ) -> str:
        prefix = ""
        if config_obj_name is not None:
            prefix = f"{config_obj_name}."

        return f"{prefix}{self._format(positional_args=positional_args, show_defaults=show_defaults)}"


@dataclass
class _ConfigReset(_ConfigModification):
    config: Optional[Type[ConfigBase]]
    mk_idx: Optional[int] = None
    sk_idx: Optional[int] = None
    vk_idx: Optional[int] = None
    mk_name: str = ""
    sk_name: str = ""

    def sort_indizes(self) -> tuple[object, ...]:
        if self.config is None:
            return (0, "", 0, "", 0, "", 0, 0)
        if self.sk_idx is None:
            return (0, self.config.__name__, self.mk_idx or 0, "", 0, "", 0, 0)
        if self.vk_idx is None:
            return (0, self.mk_name, self.mk_idx or 0, self.config.__name__, self.sk_idx or 0, "", 0, 0)
        return (0, self.mk_name, self.mk_idx or 0, self.sk_name, self.sk_idx or 0, self.config.__name__, self.vk_idx or 0, 0)

    def _format(
        self,
        positional_args: Optional[bool] = None,
        show_defaults: bool = False,
    ) -> str:
        if self.config is None:
            return "reset()"

        params = ", ".join(
            f'{n}={v!r}'
            for n, v in zip(
                _index_params(self.config),
                _active_index_values(self.config, self.mk_idx, self.sk_idx, self.vk_idx)
            )
        )
        return f"{self.config.__name__}.reset({params})"


@dataclass
class _ConfigSet(_ConfigModification):
    config: Type[ConfigBase]
    result: Any
    mk_idx: Optional[int] = None
    sk_idx: Optional[int] = None
    vk_idx: Optional[int] = None
    mk_name: str = ""
    sk_name: str = ""

    def sort_indizes(self) -> tuple[object, ...]:
        return (0, self.mk_name, self.mk_idx or 0, self.sk_name, self.sk_idx or 0, self.config.__name__, self.vk_idx or 0, 1)

    def _format(
        self,
        positional_args: Optional[bool] = None,
        show_defaults: bool = False,
    ) -> str:
        params_dict = {
            name: index
            for name, index in zip(
                _index_params(self.config),
                _active_index_values(self.config, self.mk_idx, self.sk_idx, self.vk_idx)
            )
        }

        call_params = _call_params(self.config)
        if len(call_params) == 1:
            params_dict[call_params[0]] = self.result
        else:
            for field_name, field_value in zip(call_params, self.result):
                params_dict[field_name] = field_value

        # Determine argument style
        use_positional = len(params_dict) == 1 if positional_args is None else positional_args

        # Filter out default values if requested (only for keyword args)
        if not use_positional and not show_defaults:
            defaults = _defaults(self.config)
            params_dict = {
                param: value
                for param, value in params_dict.items()
                if not (param in defaults and defaults[param] == value)
            }

        # Format parameters
        if use_positional:
            params = [_format_value(v) for v in params_dict.values()]
        else:
            params = [f'{n}={_format_value(v)}' for n, v in params_dict.items()]

        return f"{self.config.__name__}({', '.join(params)})"


@dataclass
class _ConfigLegacyModification(_ConfigModification):
    key: int
    value: int
    payload: Optional[bytes]
    comment: str

    def sort_indizes(self) -> Tuple[int, int, int]:
        if self.payload is None:
            return 1, 0, 0
        return 1, self.key, self.value

    def _format(
        self,
        positional_args: Optional[bool] = None,
        show_defaults: bool = False,
    ) -> str:
        return f"confdict[(0x{self.key:04X}, 0x{self.value:02X})] = {_format_value(self.payload)}  # {self.comment}"


@dataclass
class _ConfigAccessDenied(_ConfigModification):
    key: int
    value: int
    cls: Optional[Type[ConfigBase]]

    def sort_indizes(self) -> Tuple[int, int, str, int, int]:
        return (
            2,
            1 if self.cls is None or not issubclass(self.cls, ConfigValue) else 0,
            self.cls.__name__ if self.cls else chr(127),
            self.key, self.value
        )

    def _format(
        self,
        positional_args: Optional[bool] = None,
        show_defaults: bool = False,
    ) -> str:
        raise NotImplementedError()

    def format(
        self,
        config_obj_name: Optional[str] = "cfg",
        positional_args: Optional[bool] = None,
        show_defaults: bool = False,
    ) -> str:
        if self.cls is None:
            return f"# Access denied to Key=0x{self.key:04X} Value=0x{self.value:02X}"
        if issubclass(self.cls, ConfigValue):
            return f"# Access denied to {self.cls.__name__}"
        return f"# Access denied to Key=0x{self.key:04X} Value=0x{self.value:02X} ({self.cls.__name__})"


class _ConfigReprDummyProcessor(FrameProcessor):
    """
    Dummy frame processor for _config_modification function.

    This processor is used internally by _config_modification to instantiate
    configuration classes for introspection without actually communicating
    with a reader.
    """

    def process_frame(self, frame: bytes) -> bytes:
        """Return empty response."""
        return b""

RegistryType = Dict[Tuple[int, int, int], Type['ConfigBase']]

def _build_registry(cls: Type[ConfigBase], *, initial: Optional[RegistryType] = None) -> RegistryType:
    """
    Auto-register ConfigBase subclasses on class definition.

    This metaclass hook registers all (mk, sk, vk) combinations in the
    class registry.
    """
    registry: RegistryType = {} if initial is None else initial
    if hasattr(cls, "MasterKey"):
        master_keys = [cls.MasterKey + idx for idx in range(cls.MasterKeyCount)]
        sub_keys = [cls.SubKey + idx for idx in range(cls.SubKeyCount)] if cls.SubKey is not None else []
        value_keys = [cls.ValueKey + idx for idx in range(cls.ValueKeyCount)] if cls.ValueKey is not None else []
        # Register all combinations of MK+ndx, SK+ndx, VK+ndx
        for mk in master_keys:
            if not sub_keys:
                registry[(mk, _RESET_WILDCARD, _RESET_WILDCARD)] = cls
            for sk in sub_keys:
                if not value_keys:
                    registry[(mk, sk, _RESET_WILDCARD)] = cls
                    if getattr(cls, "HasProtectedValues", False):
                        registry[(mk, sk | 0x80, _RESET_WILDCARD)] = cls
                for vk in value_keys:
                    registry[(mk, sk, vk)] = cls
                    
    for subclass in cls.__subclasses__():
        _build_registry(subclass, initial=registry)

    return registry


def _index_params(cls: Type[ConfigBase]) -> List[str]:
    sig = inspect.signature(cls.__call__)
    return [
        name
        for name in sig.parameters.keys()
        if name.endswith('_ndx')
    ]


def _call_params(cls: Type[ConfigBase]) -> List[str]:
    sig = inspect.signature(cls.__call__)
    return [
        name
        for name in sig.parameters.keys()
        if name != 'self' and not name.endswith('_ndx')
    ]


def _defaults(cls: Type[ConfigBase]) -> Dict[str, Any]:
    sig = inspect.signature(cls.__call__)
    return {
        name: param.default
        for name, param in sig.parameters.items()
        if param.default is not inspect.Parameter.empty
    }


def _active_index_values(cls: Type[ConfigBase], mk_idx: Optional[int], sk_idx: Optional[int], vk_idx: Optional[int]) -> List[Optional[int]]:
    values = []
    if cls.MasterKeyCount > 1:
        values.append(mk_idx)
    if cls.SubKeyCount > 1:
        values.append(sk_idx)
    if cls.ValueKeyCount > 1:
        values.append(vk_idx)
    return values


def _config_modification(
    registry: RegistryType,
    confdict: ConfDict,
    mk_sk: int,
    maybe_vk: Optional[int],
    *,
    mode: AsCodeMode = "strict"
) -> _ConfigModification:
    """Convert a configuration dictionary entry to a Python SDK config modification.

    Args:
        confdict: ConfDict to get the payload from
        mk_sk: 16-bit combined master key and sub key (mk << 8 | sk)
        vk: Value key (8-bit), 0xFF as wildcard for resets
        mode: Behavior when encountering unknown keys or parsing errors.
              "strict": Raise KeyError/ValueError (default)
              "accept-unknown": Fallback to Sys_CfgSetValue/Sys_CfgDelValues for unknown keys

    Returns:
        ConfigModification of the Python SDK call or reset operation

    Raises:
        KeyError: If no matching ConfigValue or ConfigBase class is found (strict mode only)
        ValueError: If payload cannot be parsed (strict mode only)
    """
    # Split combined mk_sk into separate bytes
    mk = (mk_sk >> 8) & 0xFF
    sk = mk_sk & 0xFF
    vk = _RESET_WILDCARD if maybe_vk is None else maybe_vk

    known_cls = registry.get((mk, sk, vk), None) \
                or registry.get((mk, sk, _RESET_WILDCARD), None) \
                or registry.get((mk, _RESET_WILDCARD, _RESET_WILDCARD), None)

    try:
        payload = confdict[(mk_sk, maybe_vk)]
    except Sys_ErrCfgAccessDenied:
        if mode == "ignore-access-denied":
            return _ConfigAccessDenied(key=mk_sk, value=vk, cls=known_cls)
        raise

    is_reset = (mk == _RESET_WILDCARD) or (sk == _RESET_WILDCARD) or (vk == _RESET_WILDCARD) or (payload is None)
    if is_reset and payload is not None:
        raise TypeError("payload not allowed on configuration reset")

    if mk == _RESET_WILDCARD:
        return _ConfigReset(config=None)

    try:
        cls = registry[(mk, sk, vk)]
    except KeyError:
        if mode == "strict":
            raise KeyError(
                f"No config value defined for Key=0x{mk_sk:04X} Value=0x{vk:02X}"
            )
        return _ConfigLegacyModification(
            key=mk_sk, value=vk, payload=payload,
            comment=f"unknown configuration value in {known_cls.__name__}" if known_cls else "unknown configuration value"
        )

    mk_idx = mk - cls.MasterKey
    sk_idx = sk - cls.SubKey if cls.SubKey is not None else None
    vk_idx = vk - cls.ValueKey if cls.ValueKey is not None else None

    mk_key_cls = registry.get((mk, _RESET_WILDCARD, _RESET_WILDCARD))
    sk_key_cls = registry.get((mk, sk, _RESET_WILDCARD)) if sk != _RESET_WILDCARD else None
    mk_name = mk_key_cls.__name__ if mk_key_cls else ""
    sk_name = sk_key_cls.__name__ if sk_key_cls else ""

    if is_reset:
        return _ConfigReset(config=cls, mk_idx=mk_idx, sk_idx=sk_idx, vk_idx=vk_idx, mk_name=mk_name, sk_name=sk_name)

    # at this point cls must be a ConfigValue
    assert issubclass(cls, ConfigValue)
    instance: ConfigBase = cls(_ConfigReprDummyProcessor())
    assert isinstance(instance, ConfigValue)
    assert payload is not None

    try:
        result = instance.parse_frame(payload)
    except Exception as e:
        if mode == "strict":
            raise
        return _ConfigLegacyModification(key=mk_sk, value=vk, payload=payload, comment=f"{cls.__name__} parse error: {e}")

    return _ConfigSet(config=cls, mk_idx=mk_idx, sk_idx=sk_idx, vk_idx=vk_idx, mk_name=mk_name, sk_name=sk_name, result=result)
