from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, Callable

from flownet.v1.runtime.flowengine.cursor_models import (
    EventCursor,
    FlowProgramRef,
    FlowStackFrame,
)
from flownet.v1.runtime.flowengine.operator_runtime import (
    JoinReducerRuntime,
    MergeReducerRuntime,
    OperatorExecutionError,
    ReducerRuntime,
    StatefulProcessRuntime,
    evaluate_operator,
)


@dataclass(frozen=True)
class CursorStepResult:
    operator_type: str
    next_cursors: tuple[EventCursor, ...]
    terminal_payloads: tuple[Any, ...]
    event_chain_delta: int
    process_called: bool


def materialize_exception_fallback_step(
    *,
    cursor: EventCursor,
    fallback_payloads: tuple[Any, ...],
    flow_program: Any | None,
) -> CursorStepResult:
    """Materialize fallback decision into canonical cursor-step semantics."""
    fallback_outputs = _bind_output_tags(
        base_tags=cursor.tags,
        outputs=fallback_payloads,
        output_tags=None,
    )
    downstream_node_ids = _resolve_cursor_downstream_node_ids(
        cursor=cursor,
        flow_program=flow_program,
    )
    if downstream_node_ids:
        next_cursors = _spawn_linear_cursors(
            cursor=cursor,
            outputs=fallback_outputs,
            downstream_node_ids=downstream_node_ids,
        )
        return CursorStepResult(
            operator_type="exception/fallback",
            next_cursors=next_cursors,
            terminal_payloads=(),
            event_chain_delta=len(next_cursors) - 1,
            process_called=False,
        )

    if cursor.flow_stack:
        next_cursors = _spawn_resume_cursors(
            cursor=cursor,
            outputs=fallback_outputs,
        )
        return CursorStepResult(
            operator_type="exception/fallback",
            next_cursors=next_cursors,
            terminal_payloads=(),
            event_chain_delta=len(next_cursors) - 1,
            process_called=False,
        )

    return CursorStepResult(
        operator_type="exception/fallback",
        next_cursors=(),
        terminal_payloads=fallback_payloads,
        event_chain_delta=-1,
        process_called=False,
    )


def materialize_exception_drop_step(
    *,
    action: str = "drop",
) -> CursorStepResult:
    normalized_action = str(action or "").strip() or "drop"
    if normalized_action not in {"drop", "abort"}:
        normalized_action = "drop"
    return CursorStepResult(
        operator_type=f"exception/{normalized_action}",
        next_cursors=(),
        terminal_payloads=(),
        event_chain_delta=-1,
        process_called=False,
    )


def execute_cursor_step(
    *,
    cursor: EventCursor,
    flow_program: Any,
    invoke_target: Callable[[Any, Any, dict[str, str], int | None], Any] | None = None,
    state_runtime: StatefulProcessRuntime | None = None,
    reducer_runtime: ReducerRuntime | None = None,
    join_reducer_runtime: JoinReducerRuntime | None = None,
    merge_reducer_runtime: MergeReducerRuntime | None = None,
    resolve_process_entry_nodes: Callable[[FlowProgramRef], tuple[str, ...]] | None = None,
    child_chain_id_factory: Callable[[EventCursor, Any, int], str] | None = None,
) -> CursorStepResult:
    transformation = _lookup_transformation(flow_program, cursor.pc_node_id)
    operator_type = str(getattr(transformation, "type", "")).strip()

    evaluation = evaluate_operator(
        transformation=transformation,
        payload=cursor.payload,
        tags=cursor.tags,
        seq=cursor.seq,
        invoke_target=invoke_target,
        state_runtime=state_runtime,
        reducer_runtime=reducer_runtime,
        join_reducer_runtime=join_reducer_runtime,
        merge_reducer_runtime=merge_reducer_runtime,
        event_group_id=cursor.event_group_id,
    )
    output_items = _bind_output_tags(
        base_tags=cursor.tags,
        outputs=evaluation.outputs,
        output_tags=evaluation.output_tags,
    )

    if evaluation.process_program_ref is not None:
        if resolve_process_entry_nodes is None:
            raise OperatorExecutionError("resolve_process_entry_nodes is required for process operator.")
        next_cursors = _spawn_process_cursors(
            cursor=cursor,
            transformation=transformation,
            outputs=output_items,
            process_program_ref=evaluation.process_program_ref,
            resolve_process_entry_nodes=resolve_process_entry_nodes,
            child_chain_id_factory=child_chain_id_factory,
        )
        return CursorStepResult(
            operator_type=operator_type,
            next_cursors=next_cursors,
            terminal_payloads=(),
            event_chain_delta=len(next_cursors) - 1,
            process_called=True,
        )

    downstream_ids = _resolve_downstream_node_ids(
        transformation=transformation,
        route_node_ids=evaluation.route_node_ids,
    )
    if downstream_ids:
        next_cursors = _spawn_linear_cursors(
            cursor=cursor,
            outputs=output_items,
            downstream_node_ids=downstream_ids,
        )
        return CursorStepResult(
            operator_type=operator_type,
            next_cursors=next_cursors,
            terminal_payloads=(),
            event_chain_delta=len(next_cursors) - 1,
            process_called=False,
        )

    if cursor.flow_stack:
        next_cursors = _spawn_resume_cursors(
            cursor=cursor,
            outputs=output_items,
        )
        return CursorStepResult(
            operator_type=operator_type,
            next_cursors=next_cursors,
            terminal_payloads=(),
            event_chain_delta=len(next_cursors) - 1,
            process_called=False,
        )

    return CursorStepResult(
        operator_type=operator_type,
        next_cursors=(),
        terminal_payloads=tuple(payload for payload, _tags in output_items),
        event_chain_delta=-1,
        process_called=False,
    )


def _bind_output_tags(
    *,
    base_tags: Mapping[str, str],
    outputs: tuple[Any, ...],
    output_tags: tuple[dict[str, str], ...] | None,
) -> tuple[tuple[Any, dict[str, str]], ...]:
    if output_tags is None:
        return tuple((payload, dict(base_tags)) for payload in outputs)
    if len(output_tags) != len(outputs):
        raise OperatorExecutionError("operator output_tags length must match outputs length.")

    resolved: list[tuple[Any, dict[str, str]]] = []
    for payload, overlay in zip(outputs, output_tags):
        if not isinstance(overlay, Mapping):
            raise OperatorExecutionError("operator output_tags entries must be mappings.")
        merged_tags = dict(base_tags)
        for key, value in overlay.items():
            merged_tags[str(key)] = str(value)
        resolved.append((payload, merged_tags))
    return tuple(resolved)


def _lookup_transformation(flow_program: Any, pc_node_id: str) -> Any:
    lookup = getattr(flow_program, "lookup_transformation", None)
    transformation = lookup(pc_node_id) if callable(lookup) else None
    if transformation is None:
        transformations = getattr(flow_program, "transformations", None)
        if isinstance(transformations, dict):
            transformation = transformations.get(pc_node_id)

    if transformation is None:
        raise KeyError(f"unknown_pc_node_id:{pc_node_id}")
    return transformation


def _resolve_downstream_node_ids(
    *,
    transformation: Any,
    route_node_ids: tuple[str, ...] | None,
) -> tuple[str, ...]:
    if route_node_ids is not None:
        return route_node_ids

    downstreams = getattr(transformation, "downstreams", None)
    if downstreams is None:
        return ()
    if not isinstance(downstreams, list):
        raise OperatorExecutionError("transformation.downstreams must be a list.")

    downstream_ids: list[str] = []
    for index, downstream in enumerate(downstreams):
        trans_id = getattr(downstream, "trans_id", None)
        if not isinstance(trans_id, str) or not trans_id.strip():
            raise OperatorExecutionError(f"downstreams[{index}] missing valid trans_id")
        downstream_ids.append(trans_id)
    return tuple(downstream_ids)


def _spawn_linear_cursors(
    *,
    cursor: EventCursor,
    outputs: tuple[tuple[Any, dict[str, str]], ...],
    downstream_node_ids: tuple[str, ...],
) -> tuple[EventCursor, ...]:
    next_cursors: list[EventCursor] = []
    for payload, output_tags in outputs:
        for node_id in downstream_node_ids:
            next_cursors.append(
                EventCursor(
                    event_group_id=cursor.event_group_id,
                    event_chain_id=cursor.event_chain_id,
                    flow_program_ref=cursor.flow_program_ref,
                    pc_node_id=node_id,
                    flow_stack=cursor.flow_stack,
                    payload=payload,
                    tags=dict(output_tags),
                    seq=cursor.seq,
                    origin_topic_ref=cursor.origin_topic_ref,
                    convergence_topic_ref=cursor.convergence_topic_ref,
                    convergence_topic_epoch=cursor.convergence_topic_epoch,
                    meta=dict(cursor.meta),
                )
            )
    return tuple(next_cursors)


def _resolve_cursor_downstream_node_ids(
    *,
    cursor: EventCursor,
    flow_program: Any | None,
) -> tuple[str, ...]:
    if flow_program is None:
        return ()
    try:
        transformation = _lookup_transformation(flow_program, cursor.pc_node_id)
    except Exception:
        return ()

    downstreams = getattr(transformation, "downstreams", None)
    if not isinstance(downstreams, list):
        return ()

    downstream_ids: list[str] = []
    for downstream in downstreams:
        trans_id = getattr(downstream, "trans_id", None)
        if isinstance(trans_id, str) and trans_id.strip():
            downstream_ids.append(trans_id.strip())
    return tuple(downstream_ids)


def _spawn_process_cursors(
    *,
    cursor: EventCursor,
    transformation: Any,
    outputs: tuple[tuple[Any, dict[str, str]], ...],
    process_program_ref: FlowProgramRef,
    resolve_process_entry_nodes: Callable[[FlowProgramRef], tuple[str, ...]],
    child_chain_id_factory: Callable[[EventCursor, Any, int], str] | None,
) -> tuple[EventCursor, ...]:
    entry_nodes = tuple(resolve_process_entry_nodes(process_program_ref))
    if not entry_nodes:
        raise OperatorExecutionError(
            "process entry nodes must not be empty.",
        )

    resume_node_ids = _resolve_downstream_node_ids(
        transformation=transformation,
        route_node_ids=None,
    )
    frame = FlowStackFrame(
        resume_flow_program_ref=cursor.flow_program_ref,
        resume_pc_node_ids=resume_node_ids,
        caller_event_chain_id=cursor.event_chain_id,
        callsite_pc_node_id=cursor.pc_node_id,
        frame_meta={},
    )

    factory = child_chain_id_factory or _default_child_chain_id

    next_cursors: list[EventCursor] = []
    spawn_index = 0
    for payload, output_tags in outputs:
        for node_id in entry_nodes:
            child_chain_id = factory(cursor, transformation, spawn_index)
            spawn_index += 1
            child_meta = dict(cursor.meta)
            child_meta.setdefault("parent_event_chain_id", cursor.event_chain_id)
            child_meta.setdefault("process_callsite_pc_node_id", cursor.pc_node_id)
            next_cursors.append(
                EventCursor(
                    event_group_id=cursor.event_group_id,
                    event_chain_id=child_chain_id,
                    flow_program_ref=process_program_ref,
                    pc_node_id=node_id,
                    flow_stack=cursor.flow_stack + (frame,),
                    payload=payload,
                    tags=dict(output_tags),
                    seq=cursor.seq,
                    origin_topic_ref=cursor.origin_topic_ref,
                    convergence_topic_ref=cursor.convergence_topic_ref,
                    convergence_topic_epoch=cursor.convergence_topic_epoch,
                    meta=child_meta,
                )
            )
    return tuple(next_cursors)


def _default_child_chain_id(cursor: EventCursor, transformation: Any, spawn_index: int) -> str:
    trans_id = getattr(transformation, "trans_id", None)
    if not isinstance(trans_id, str) or not trans_id.strip():
        trans_id = "unknown"
    return f"{cursor.event_chain_id}:process:{trans_id}:{spawn_index}"


def _spawn_resume_cursors(
    *,
    cursor: EventCursor,
    outputs: tuple[tuple[Any, dict[str, str]], ...],
) -> tuple[EventCursor, ...]:
    frame = cursor.flow_stack[-1]
    remaining_stack = cursor.flow_stack[:-1]
    resume_node_ids = tuple(frame.resume_pc_node_ids)
    if not resume_node_ids:
        return ()

    next_cursors: list[EventCursor] = []
    for payload, output_tags in outputs:
        for node_id in resume_node_ids:
            resumed_meta = dict(cursor.meta)
            resumed_meta.setdefault("resumed_from_process_callsite", frame.callsite_pc_node_id)
            next_cursors.append(
                EventCursor(
                    event_group_id=cursor.event_group_id,
                    event_chain_id=frame.caller_event_chain_id,
                    flow_program_ref=frame.resume_flow_program_ref,
                    pc_node_id=node_id,
                    flow_stack=remaining_stack,
                    payload=payload,
                    tags=dict(output_tags),
                    seq=cursor.seq,
                    origin_topic_ref=cursor.origin_topic_ref,
                    convergence_topic_ref=cursor.convergence_topic_ref,
                    convergence_topic_epoch=cursor.convergence_topic_epoch,
                    meta=resumed_meta,
                )
            )
    return tuple(next_cursors)


__all__ = [
    "CursorStepResult",
    "execute_cursor_step",
    "materialize_exception_fallback_step",
    "materialize_exception_drop_step",
]
