from __future__ import annotations

from collections.abc import Mapping
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from flownet.v1.compiler.transformation import Transformation


class FlowProgram:
    """
    v1 compiled flow topology.

    This is an independent program model and does not depend on
    compiler.flowtask.FlowProgram/FlowTask.

    v1 execution model notes:
    - FlowProgram is a static topology + program defaults.
    - Runtime invocation is not direct submit/call; it is topic binding.
    - Binding (in_topic -> out_topic) creates a cluster lifecycle object:
      FlowProcess (metadata registration in runtime/topic modules).
    - FlowProcessCatalog and TopicRoutingDirectory are URI/meta views only.
    - In-topic coordinator drives per-input-event flow execution.
    - Internal control signals (event delta/ack/done) converge at out-topic
      coordinator, which decides request completion.
    - Coordinator state is lazily materialized by (topic_uri, epoch) and
      owner-checked by routing snapshot.
    - Control-plane signals are decoupled from data-plane payload routing.
    """

    def __init__(self):
        self.pipeline: list[Transformation] = []
        self.transformations: dict[str, Transformation] = {}
        self.entry_id: str | None = None
        self.entry_ids: list[str] = []
        self._entry_router: Transformation | None = None
        self._exception_handler_stack: list[Any | None] = [None]
        self.return_ids: list[str] = []
        self.sink_ids: list[str] = []
        self.no_return: bool = True
        # Program-level definition metadata. Topic bindings are intentionally
        # outside this object and should be attached in runtime registration APIs.
        # In v1, scheduler/resources/uri belong to program definition, while
        # input/output topic wiring belongs to FlowProcess registration.
        # Coordinator request-ledger state is also outside this object.
        self.flow_uri: str | None = None
        self.scheduler: dict[str, Any] = {}
        self.resources: dict[str, Any] = {}
        self.metadata: dict[str, Any] = {}
        self.dsl_name: str | None = None

    def configure(
        self,
        *,
        flow_uri: str | None = None,
        scheduler: Mapping[str, Any] | None = None,
        resources: Mapping[str, Any] | None = None,
        metadata: Mapping[str, Any] | None = None,
        dsl_name: str | None = None,
    ) -> FlowProgram:
        # configure() only sets program-level defaults; it does not create
        # runtime bindings or coordinator registrations.
        if flow_uri is not None:
            normalized_uri = str(flow_uri).strip()
            if not normalized_uri:
                raise ValueError("flow_uri must not be empty when provided.")
            self.flow_uri = normalized_uri
        else:
            self.flow_uri = None
        self.scheduler = dict(scheduler or {})
        self.resources = dict(resources or {})
        self.metadata = dict(metadata or {})
        self.dsl_name = str(dsl_name).strip() if dsl_name is not None else None
        return self

    def append(self, transformation: Transformation) -> None:
        self.pipeline.append(transformation)

    def lookup_transformation(self, trans_id: str) -> Transformation | None:
        return self.transformations.get(trans_id)

    def set_entry_id(self, entry_id: str) -> None:
        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.v1.compiler.targets import system_target

        return system_target()

    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.v1.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)

    @property
    def graph(self) -> FlowProgram:
        return self

    @property
    def task(self) -> FlowProgram:
        return self

    def __get__(self, instance, owner):
        # v1 does not support descriptor-style bound flow methods (no BoundFlow).
        if instance is not None:
            raise TypeError(
                "v1 FlowProgram does not support class-instance binding. "
                "Define v1 flow DSL as module/static function and bind topics externally.",
            )
        return self


__all__ = [
    "FlowProgram",
]
