from __future__ import annotations

from collections import deque
import inspect
from typing import Any, Callable

from flownet.v1.runtime.flowengine.cursor_models import (
    EventCursor,
    FlowProgramRef,
    TopicRef,
)
from flownet.v1.runtime.flowengine.engine import FlowEngineV1
from flownet.v1.runtime.flowengine.exception_runner import resolve_exception_for_cursor_sync
from flownet.v1.runtime.flowengine.operator_executor import (
    CursorStepResult,
    materialize_exception_drop_step,
    materialize_exception_fallback_step,
)
from flownet.v1.runtime.flowengine.operator_runtime import (
    ReducerContractError,
    ShuffleOperatorContractError,
    StatefulProcessContractError,
    UnsupportedOperatorTypeError,
)
from flownet.v1.runtime.flowengine.scope_resolver import (
    ProgramScopeRegistry,
    compile_exception_scopes,
)
from flownet.v1.runtime.topics.coordinator_registry import CoordinatorTopicState
from flownet.v1.runtime.topics.flow_process_catalog import FlowProcessCatalog
from flownet.v1.runtime.topics.normalization import _normalize_non_empty
from flownet.v1.runtime.topics.routing_directory import TopicRouteView


class FlowProcessExecution:
    """Flowengine-side ingress executor: input event -> out-topic intents."""

    def __init__(
        self,
        *,
        flow_process_catalog: FlowProcessCatalog,
        flow_engine: FlowEngineV1,
        resolve_flow_program: Callable[[FlowProgramRef], Any | None],
        resolve_topic_route: Callable[..., TopicRouteView],
        publish_flow_output: Callable[
            [str, str, Any, dict[str, str] | None, int | None, int | None],
            dict[str, Any],
        ],
        apply_or_forward_event_chain_delta: Callable[
            [str, str, int, int | None],
            dict[str, Any] | None,
        ],
        invoke_exception_handler: Callable[[Any, dict[str, Any]], Any] | None = None,
        max_cursor_steps: int = 10_000,
    ) -> None:
        self._flow_process_catalog = flow_process_catalog
        self._flow_engine = flow_engine
        self._resolve_flow_program = resolve_flow_program
        self._resolve_topic_route = resolve_topic_route
        self._publish_flow_output = publish_flow_output
        self._apply_or_forward_event_chain_delta = apply_or_forward_event_chain_delta
        self._scope_registry = ProgramScopeRegistry()
        self._invoke_exception_handler = invoke_exception_handler or _default_exception_handler_invoker
        self._max_cursor_steps = max(1, int(max_cursor_steps))

    def dispatch(
        self,
        *,
        state: CoordinatorTopicState,
        event_group_id: str,
        payload: Any,
        tags: dict[str, str] | None,
        seq: int | None,
    ) -> list[dict[str, Any]]:
        reports: list[dict[str, Any]] = []
        for flow_process_uri in sorted(state.consuming_flow_process_uris):
            reports.append(
                self.execute_flow_process(
                    flow_process_uri=flow_process_uri,
                    event_group_id=event_group_id,
                    payload=payload,
                    tags=tags,
                    seq=seq,
                    in_topic_epoch=state.epoch,
                )
            )
        return reports

    def execute_flow_process(
        self,
        *,
        flow_process_uri: str,
        event_group_id: str,
        payload: Any,
        tags: dict[str, str] | None,
        seq: int | None,
        in_topic_epoch: int | None = None,
    ) -> dict[str, Any]:
        normalized_flow_process_uri = _normalize_non_empty(
            flow_process_uri,
            field_name="flow_process_uri",
        )
        normalized_event_group_id = _normalize_non_empty(
            event_group_id,
            field_name="event_group_id",
        )
        record = self._flow_process_catalog.get(normalized_flow_process_uri)
        if record is None:
            return {
                "flow_process_uri": normalized_flow_process_uri,
                "status": "missing_flow_process_record",
                "output_count": 0,
                "event_chain_delta": 0,
                "forward_intents": [],
            }

        execution = self._execute_with_cursor_runtime(
            flow_process_uri=normalized_flow_process_uri,
            flow_program_ref=FlowProgramRef(
                program_uri=record.flow_program_uri,
                program_rev=record.flow_program_rev,
            ),
            in_topic_uri=record.in_topic_uri,
            out_topic_uri=record.out_topic_uri,
            event_group_id=normalized_event_group_id,
            payload=payload,
            tags=tags,
            seq=seq,
            in_topic_epoch=in_topic_epoch,
        )

        if execution["status"] == "error":
            return {
                "flow_process_uri": normalized_flow_process_uri,
                "flow_program_uri": record.flow_program_uri,
                "status": execution["error_status"],
                "error_type": execution["error_type"],
                "message": execution["message"],
                "output_count": 0,
                "event_chain_delta": 0,
                "forward_intents": [],
            }

        output_count = 0
        forward_intents: list[dict[str, Any]] = []
        for output in execution["outputs"]:
            output_topic_epoch = int(
                output.get("convergence_topic_epoch", execution["out_topic_epoch"]),
            )
            publish_result = self._publish_flow_output(
                record.out_topic_uri,
                normalized_event_group_id,
                output["payload"],
                output["tags"],
                output["seq"],
                output_topic_epoch,
            )
            if publish_result.get("mode") == "local":
                output_count += 1
            else:
                forward_intents.append(publish_result)

        event_chain_delta = int(execution["event_chain_delta"])
        if event_chain_delta != 0:
            control_result = self._apply_or_forward_event_chain_delta(
                record.out_topic_uri,
                normalized_event_group_id,
                event_chain_delta,
                int(execution["out_topic_epoch"]),
            )
            if control_result is not None:
                forward_intents.append(control_result)

        status = "executed"
        if forward_intents:
            status = "executed_with_forward_intents"
        return {
            "flow_process_uri": normalized_flow_process_uri,
            "flow_program_uri": record.flow_program_uri,
            "status": status,
            "output_count": output_count,
            "event_chain_delta": event_chain_delta,
            "forward_intents": forward_intents,
        }

    def _execute_with_cursor_runtime(
        self,
        *,
        flow_process_uri: str,
        flow_program_ref: FlowProgramRef,
        in_topic_uri: str,
        out_topic_uri: str,
        event_group_id: str,
        payload: Any,
        tags: dict[str, str] | None,
        seq: int | None,
        in_topic_epoch: int | None,
    ) -> dict[str, Any]:
        root_program = self._resolve_flow_program(flow_program_ref)
        if root_program is None:
            return {
                "status": "error",
                "error_status": "missing_flow_program_runtime",
                "error_type": "RuntimeError",
                "message": (
                    "missing_flow_program:"
                    f"{flow_program_ref.program_uri}@{flow_program_ref.program_rev}"
                ),
                "outputs": (),
                "event_chain_delta": 0,
            }

        root_entry_node_ids = self._resolve_entry_node_ids(root_program)
        if not root_entry_node_ids:
            return {
                "status": "error",
                "error_status": "invalid_flow_program_entry",
                "error_type": "RuntimeError",
                "message": (
                    "flow_program_missing_entry:"
                    f"{flow_program_ref.program_uri}@{flow_program_ref.program_rev}"
                ),
                "outputs": (),
                "event_chain_delta": 0,
            }

        in_topic_route = self._resolve_topic_route(in_topic_uri, epoch=in_topic_epoch)
        out_topic_route = self._resolve_topic_route(out_topic_uri, epoch=None)
        root_cursor = EventCursor(
            event_group_id=event_group_id,
            event_chain_id=f"{event_group_id}:chain:0",
            flow_program_ref=flow_program_ref,
            pc_node_id=root_entry_node_ids[0],
            flow_stack=(),
            payload=payload,
            tags=dict(tags or {}),
            seq=seq,
            origin_topic_ref=TopicRef(
                topic_uri=in_topic_route.topic_uri,
                coordinator_address=in_topic_route.coordinator_address,
            ),
            convergence_topic_ref=TopicRef(
                topic_uri=out_topic_route.topic_uri,
                coordinator_address=out_topic_route.coordinator_address,
            ),
            convergence_topic_epoch=out_topic_route.epoch,
            meta={"flow_process_uri": flow_process_uri},
        )

        pending: deque[EventCursor] = deque([root_cursor])
        outputs: list[dict[str, Any]] = []
        step_count = 0
        outstanding_event_chains = 1

        try:
            while pending:
                cursor = pending.popleft()
                step_result = self._execute_cursor_step_with_exception_handling(cursor)
                step_count += 1
                if step_count > self._max_cursor_steps:
                    raise RuntimeError(f"cursor_step_limit_exceeded:{self._max_cursor_steps}")
                outstanding_event_chains += int(step_result.event_chain_delta)
                pending.extend(step_result.next_cursors)
                for terminal_payload in step_result.terminal_payloads:
                    outputs.append(
                        {
                            "payload": terminal_payload,
                            "tags": dict(cursor.tags),
                            "seq": None,
                            "convergence_topic_epoch": cursor.convergence_topic_epoch,
                        }
                    )
        except Exception as exc:
            return {
                "status": "error",
                "error_status": "cursor_execution_error",
                "error_type": exc.__class__.__name__,
                "message": str(exc),
                "outputs": (),
                "event_chain_delta": 0,
            }

        event_chain_delta = max(0, outstanding_event_chains)
        return {
            "status": "ok",
            "outputs": tuple(outputs),
            "event_chain_delta": event_chain_delta,
            "out_topic_epoch": root_cursor.convergence_topic_epoch,
        }

    def _execute_cursor_step_with_exception_handling(
        self,
        cursor: EventCursor,
    ) -> CursorStepResult:
        cursor_program = self._resolve_flow_program(cursor.flow_program_ref)
        if cursor_program is None:
            raise RuntimeError(
                "missing_flow_program:"
                f"{cursor.flow_program_ref.program_uri}@{cursor.flow_program_ref.program_rev}",
            )
        try:
            return self._flow_engine.execute_cursor_step(
                cursor=cursor,
                flow_program=cursor_program,
                invoke_target=self._invoke_target,
                resolve_process_entry_nodes=self._resolve_process_entry_nodes,
            )
        except (
            UnsupportedOperatorTypeError,
            StatefulProcessContractError,
            ShuffleOperatorContractError,
            ReducerContractError,
        ):
            # Advanced operator contract violations are non-recoverable runtime errors.
            raise
        except Exception as exc:
            return self._resolve_step_exception(
                cursor=cursor,
                flow_program=cursor_program,
                exc=exc,
            )

    def _resolve_step_exception(
        self,
        *,
        cursor: EventCursor,
        flow_program: Any,
        exc: Exception,
    ) -> CursorStepResult:
        event = {
            "kind": "operator_exception",
            "error": exc,
            "error_type": exc.__class__.__name__,
            "message": str(exc),
            "event_group_id": cursor.event_group_id,
            "event_chain_id": cursor.event_chain_id,
            "flow_program_uri": cursor.flow_program_ref.program_uri,
            "pc_node_id": cursor.pc_node_id,
            "payload": cursor.payload,
            "tags": dict(cursor.tags),
            "seq": cursor.seq,
            "meta": dict(cursor.meta),
        }
        try:
            resolution = resolve_exception_for_cursor_sync(
                cursor=cursor,
                event=event,
                resolve_scopes=self._resolve_exception_scopes,
                invoke_handler=self._invoke_exception_handler,
            )
        except Exception:
            return materialize_exception_drop_step()

        if not resolution.handled or resolution.decision is None:
            return materialize_exception_drop_step()

        decision = resolution.decision
        if decision.action == "fallback":
            fallback_payloads = tuple(decision.fallback_payloads or ())
            return materialize_exception_fallback_step(
                cursor=cursor,
                fallback_payloads=fallback_payloads,
                flow_program=flow_program,
            )

        if decision.action in {"drop", "abort"}:
            return materialize_exception_drop_step(action=decision.action)

        return materialize_exception_drop_step()

    def _resolve_exception_scopes(self, program_ref: FlowProgramRef):
        try:
            return self._scope_registry.resolve(program_ref)
        except KeyError:
            flow_program = self._resolve_flow_program(program_ref)
            if flow_program is None:
                raise
            compiled_scopes = compile_exception_scopes(flow_program)
            self._scope_registry.register(program_ref, compiled_scopes)
            return compiled_scopes

    def _resolve_process_entry_nodes(self, program_ref: FlowProgramRef) -> tuple[str, ...]:
        flow_program = self._resolve_flow_program(program_ref)
        if flow_program is None:
            raise RuntimeError(
                "missing_flow_program:"
                f"{program_ref.program_uri}@{program_ref.program_rev}",
            )
        entry_node_ids = self._resolve_entry_node_ids(flow_program)
        if not entry_node_ids:
            raise RuntimeError(
                "flow_program_missing_entry:"
                f"{program_ref.program_uri}@{program_ref.program_rev}",
            )
        return entry_node_ids

    def _resolve_entry_node_ids(self, flow_program: Any) -> tuple[str, ...]:
        resolver = getattr(flow_program, "resolve_entry_transformation", None)
        if callable(resolver):
            entry_transformation = resolver()
            trans_id = getattr(entry_transformation, "trans_id", None)
            if isinstance(trans_id, str) and trans_id.strip():
                return (trans_id.strip(),)
        return ()

    def _invoke_target(self, target: Any, payload: Any, tags: dict[str, str], seq: int | None) -> Any:
        if not callable(target):
            raise RuntimeError(f"unsupported_operator_target:{type(target).__name__}")
        return _invoke_callable_target(target, payload=payload, tags=tags, seq=seq)


def _invoke_callable_target(
    target: Callable[..., Any],
    *,
    payload: Any,
    tags: dict[str, str],
    seq: int | None,
) -> Any:
    try:
        signature = inspect.signature(target)
    except (TypeError, ValueError):
        return target(payload)

    positional_capacity = 0
    has_var_positional = False
    for parameter in signature.parameters.values():
        if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
            has_var_positional = True
            break
        if parameter.kind in (
            inspect.Parameter.POSITIONAL_ONLY,
            inspect.Parameter.POSITIONAL_OR_KEYWORD,
        ):
            positional_capacity += 1
    if has_var_positional:
        return target(payload, tags, seq)
    if positional_capacity >= 3:
        return target(payload, tags, seq)
    if positional_capacity == 2:
        return target(payload, tags)
    if positional_capacity == 1:
        return target(payload)
    return target()


def _default_exception_handler_invoker(handler_target: Any, event: dict[str, Any]) -> Any:
    if callable(handler_target):
        return handler_target(event)
    raise RuntimeError(
        f"unsupported_exception_handler_target:{type(handler_target).__name__}",
    )


__all__ = ["FlowProcessExecution"]
