from __future__ import annotations

from dataclasses import dataclass
from threading import Lock
from typing import Any

from flownet.v1.runtime.flowengine.cursor_models import FlowProgramRef

from flownet.v1.runtime.topics.normalization import (
    _normalize_non_empty,
    _normalize_non_negative_int,
    _normalize_topic_uri,
)


@dataclass(frozen=True)
class TopicRouteView:
    topic_uri: str
    coordinator_address: str
    epoch: int


@dataclass(frozen=True)
class FlowProgramRouteView:
    flow_program_ref: FlowProgramRef
    owner_address: str


class TopicRoutingDirectory:
    """
    URI routing view only.

    This directory holds topic coordinator owner snapshots (address + epoch)
    and does not execute coordinator request logic.
    """

    def __init__(self):
        self._routes: dict[tuple[str, int], TopicRouteView] = {}
        self._latest_epoch_by_topic: dict[str, int] = {}
        self._lock = Lock()

    def upsert_route(
        self,
        *,
        topic_uri: str,
        coordinator_address: str,
        epoch: int,
    ) -> TopicRouteView:
        route = TopicRouteView(
            topic_uri=_normalize_topic_uri(topic_uri),
            coordinator_address=_normalize_non_empty(
                coordinator_address,
                field_name="coordinator_address",
            ),
            epoch=_normalize_non_negative_int(epoch, field_name="epoch"),
        )
        with self._lock:
            key = (route.topic_uri, route.epoch)
            self._routes[key] = route
            previous_latest = self._latest_epoch_by_topic.get(route.topic_uri)
            if previous_latest is None or route.epoch >= previous_latest:
                self._latest_epoch_by_topic[route.topic_uri] = route.epoch
        return route

    def resolve(
        self,
        topic_uri: str,
        *,
        epoch: int | None = None,
    ) -> TopicRouteView | None:
        normalized_topic_uri = _normalize_topic_uri(topic_uri)
        normalized_epoch: int | None = None
        if epoch is not None:
            normalized_epoch = _normalize_non_negative_int(epoch, field_name="epoch")
        with self._lock:
            if normalized_epoch is None:
                latest_epoch = self._latest_epoch_by_topic.get(normalized_topic_uri)
                if latest_epoch is None:
                    return None
                normalized_epoch = latest_epoch
            return self._routes.get((normalized_topic_uri, normalized_epoch))

    def require(
        self,
        topic_uri: str,
        *,
        epoch: int | None = None,
    ) -> TopicRouteView:
        route = self.resolve(topic_uri, epoch=epoch)
        if route is None:
            normalized_topic_uri = _normalize_topic_uri(topic_uri)
            if epoch is None:
                raise RuntimeError(f"topic_route_not_found:{normalized_topic_uri}")
            normalized_epoch = _normalize_non_negative_int(epoch, field_name="epoch")
            raise RuntimeError(
                f"topic_epoch_route_not_found:{normalized_topic_uri}@{normalized_epoch}",
            )
        return route


class FlowProgramRoutingDirectory:
    """Flow program owner route view keyed by canonical FlowProgramRef."""

    def __init__(self):
        self._routes: dict[FlowProgramRef, FlowProgramRouteView] = {}
        self._lock = Lock()

    def upsert_route(
        self,
        *,
        flow_program_uri: str,
        flow_program_rev: str,
        owner_address: str,
    ) -> FlowProgramRouteView:
        flow_program_ref = _normalize_program_ref(
            flow_program_uri=flow_program_uri,
            flow_program_rev=flow_program_rev,
        )
        route = FlowProgramRouteView(
            flow_program_ref=flow_program_ref,
            owner_address=_normalize_non_empty(
                owner_address,
                field_name="owner_address",
            ),
        )
        with self._lock:
            self._routes[flow_program_ref] = route
        return route

    def resolve(self, flow_program_ref: FlowProgramRef) -> FlowProgramRouteView | None:
        normalized_ref = _coerce_flow_program_ref(flow_program_ref)
        with self._lock:
            return self._routes.get(normalized_ref)

    def require(self, flow_program_ref: FlowProgramRef) -> FlowProgramRouteView:
        route = self.resolve(flow_program_ref)
        if route is None:
            normalized_ref = _coerce_flow_program_ref(flow_program_ref)
            raise RuntimeError(
                "flow_program_route_not_found:"
                f"{normalized_ref.program_uri}@{normalized_ref.program_rev}",
            )
        return route


def _normalize_program_ref(
    *,
    flow_program_uri: str,
    flow_program_rev: str,
) -> FlowProgramRef:
    return FlowProgramRef(
        program_uri=_normalize_non_empty(
            flow_program_uri,
            field_name="flow_program_uri",
        ),
        program_rev=_normalize_non_empty(
            flow_program_rev,
            field_name="flow_program_rev",
        ),
    )


def _coerce_flow_program_ref(raw: Any) -> FlowProgramRef:
    if not isinstance(raw, FlowProgramRef):
        raise TypeError("flow_program_ref must be FlowProgramRef.")
    return _normalize_program_ref(
        flow_program_uri=raw.program_uri,
        flow_program_rev=raw.program_rev,
    )


__all__ = [
    "TopicRouteView",
    "TopicRoutingDirectory",
    "FlowProgramRouteView",
    "FlowProgramRoutingDirectory",
]
