from __future__ import annotations
from collections.abc import Mapping
from typing import TYPE_CHECKING
import uuid

from flownet.core.types import PayloadData
if TYPE_CHECKING:
    from flownet.compiler.transformation import Transformation
    from flownet.core.request import FlowAckRoute, FlowRequest


class FlowProgram:
    def __init__(self):
        # ARCH_NOTE:
        # - FlowProgram is the in-memory logical topology compiled from a FlowDef.
        # - It is cached by FlowDef (default/object scope) and reused by submits.
        # - By itself FlowProgram is not a globally discoverable catalog entry;
        #   submit_flow() creates run-scoped handles in FlowRunRegistry.
        self.pipeline: list['Transformation'] = []
        self.transformations: dict[str, 'Transformation'] = {}
        # key: transformation id, value: Transformation object
        self.entry_id: str | None = None
        self.entry_ids: list[str] = []
        self._entry_router: "Transformation | None" = None
        self._exception_handler_stack: list = [None]
        self.return_ids: list[str] = []
        self.sink_ids: list[str] = []
        self.no_return: bool = True
        
        
    def append(self, transformation: 'Transformation'):
        self.pipeline.append(transformation)
        
    # def set_sink(self, transformation: 'Transformation'):
    #     self.sink_trans = transformation
    #     transformation.is_sink = True
        
    def lookup_transformation(self, trans_id: str) -> 'Transformation':
        return self.transformations.get(trans_id)

    def set_entry_id(self, entry_id: str):
        if entry_id not in self.entry_ids:
            self.entry_ids.append(entry_id)
        if self.entry_id is None:
            self.entry_id = entry_id
        self._entry_router = None

    @staticmethod
    def _system_target():
        from flownet.api.actor_method_ref import ActorMethodRef

        return ActorMethodRef(address="__local__", actor_id="__system__", method="noop")

    def _resolve_entry_transformations(self) -> list["Transformation"]:
        entry_ids = list(self.entry_ids)
        if not entry_ids and self.entry_id is not None:
            entry_ids = [self.entry_id]

        entries: list[Transformation] = []
        for trans_id in entry_ids:
            trans = self.lookup_transformation(trans_id)
            if trans is not None:
                entries.append(trans)
        if entries:
            return entries
        if self.pipeline:
            return [self.pipeline[0]]
        return []

    def resolve_entry_transformation(self) -> "Transformation | None":
        entries = self._resolve_entry_transformations()
        if not entries:
            return None
        if len(entries) == 1:
            return entries[0]

        entry_trans_ids = [trans.trans_id for trans in entries]
        router = self._entry_router
        if router is not None:
            routes = (router.operator_config or {}).get("routes") or []
            route_ids = [
                route.get("transformation").trans_id
                for route in routes
                if isinstance(route, dict) and route.get("transformation") is not None
            ]
            if route_ids == entry_trans_ids:
                return router

        from flownet.compiler.transformation import Transformation

        router = Transformation(
            self,
            self._system_target(),
            type="sys/router",
            operator_config={
                "routes": [{"transformation": trans} for trans in entries],
            },
        )
        router.exception_handler_stack = list(self._exception_handler_stack)
        self._entry_router = router
        return router

    def current_exception_handler(self):
        if not self._exception_handler_stack:
            return None
        return self._exception_handler_stack[-1]

    def mark_return(self, transformation: 'Transformation') -> None:
        if not transformation.is_return:
            transformation.is_return = True
            self.return_ids.append(transformation.trans_id)
        self.no_return = False

    def mark_sink(self, transformation: 'Transformation') -> None:
        if not transformation.is_sink:
            transformation.is_sink = True
            self.sink_ids.append(transformation.trans_id)
        
    def make_request(
        self,
        payload: PayloadData,
        request_id: str | None = None,
        origin_address: str | None = None,
        ack_route: FlowAckRoute | Mapping[str, object] | str | None = None,
    ) -> FlowRequest:
        # ARCH_NOTE:
        # - Request construction currently pushes Transformation objects into
        #   FlowStack frames (not only IDs). This keeps dynamic runtime assembly
        #   simple but implies request packets carry executable topology frames.
        from flownet.core.exceptions import FlowDefinitionError
        from flownet.core.request import (
            FlowRequest,
            FlowStack,
            TransformationFrame,
            coerce_flow_ack_route,
        )

        if request_id is None:
            request_id = uuid.uuid4().hex
        if origin_address is None:
            from flownet.runtime.kernel import get_runtime_kernel
            origin_address = get_runtime_kernel().local_address()
        entry = self.resolve_entry_transformation()
        if entry is None:
            raise FlowDefinitionError("FlowProgram has no entry transformation; cannot create request.")
        request = FlowRequest(
            flow_stack=FlowStack(
                frames=[TransformationFrame(entry, payload)]
            ),
            request_id=request_id,
            origin_address=origin_address,
            ack_route=coerce_flow_ack_route(ack_route),
        )
        return request


# Compatibility alias during FlowTask -> FlowProgram migration.
FlowTask = FlowProgram


__all__ = ["FlowProgram", "FlowTask"]
