from __future__ import annotations

from enum import Enum
from typing import Any, Mapping, Optional, Sequence, TYPE_CHECKING, Set

from .dispatcher import Decision, StateMachineRuntime
from .semantics import classify_core_pair
from .state_apply import apply_state_to_node

if TYPE_CHECKING:
    from ..common.sync_node import SyncNode


class CoreBindingResult(str, Enum):
    NORMAL = "NORMAL"
    WARNING = "WARNING"
    ABNORMAL = "ABNORMAL"


class AutoBindOutcome(str, Enum):
    ONE_TO_ONE = "ONE_TO_ONE"
    AMBIGUOUS = "AMBIGUOUS"
    ZERO = "ZERO"
    KEY_MISSING = "KEY_MISSING"
    KEY_ID_RESOLVE_FAIL = "KEY_ID_RESOLVE_FAIL"
    DATA_MISSING = "DATA_MISSING"


class ExecuteAction(str, Enum):
    CREATE = "CREATE"
    UPDATE = "UPDATE"
    DELETE = "DELETE"


class HandlerResult(str, Enum):
    SUCCESS = "SUCCESS"
    FAILED = "FAILED"
    SKIPPED = "SKIPPED"
    IN_PROGRESS = "IN_PROGRESS"


def _as_upper_text(value: Any, *, field_name: str) -> str:
    if isinstance(value, Enum):
        raw = value.value
    else:
        raw = value
    if not isinstance(raw, str):
        raise RuntimeError(f"{field_name} must be str/Enum, got {type(raw).__name__}")
    return raw.upper()


def decision_note(decision: Decision) -> str:
    if not decision.emit:
        return ""
    note = decision.emit.get("note")
    return note if isinstance(note, str) else ""


def _apply_decision(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    decision: Decision,
    fields: Optional[Set[str]] = None,
    reason_override: Optional[str] = None,
) -> str:
    reason = reason_override if reason_override is not None else decision_note(decision)
    if decision.to_state:
        reason_text = reason or "<no-reason>"
        node.append_log(
            f"状态机流转: {decision.from_state} --{decision.event_id}[{decision.outcome_index}]--> {decision.to_state} | reason={reason_text}"
        )
    apply_state_to_node(runtime, node, state_id=decision.to_state, reason=reason, fields=fields)

    if decision.to_state:
        snapshot = {
            "binding_status": node.binding_status.value,
            "action": node.action.value,
            "status": node.status.value,
            "data_id_exist": bool(node.data_id),
        }
        violations = runtime.check_invariants(snapshot)
        if violations:
            details = "; ".join(f"{v.invariant_id}: {v.message}" for v in violations)
            raise RuntimeError(f"Invariant violation after {decision.event_id}->{decision.to_state}: {details}")

    return reason


def _format_dependency_reason(
    *,
    base_reason: str,
    dep_errors: Optional[list[str]] = None,
    semantic_errors: Optional[list[str]] = None,
) -> str:
    details: list[str] = []
    for dep_err in dep_errors or []:
        parts = dep_err.split("|")
        tag = parts[0] if parts else "unknown"
        if tag == "not_found" and len(parts) >= 4:
            details.append(f"{parts[1]}:{parts[2]} data_id={parts[3]} -> not_found")
        elif tag == "bad_status" and len(parts) >= 3:
            details.append(f"{parts[1]} status={parts[2]} -> not_normal")
        elif tag == "no_data_id" and len(parts) >= 3:
            details.append(f"{parts[1]} {parts[2]} -> no_data_id")
        else:
            details.append(dep_err)

    if semantic_errors:
        details.extend([e for e in semantic_errors if e])

    if not details:
        return base_reason
    return f"{base_reason} | depends=[{'; '.join(details)}]"


def _expected_matches_actual(expected: Any, actual: Any) -> bool:
    if isinstance(expected, list):
        return any(_expected_matches_actual(x, actual) for x in expected)

    if isinstance(expected, str):
        actual_candidates = set()
        if isinstance(actual, Enum):
            actual_candidates.update({str(actual.name), str(actual.value)})
        else:
            actual_candidates.add(str(actual))
        expected_lower = expected.lower()
        return any(str(c).lower() == expected_lower for c in actual_candidates)

    return expected == actual


def _state_matches_node(
    runtime: StateMachineRuntime,
    node: "SyncNode",
    state_id: str,
    *,
    action_hint: Optional[str] = None,
    match_fields: Optional[Set[str]] = None,
) -> bool:
    states = runtime._config.raw.get("states")
    pseudo_states = runtime._config.raw.get("pseudo_states")
    if not isinstance(states, dict):
        raise RuntimeError("invalid state-machine config: missing states")
    if pseudo_states is not None and not isinstance(pseudo_states, dict):
        raise RuntimeError("invalid state-machine config: pseudo_states must be mapping")
    state_def = states.get(state_id)
    if state_def is None and isinstance(pseudo_states, dict):
        state_def = pseudo_states.get(state_id)
    if not isinstance(state_def, dict):
        return False

    snapshot = {
        "binding_status": node.binding_status,
        "action": action_hint if action_hint is not None else node.action,
        "status": node.status,
        "data_id_exist": bool(node.data_id),
    }

    selected_fields = match_fields or set(snapshot.keys())

    for k, actual in snapshot.items():
        if k not in selected_fields:
            continue
        if k not in state_def:
            continue
        expected = state_def[k]
        if not _expected_matches_actual(expected, actual):
            return False
    return True


def _resolve_current_state(
    runtime: StateMachineRuntime,
    node: "SyncNode",
    *,
    candidate_states: Sequence[str],
    action_hint: Optional[str] = None,
    match_fields: Optional[Set[str]] = None,
) -> str:
    matches = [
        s
        for s in candidate_states
        if _state_matches_node(runtime, node, s, action_hint=action_hint, match_fields=match_fields)
    ]
    if len(matches) == 1:
        return matches[0]
    if not matches:
        raise RuntimeError(
            f"cannot resolve current state for node={node.node_id}, candidates={list(candidate_states)}"
        )
    raise RuntimeError(
        f"ambiguous current state for node={node.node_id}, candidates={list(candidate_states)}, matches={matches}"
    )


def _config_from_states(runtime: StateMachineRuntime, event_id: str) -> list[str]:
    transition = runtime._config.transitions.get(event_id)
    if transition is None:
        raise RuntimeError(f"unknown event: {event_id}")
    from_states = [s for s in transition.from_states if s != "*"]
    if not from_states:
        raise RuntimeError(f"event {event_id} has no concrete from states")
    return from_states


def _assert_expected_from_states(
    *,
    event_id: str,
    config_from_states: Sequence[str],
    expected_from_states: Optional[Sequence[str]],
) -> None:
    if expected_from_states is None:
        return
    config_set = set(config_from_states)
    expected_set = set(expected_from_states)
    if config_set != expected_set:
        raise RuntimeError(
            f"event {event_id} from-states mismatch with config: "
            f"config={sorted(config_set)}, expected={sorted(expected_set)}"
        )


def _dispatch_on_resolved_state(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    current_state: str,
    event_id: str,
    context: Mapping[str, Any],
) -> Decision:
    decision = runtime.dispatch(current_state=current_state, event_id=event_id, context=context)
    if decision is None:
        raise RuntimeError(
            f"state-machine miss: node={node.node_id}, state={current_state}, event={event_id}, context={dict(context)}"
        )
    return decision


def _dispatch_required(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    event_id: str,
    context: Mapping[str, Any],
    action_hint: Optional[str] = None,
    match_fields: Optional[Set[str]] = None,
    expected_from_states: Optional[Sequence[str]] = None,
) -> Decision:
    from_states = _config_from_states(runtime, event_id)
    _assert_expected_from_states(
        event_id=event_id,
        config_from_states=from_states,
        expected_from_states=expected_from_states,
    )

    current_state = _resolve_current_state(
        runtime,
        node,
        candidate_states=from_states,
        action_hint=action_hint,
        match_fields=match_fields,
    )

    return _dispatch_on_resolved_state(
        runtime,
        node=node,
        current_state=current_state,
        event_id=event_id,
        context=context,
    )


def e01_bootstrap(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    is_create_zombie: bool,
) -> Decision:
    context = {"is_create_zombie": is_create_zombie}
    decision = _dispatch_required(
        runtime,
        node=node,
        event_id="E01",
        context=context,
        expected_from_states=["S16"],
    )
    if decision.to_state == "S00":
        _apply_decision(runtime, node=node, decision=decision, fields={"binding_status", "action", "status"})
        node.error = None
        return decision
    if decision.to_state == "S15":
        reason_text = decision_note(decision) or "<no-reason>"
        node.append_log(
            f"状态机流转: {decision.from_state} --{decision.event_id}[{decision.outcome_index}]--> {decision.to_state} | reason={reason_text}"
        )
        return decision
    raise RuntimeError(f"unexpected bootstrap state: {decision.to_state}")


def e10_bind_core(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    has_binding_record: bool,
    core_binding_result: Optional[str] = None,
    peer_core_binding_result: Optional[str] = None,
    local_data: Optional[Any] = None,
    peer_data: Optional[Any] = None,
    local_valid: bool = True,
    peer_valid: bool = True,
) -> Decision:
    if has_binding_record and (core_binding_result is None or peer_core_binding_result is None):
        local_result, peer_result = classify_core_pair(
            has_binding_record=has_binding_record,
            local_data=local_data,
            peer_data=peer_data,
            local_valid=local_valid,
            peer_valid=peer_valid,
        )
        core_binding_result = local_result.value
        peer_core_binding_result = peer_result.value

    normalized_core = _as_upper_text(core_binding_result, field_name="core_binding_result") if core_binding_result is not None else None
    normalized_peer_core = (
        _as_upper_text(peer_core_binding_result, field_name="peer_core_binding_result")
        if peer_core_binding_result is not None
        else None
    )

    context: dict[str, Any] = {
        "has_binding_record": has_binding_record,
    }
    if normalized_core is not None:
        context["core_binding_result"] = normalized_core
    if normalized_peer_core is not None:
        context["peer_core_binding_result"] = normalized_peer_core
    decision = _dispatch_required(
        runtime,
        node=node,
        event_id="E10",
        context=context,
        expected_from_states=["S00"],
    )
    _apply_decision(runtime, node=node, decision=decision, fields={"binding_status"})
    return decision


def e20_create_prepare(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    spawn_success: bool,
) -> Decision:
    if not spawn_success:
        node.append_log("状态机流转: S13 --E20[-1]--> <no-transition> | reason=创建副作用未完成")
        return Decision(
            event_id="E20",
            from_state="S13",
            outcome_index=-1,
            to_state=None,
            emit={"note": "创建副作用未完成"},
        )

    context = {
        "spawn_success": spawn_success,
    }
    decision = _dispatch_required(
        runtime,
        node=node,
        event_id="E20",
        context=context,
        expected_from_states=["S13"],
    )
    _apply_decision(runtime, node=node, decision=decision, fields={"binding_status", "action", "status"})
    return decision


def e21_create_prepare(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    create_enabled: bool,
    data_present: bool,
    create_id_resolve_ok: bool,
) -> Decision:
    context = {
        "create_enabled": create_enabled,
        "data_present": data_present,
        "create_id_resolve_ok": create_id_resolve_ok,
    }
    decision = _dispatch_required(
        runtime,
        node=node,
        event_id="E21",
        context=context,
        expected_from_states=["S02"],
    )
    _apply_decision(runtime, node=node, decision=decision, fields={"binding_status"})
    return decision


def e22_delete_prepare(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
) -> Decision:
    decision = _dispatch_required(
        runtime,
        node=node,
        event_id="E22",
        context={},
        expected_from_states=["S02"],
    )
    _apply_decision(runtime, node=node, decision=decision, fields={"binding_status", "action", "status"})
    return decision


def e30_update_prepare(
    runtime: StateMachineRuntime,
    *,
    source_node: "SyncNode",
    target_node: "SyncNode",
    update_enabled: bool,
    target_has_data_id: bool,
    update_id_resolve_ok: bool,
    has_diff: bool,
    id_resolve_detail: Optional[str] = None,
) -> Decision:
    context = {
        "update_enabled": update_enabled,
        "target_has_data_id": target_has_data_id,
        "update_id_resolve_ok": update_id_resolve_ok,
        "has_diff": has_diff,
    }
    decision = _dispatch_required(
        runtime,
        node=source_node,
        event_id="E30",
        context=context,
        expected_from_states=["S01"],
    )
    if decision.to_state not in {"S07", "S08"}:
        raise RuntimeError(f"unexpected update-prepare state: {decision.to_state}")
    reason_override: Optional[str] = None
    if decision.to_state == "S08":
        base_reason = decision_note(decision)
        details: list[str] = []
        if not target_has_data_id:
            details.append(f"target_data_id_missing=true")
            details.append(f"target_node_id={target_node.node_id}")
        if target_has_data_id and not update_id_resolve_ok:
            if id_resolve_detail:
                details.append(f"update_id_resolve_fail=true")
                details.append(f"update_id_resolve_detail={id_resolve_detail}")
            else:
                details.append("update_id_resolve_fail=true")
        if details:
            reason_override = f"{base_reason} | {'; '.join(details)}"
            target_node.error = reason_override

    _apply_decision(
        runtime,
        node=target_node,
        decision=decision,
        fields={"action", "status"},
        reason_override=reason_override,
    )
    return decision


def e41_post_create_ready(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    execute_action: str = "CREATE",
) -> Decision:
    execute_action_norm = _as_upper_text(execute_action, field_name="execute_action")
    context = {
        "execute_action": execute_action_norm,
    }
    decision = _dispatch_required(
        runtime,
        node=node,
        event_id="E41",
        context=context,
        expected_from_states=["S11C"],
    )
    _apply_decision(runtime, node=node, decision=decision, fields={"action", "status"})
    return decision


def e15_bind_dependency(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    data_present: bool,
    has_depend_fields: bool = True,
    dep_satisfied: bool = True,
    dep_errors: Optional[list[str]] = None,
    semantic_errors: Optional[list[str]] = None,
) -> Decision:
    if data_present and dep_satisfied:
        node.append_log("状态机流转: S02 --E15[-1]--> <no-transition> | reason=依赖满足")
        return Decision(
            event_id="E15",
            from_state="S02",
            outcome_index=-1,
            to_state=None,
            emit={"note": "依赖满足"},
        )

    context = {
        "data_present": data_present,
        "has_depend_fields": has_depend_fields,
        "dep_satisfied": dep_satisfied,
    }
    decision = _dispatch_required(
        runtime,
        node=node,
        event_id="E15",
        context=context,
        expected_from_states=["S02"],
    )
    reason = decision_note(decision)
    if decision.to_state in {"S03", "S04", "S05"}:
        reason = _format_dependency_reason(
            base_reason=reason,
            dep_errors=dep_errors,
            semantic_errors=semantic_errors,
        )
        node.error = reason

    _apply_decision(
        runtime,
        node=node,
        decision=decision,
        fields={"binding_status"},
        reason_override=reason,
    )
    return decision


def e16_bind_auto(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    auto_bind_enabled: bool,
    auto_bind_outcome: Optional[str] = None,
    create_enabled: Optional[bool] = None,
    id_resolve_detail: Optional[str] = None,
) -> Decision:
    auto_bind_outcome_norm = (
        _as_upper_text(auto_bind_outcome, field_name="auto_bind_outcome")
        if auto_bind_outcome is not None
        else None
    )

    context: dict[str, Any] = {"auto_bind_enabled": auto_bind_enabled}
    if auto_bind_outcome_norm is not None:
        context["auto_bind_outcome"] = auto_bind_outcome_norm
    if create_enabled is not None:
        context["create_enabled"] = create_enabled
    decision = _dispatch_required(
        runtime,
        node=node,
        event_id="E16",
        context=context,
        expected_from_states=["S02"],
    )
    reason_override: Optional[str] = None
    if auto_bind_outcome_norm == "KEY_ID_RESOLVE_FAIL" and id_resolve_detail:
        base_reason = decision_note(decision)
        reason_override = f"{base_reason} | key_id_resolve_fail=true; key_id_resolve_detail={id_resolve_detail}"
        node.error = reason_override

    _apply_decision(
        runtime,
        node=node,
        decision=decision,
        fields={"binding_status", "action", "status"},
        reason_override=reason_override,
    )
    return decision


def e40_sync_execute(
    runtime: StateMachineRuntime,
    *,
    node: "SyncNode",
    handler_result: str,
    action: str,
    poll_timeout: bool,
    result_data_id: Optional[str] = None,
) -> Optional[Decision]:
    action_norm = _as_upper_text(action, field_name="action")
    if action_norm == "CREATE":
        start_event_id = "E40C_START"
        start_states = ["S06"]
        result_event_id = "E40C"
        result_states = ["S10C"]
    elif action_norm == "UPDATE":
        start_event_id = "E40U_START"
        start_states = ["S07"]
        result_event_id = "E40U"
        result_states = ["S10U"]
    elif action_norm == "DELETE":
        start_event_id = "E40D_START"
        start_states = ["S09"]
        result_event_id = "E40D"
        result_states = ["S10D"]
    else:
        return None

    normalized_result = "FAILED" if poll_timeout else _as_upper_text(handler_result, field_name="handler_result")
    if normalized_result == "SKIPPED" and action_norm != "UPDATE":
        return None

    start_context = {
        "poll_timeout": poll_timeout,
        "handler_result": "IN_PROGRESS",
        "execute_action": action_norm,
    }
    result_context = {
        "poll_timeout": poll_timeout,
        "handler_result": normalized_result,
        "execute_action": action_norm,
    }

    decision: Optional[Decision] = None
    in_entry_state = any(_state_matches_node(runtime, node, s) for s in start_states)
    if in_entry_state:
        start_decision = _dispatch_required(
            runtime,
            node=node,
            event_id=start_event_id,
            context=start_context,
            expected_from_states=start_states,
        )
        _apply_decision(runtime, node=node, decision=start_decision, fields={"action", "status"})
        if normalized_result == "IN_PROGRESS":
            return start_decision

    if normalized_result == "IN_PROGRESS":
        return None

    if normalized_result not in {"SUCCESS", "FAILED", "SKIPPED"}:
        return None

    decision = _dispatch_required(
        runtime,
        node=node,
        event_id=result_event_id,
        context=result_context,
        action_hint="UPDATE" if action_norm == "UPDATE" else None,
        expected_from_states=result_states,
    )

    if decision is None:
        return None

    apply_fields: Set[str] = {"action", "status"}
    apply_data_id: Optional[str] = None
    apply_data_id_field = False
    if decision.to_state == "S11C":
        if action_norm == "CREATE" and not result_data_id:
            raise RuntimeError("E40 SUCCESS for CREATE requires non-empty result_data_id")
        if result_data_id:
            apply_data_id = result_data_id
            apply_data_id_field = True

    _apply_decision(runtime, node=node, decision=decision, fields=apply_fields)
    if apply_data_id_field:
        apply_state_to_node(runtime, node, state_id=decision.to_state, reason=decision_note(decision), fields={"data_id"}, data_id=apply_data_id)
    return decision
