from __future__ import annotations

from enum import Enum
from typing import Any, Optional, Set, Type, TypeVar, cast

from ..common.sync_node import SyncNode
from ..common.types import BindingStatus, SyncAction, SyncStatus
from .dispatcher import StateMachineRuntime

E = TypeVar("E", bound=Enum)


def _state_def(runtime: StateMachineRuntime, state_id: str) -> dict[str, Any]:
    states = runtime._config.raw.get("states")
    if not isinstance(states, dict):
        raise RuntimeError("invalid state-machine config: missing states")
    state_def = states.get(state_id)
    if not isinstance(state_def, dict):
        raise RuntimeError(f"unknown state in config: {state_id}")
    return state_def


def _to_enum(enum_cls: Type[E], raw_value: Any, field_name: str, state_id: str) -> E:
    if not isinstance(raw_value, str):
        raise RuntimeError(
            f"invalid {field_name} for {state_id}: expected str/list[str], got {type(raw_value).__name__}"
        )
    try:
        return cast(E, enum_cls(raw_value))
    except Exception:
        raw_lower = str(raw_value).lower()
        for member in enum_cls:
            if str(member.value).lower() == raw_lower or str(member.name).lower() == raw_lower:
                return cast(E, member)
        raise RuntimeError(f"invalid {field_name} value for {state_id}: {raw_value}")


def _resolve_enum_value(
    *,
    state_id: str,
    field_name: str,
    raw_value: Any,
    enum_cls: Type[E],
    current_value: E,
) -> E:
    if isinstance(raw_value, str):
        return _to_enum(enum_cls, raw_value, field_name, state_id)

    if isinstance(raw_value, list):
        candidates = [_to_enum(enum_cls, v, field_name, state_id) for v in raw_value]
        if not candidates:
            raise RuntimeError(f"empty {field_name} candidates for {state_id}")
        if current_value in candidates:
            return current_value
        if len(candidates) == 1:
            return candidates[0]
        raise RuntimeError(
            f"ambiguous {field_name} for {state_id}: candidates={[c.value for c in candidates]}, "
            f"current={current_value.value}"
        )

    raise RuntimeError(
        f"invalid {field_name} for {state_id}: expected str/list[str], got {type(raw_value).__name__}"
    )


def apply_state_to_node(
    runtime: StateMachineRuntime,
    node: SyncNode,
    *,
    state_id: Optional[str],
    reason: str = "",
    fields: Optional[Set[str]] = None,
    data_id: Optional[str] = None,
) -> bool:
    if not state_id:
        return False

    state_def = _state_def(runtime, state_id)
    selected_fields = fields or {"binding_status", "action", "status"}

    with node.allow_core_state_write(source="state_machine"):
        if "binding_status" in selected_fields and "binding_status" in state_def:
            target_binding = _resolve_enum_value(
                state_id=state_id,
                field_name="binding_status",
                raw_value=state_def["binding_status"],
                enum_cls=BindingStatus,
                current_value=node.binding_status,
            )
            node.set_binding_status(target_binding, reason)

        if "action" in selected_fields and "action" in state_def:
            target_action = _resolve_enum_value(
                state_id=state_id,
                field_name="action",
                raw_value=state_def["action"],
                enum_cls=SyncAction,
                current_value=node.action,
            )
            node.set_action(target_action, reason)

        if "status" in selected_fields and "status" in state_def:
            target_status = _resolve_enum_value(
                state_id=state_id,
                field_name="status",
                raw_value=state_def["status"],
                enum_cls=SyncStatus,
                current_value=node.status,
            )
            node.set_status(target_status, reason)

        if "data_id" in selected_fields:
            if data_id is None:
                raise RuntimeError(f"state {state_id} requested data_id update but no data_id provided")
            node.set_data_id(data_id, reason)

    return True
