from __future__ import annotations

from dataclasses import dataclass
from threading import Lock
from typing import TYPE_CHECKING, Any, Callable, Mapping

from flownet.compiler.flowtask import FlowProgram
from flownet.runtime.flows.program_catalog import FlowProgramCatalog

if TYPE_CHECKING:
    from flownet.runtime.topics.topic_registry import TopicRegistry


@dataclass(frozen=True)
class TopicFlowProgramBinding:
    flow_uri: str
    version: str
    topic_id: str
    subscription_id: str
    execution_address: str
    tags: dict[str, str] | None = None

    def to_public_dict(self) -> dict[str, Any]:
        return {
            "flow_uri": self.flow_uri,
            "version": self.version,
            "topic_id": self.topic_id,
            "subscription_id": self.subscription_id,
            "execution_address": self.execution_address,
            "tags": dict(self.tags or {}),
        }


class TopicFlowProgramService:
    """
    Experimental topic-first FlowProgram control-plane service.

    Model boundary:
    - URL catalog and flow-program binding metadata live in topics module.
    - This service is a catalog/binding view layer, not a coordinator ledger.
    - Run materialization remains a compatibility bridge via submit_flow_fn.

    v1 flat model alignment:
    - canonical flow-process binding truth should be coordinator-owned metadata;
    - this service keeps lightweight program/binding records for registration APIs.
    """

    def __init__(
        self,
        *,
        topic_registry: "TopicRegistry",
        local_address_fn: Callable[[], str],
        submit_flow_fn: Callable[..., Any] | None = None,
    ):
        self._topic_registry = topic_registry
        self._local_address_fn = local_address_fn
        self._submit_flow_fn = submit_flow_fn
        self._programs = FlowProgramCatalog()
        self._lock = Lock()
        self._bindings_by_program: dict[tuple[str, str], dict[str, TopicFlowProgramBinding]] = {}

    def register_flow_program(
        self,
        flow_def_or_task,
        *,
        flow_uri: str,
        version: str,
        metadata: Mapping[str, Any] | None = None,
        io_contract: Mapping[str, Any] | None = None,
    ) -> dict[str, Any]:
        payload = self._resolve_task(flow_def_or_task)
        if payload is None:
            payload = flow_def_or_task
        record = self._programs.register(
            flow_uri=flow_uri,
            version=version,
            payload=payload,
            metadata=metadata,
            io_contract=io_contract,
        )
        return record.to_public_dict()

    def resolve_flow_program_payload(self, flow_uri: str, version: str) -> Any:
        record = self._programs.get(flow_uri, version=version)
        if record is None:
            raise ValueError(f"FlowProgram not found: {flow_uri}@{version}")
        return record.payload

    def list_flow_programs(
        self,
        *,
        prefix: str | None = None,
        tags: Mapping[str, Any] | None = None,
        limit: int | None = None,
    ) -> list[dict[str, Any]]:
        records = self._programs.list(prefix=prefix, tags=tags, limit=limit)
        return [record.to_public_dict() for record in records]

    def get_flow_program(self, flow_uri: str, version: str | None = None) -> dict[str, Any] | None:
        record = self._programs.get(flow_uri, version=version)
        if record is None:
            return None
        return record.to_public_dict()

    def delete_flow_program(self, flow_uri: str, version: str | None = None) -> dict[str, Any]:
        deleted = self._programs.delete(flow_uri, version=version)
        if deleted > 0:
            self._drop_bindings(flow_uri=flow_uri, version=version)
        return {"deleted": deleted}

    def submit_flow_program(
        self,
        flow_uri: str,
        version: str | None = None,
        *,
        bindings: Mapping[str, Any] | None = None,
        ingress=None,
        egress=None,
        run_config: Mapping[str, Any] | None = None,
    ):
        if self._submit_flow_fn is None:
            raise RuntimeError(
                "TopicFlowProgramService.submit_flow_program requires submit_flow_fn bridge."
            )
        record = self._programs.get(flow_uri, version=version)
        if record is None:
            if version is None:
                raise ValueError(f"FlowProgram not found: {flow_uri}")
            raise ValueError(f"FlowProgram not found: {flow_uri}@{version}")

        connector_defaults = record.io_contract.get("connector_defaults")
        if isinstance(connector_defaults, Mapping):
            if ingress is None:
                ingress = connector_defaults.get("ingress")
            if egress is None:
                egress = connector_defaults.get("egress")

        effective_run_config: dict[str, Any] = dict(run_config or {})
        if isinstance(bindings, Mapping):
            effective_run_config["bindings"] = dict(bindings)
        flow_id = record.flow_id()
        effective_run_config.setdefault("flow_id", flow_id)
        effective_run_config.setdefault("native_flow_id", flow_id)
        effective_run_config.setdefault(
            "flow_program",
            {
                "program_id": record.program_id,
                "flow_uri": record.flow_uri,
                "version": record.version,
                "program_hash": record.program_hash,
            },
        )
        return self._submit_flow_fn(
            record.payload,
            ingress=ingress,
            egress=egress,
            run_config=effective_run_config,
        )

    def bind_flow_program_topic(
        self,
        flow_uri: str,
        *,
        topic_id: str,
        version: str | None = None,
        tags: Mapping[str, Any] | None = None,
        execution_address: str | None = None,
    ) -> dict[str, Any]:
        record = self._programs.get(flow_uri, version=version)
        if record is None:
            if version is None:
                raise ValueError(f"FlowProgram not found: {flow_uri}")
            raise ValueError(f"FlowProgram not found: {flow_uri}@{version}")
        task = self._resolve_task(record.payload)
        if task is None:
            raise ValueError(
                "Topic binding requires FlowProgram payload. "
                "Register compiled FlowProgram (or FlowDef with resolvable _get_task) for this path."
            )
        normalized_topic_id = str(topic_id or "").strip()
        if not normalized_topic_id:
            raise ValueError("topic_id must be a non-empty string.")
        normalized_execution_address = str(
            execution_address or self._local_address_fn()
        ).strip()
        if not normalized_execution_address:
            raise ValueError("execution_address must be a non-empty string.")
        normalized_tags: dict[str, str] | None = None
        if tags is not None:
            if not isinstance(tags, Mapping):
                raise ValueError("tags must be a mapping when provided.")
            normalized_tags = {str(key): str(value) for key, value in tags.items()}
        subscription_id = self._topic_registry.attach_task(
            normalized_topic_id,
            task,
            normalized_execution_address,
            tags=normalized_tags,
        )
        binding = TopicFlowProgramBinding(
            flow_uri=record.flow_uri,
            version=record.version,
            topic_id=normalized_topic_id,
            subscription_id=subscription_id,
            execution_address=normalized_execution_address,
            tags=normalized_tags,
        )
        key = (record.flow_uri, record.version)
        with self._lock:
            per_program = self._bindings_by_program.setdefault(key, {})
            per_program[subscription_id] = binding
        return binding.to_public_dict()

    def unbind_flow_program_topic(self, subscription_id: str) -> bool:
        normalized_subscription_id = str(subscription_id or "").strip()
        if not normalized_subscription_id:
            return False
        detached = bool(self._topic_registry.detach_task(normalized_subscription_id))
        with self._lock:
            for key, per_program in list(self._bindings_by_program.items()):
                if normalized_subscription_id in per_program:
                    per_program.pop(normalized_subscription_id, None)
                    if not per_program:
                        self._bindings_by_program.pop(key, None)
                    break
        return detached

    def list_topic_bindings(
        self,
        flow_uri: str,
        *,
        version: str | None = None,
    ) -> list[dict[str, Any]]:
        normalized_uri = str(flow_uri or "").strip()
        if not normalized_uri:
            raise ValueError("flow_uri must be a non-empty string.")
        result: list[dict[str, Any]] = []
        with self._lock:
            for (uri, record_version), per_program in self._bindings_by_program.items():
                if uri != normalized_uri:
                    continue
                if version is not None and record_version != str(version):
                    continue
                for binding in per_program.values():
                    result.append(binding.to_public_dict())
        result.sort(key=lambda item: str(item.get("subscription_id") or ""))
        return result

    def _drop_bindings(self, *, flow_uri: str, version: str | None) -> None:
        to_remove: list[str] = []
        with self._lock:
            for (uri, record_version), per_program in list(self._bindings_by_program.items()):
                if uri != flow_uri:
                    continue
                if version is not None and record_version != str(version):
                    continue
                to_remove.extend(per_program.keys())
        for subscription_id in to_remove:
            self.unbind_flow_program_topic(subscription_id)

    @staticmethod
    def _resolve_task(flow_def_or_task) -> FlowProgram | None:
        if isinstance(flow_def_or_task, FlowProgram):
            return flow_def_or_task
        get_task = getattr(flow_def_or_task, "_get_task", None)
        if callable(get_task):
            try:
                return get_task(None)
            except TypeError:
                return get_task()
        return None


__all__ = [
    "TopicFlowProgramBinding",
    "TopicFlowProgramService",
]
