from __future__ import annotations

from dataclasses import dataclass, field
from threading import Event, Lock
from typing import Any, Callable, Literal
import inspect
import uuid

from flownet.runtime.flows.request_tracker import RunRequestTracker

RunStatus = Literal["created", "running", "completed", "failed", "cancelled"]
IngressState = Literal["active", "draining", "closed"]


def _default_request_id_factory(_tracker: Any | None = None) -> str:
    return uuid.uuid4().hex


@dataclass
class FlowRunRef:
    """
    Compatibility facade for flow execution handles.

    Terminology freeze (WS-0 / M0):
    - This object remains the runtime API compatibility surface.
    - New docs refer to long-lived binding as FlowProcess.
    - Per-request external handle is named RequestRef (request_ref_id), which
      still maps to legacy request_id fields in this phase.
    """
    run_id: str
    flow_id: str
    ingress_kind: str
    output_mode: str
    ingress_writable: bool = False
    native_flow_id: str | None = None
    augmented_task_id: str | None = None
    request_id: str | None = None
    _status: RunStatus = "created"
    _capabilities: frozenset[str] = field(default_factory=frozenset)
    _cancel_fn: Callable[[], bool] | None = None
    _dispatch_fn: Callable[..., Any] | None = None
    _dispatch_tracks_requests: bool = False
    _request_id_factory: Callable[[Any | None], str] = _default_request_id_factory
    _close_ingress_fn: Callable[[Any], Any] | None = None
    # Control-plane state of ingress binding lifecycle.
    _ingress_state: IngressState = "active"
    # Request tracking state extracted from FlowRunRef hot path.
    _request_tracker: RunRequestTracker = field(default_factory=RunRequestTracker, repr=False)
    _outputs: dict[str, Any] = field(default_factory=dict)
    _dispatch_accepts_next_meta: bool = field(default=False, repr=False)
    _lock: Lock = field(default_factory=Lock, repr=False)

    def __post_init__(self) -> None:
        # Keep backward compatibility for code still reading flow_id while
        # exposing explicit native/augmented identities.
        if self.native_flow_id is None:
            self.native_flow_id = self.flow_id
        self._dispatch_accepts_next_meta = self._supports_dispatch_next_meta(self._dispatch_fn)

    @staticmethod
    def _supports_dispatch_next_meta(dispatch_fn: Callable[..., Any] | None) -> bool:
        if dispatch_fn is None:
            return False
        try:
            signature = inspect.signature(dispatch_fn)
        except (TypeError, ValueError):
            return False
        return "_next_meta" in signature.parameters

    def status(self) -> RunStatus:
        with self._lock:
            return self._status

    def ingress_state(self) -> IngressState:
        with self._lock:
            return self._ingress_state

    def inflight_requests(self) -> int:
        return self._request_tracker.inflight_requests()

    def mark_ingress_draining(self) -> None:
        with self._lock:
            if self._ingress_state == "closed":
                return
            self._ingress_state = "draining"

    def close_ingress(self) -> bool:
        close_fn: Callable[[Any], Any] | None = None
        should_invoke_close = False
        with self._lock:
            was_closed = self._ingress_state == "closed"
            self._ingress_state = "closed"
            if not was_closed and self._close_ingress_fn is not None:
                close_fn = self._close_ingress_fn
                should_invoke_close = True
        if should_invoke_close and close_fn is not None:
            close_fn(self)
        with self._lock:
            self._request_tracker.mark_all_producer_done()
            return self._maybe_complete_locked()

    def ack_delta(self, delta: int) -> int:
        with self._lock:
            next_value = self._request_tracker.ack_inflight_delta(delta)
            self._maybe_complete_locked()
            return next_value

    def start_request(self, request_id: str, initial_delta: int = 1) -> Event:
        with self._lock:
            event = self._request_tracker.start_request(
                request_id,
                initial_delta=initial_delta,
            )
            self._maybe_complete_locked()
            return event

    def request_pending(self, request_id: str) -> int | None:
        return self._request_tracker.request_pending(request_id)

    def request_done(self, request_id: str) -> bool:
        return self._request_tracker.request_done(request_id)

    def request_producer_done(self, request_id: str) -> bool:
        return self._request_tracker.request_producer_done(request_id)

    def mark_request_producer_done(self, request_id: str) -> bool:
        with self._lock:
            done = self._request_tracker.mark_producer_done(request_id)
            self._maybe_complete_locked()
            return done

    def request_delivery_done(self, request_id: str) -> bool:
        delivery_done_value: bool | None = None
        output = self.output("output")
        if output is not None:
            delivery_done_fn = getattr(output, "request_delivery_done", None)
            if callable(delivery_done_fn):
                try:
                    delivery_done_value = bool(delivery_done_fn(request_id))
                except Exception:
                    delivery_done_value = None
        if delivery_done_value is None:
            delivery_done_value = self.request_done(request_id)
        return self.request_done(request_id) and delivery_done_value

    def request_delivery_status(self, request_id: str) -> dict[str, Any]:
        def _coerce_non_negative_int(value: Any) -> int | None:
            try:
                normalized = int(value)
            except (TypeError, ValueError):
                return None
            if normalized < 0:
                return None
            return normalized

        status: dict[str, Any] | None = None
        output = self.output("output")
        has_output_delivery = output is not None
        if output is not None:
            delivery_status = getattr(output, "request_delivery_status", None)
            if callable(delivery_status):
                try:
                    maybe_status = delivery_status(request_id)
                    if isinstance(maybe_status, dict):
                        status = dict(maybe_status)
                except Exception:
                    pass
        if status is None:
            status = {
                "request_id": request_id,
                "request_ref_id": request_id,
                "delivery_done": False,
                "watermark_done": False,
                "final_seq": None,
                "stats": {},
            }
        request_ref_id = str(
            status.get("request_ref_id")
            or status.get("request_id")
            or request_id
        )
        status.setdefault("request_id", request_ref_id)
        status.setdefault("request_ref_id", request_ref_id)
        status.setdefault("delivery_done", False)
        status.setdefault("watermark_done", bool(status.get("delivery_done")))
        status.setdefault("stats", {})

        request_done = self.request_done(request_id)
        pending = self.request_pending(request_id)
        producer_done = self.request_producer_done(request_id)
        source_final_seq = self.request_source_final_seq(request_id)
        delivery_done_value = bool(status.get("delivery_done"))
        watermark_done = bool(status.get("watermark_done"))
        if not has_output_delivery:
            # No data egress channel: request_done is terminal delivery completion.
            delivery_done_value = request_done
            watermark_done = request_done
        event_chain_pending = 0 if pending is None else pending
        event_chain_done = event_chain_pending == 0
        subscriber_done = request_done and delivery_done_value
        final_seq = _coerce_non_negative_int(status.get("final_seq"))
        if final_seq is None:
            final_seq = _coerce_non_negative_int(source_final_seq)
        expected_total_events = _coerce_non_negative_int(status.get("expected_total_events"))
        if expected_total_events is None:
            expected_total_events = final_seq
        status_value = str(status.get("status") or "").strip().lower()
        if not status_value:
            if request_done:
                run_status = self.status()
                if run_status == "failed":
                    status_value = "failed"
                elif run_status == "cancelled":
                    status_value = "cancelled"
                else:
                    status_value = "completed"
            elif producer_done and event_chain_pending > 0:
                status_value = "quiescing"
            else:
                status_value = "running"
        status["request_done"] = request_done
        status["request_pending"] = event_chain_pending
        status["event_chain_pending"] = event_chain_pending
        status["event_chain_done"] = event_chain_done
        status["producer_done"] = producer_done
        status["subscriber_done"] = subscriber_done
        status["status"] = status_value
        if final_seq is not None:
            status["final_seq"] = final_seq
        if expected_total_events is not None:
            status["expected_total_events"] = expected_total_events
        if source_final_seq is not None:
            status["source_final_seq"] = source_final_seq
        status["watermark_done"] = watermark_done
        status["request_watermark_done"] = request_done and watermark_done
        status["delivery_done"] = subscriber_done
        return status

    def pending_request_ids(self) -> tuple[str, ...]:
        return self._request_tracker.pending_request_ids()

    def request_source_final_seq(self, request_id: str) -> int | None:
        return self._request_tracker.request_source_final_seq(request_id)

    def next_request_meta(self, request_id: str) -> dict[str, Any]:
        return self._request_tracker.next_request_meta(request_id)

    def ack_request_delta(self, request_id: str, delta: int) -> int:
        with self._lock:
            pending = self._request_tracker.ack_request_delta(request_id, delta)
            self._maybe_complete_locked()
            return pending

    def set_status(self, status: RunStatus) -> None:
        with self._lock:
            self._status = status

    def cancel(self) -> bool:
        cancelled = False
        if self._cancel_fn is not None:
            cancelled = bool(self._cancel_fn())
        self.set_status("cancelled")
        return cancelled

    def capabilities(self) -> tuple[str, ...]:
        with self._lock:
            return tuple(sorted(self._capabilities))

    def supports(self, capability: str) -> bool:
        with self._lock:
            return capability in self._capabilities

    def attach_output(self, name: str, value: Any) -> None:
        with self._lock:
            self._outputs[name] = value

    def output(self, name: str, default: Any = None) -> Any:
        with self._lock:
            return self._outputs.get(name, default)

    def require_output(self, name: str) -> Any:
        value = self.output(name)
        if value is None:
            raise RuntimeError(f"FlowRun output '{name}' is not available.")
        return value

    def new_request_id(self, tracker: Any | None = None) -> str:
        request_id = self._request_id_factory(tracker)
        with self._lock:
            self.request_id = request_id
        return request_id

    @property
    def request_ref_id(self) -> str | None:
        """Compatibility alias of request_id for request-ref terminology."""
        with self._lock:
            return self.request_id

    def dispatch(self, *args, request_id: str | None = None, **kwargs):
        if not self.ingress_writable:
            raise RuntimeError("FlowRun ingress does not accept dispatch().")
        if self._dispatch_fn is None:
            raise RuntimeError("FlowRun does not support dispatch().")
        rid = request_id or self.new_request_id()
        tracked_request_started = False
        if self._dispatch_tracks_requests:
            self.start_request(rid, initial_delta=1)
            tracked_request_started = True
        next_meta = self.next_request_meta(rid)
        try:
            if self._dispatch_accepts_next_meta:
                self._dispatch_fn(
                    *args,
                    request_id=rid,
                    _next_meta=next_meta,
                    **kwargs,
                )
            else:
                self._dispatch_fn(*args, request_id=rid, **kwargs)
        except Exception:
            if tracked_request_started:
                try:
                    self.mark_request_producer_done(rid)
                except Exception:
                    pass
                try:
                    self.ack_request_delta(rid, -1)
                except Exception:
                    pass
            raise
        with self._lock:
            self.request_id = rid
        return rid

    def write(self, *args, **kwargs):
        if not self.ingress_writable:
            raise RuntimeError("FlowRun ingress does not accept write().")
        output = self.output("output")
        if output is not None:
            request_id = output.write(*args, **kwargs)
            with self._lock:
                self.request_id = request_id
            return request_id
        return self.dispatch(*args, **kwargs)

    def read(
        self,
        timeout: float = 5.0,
        request_id: str | None = None,
        *,
        from_seq: int | None = None,
    ):
        if not self.supports("read"):
            raise RuntimeError("FlowRun output does not support read().")
        output = self.require_output("output")
        try:
            return output.read(timeout=timeout, request_id=request_id, from_seq=from_seq)
        except TypeError:
            return output.read(timeout=timeout, request_id=request_id)

    def subscribe(
        self,
        handler,
        *,
        request_id: str | None = None,
        from_seq: int | None = None,
    ):
        if not self.supports("subscribe"):
            raise RuntimeError("FlowRun output does not support subscribe().")
        output = self.require_output("output")
        subscribe_fn = getattr(output, "subscribe", None)
        if subscribe_fn is None:
            raise RuntimeError("FlowRun output channel has no subscribe() implementation.")
        effective_request_id = request_id if request_id is not None else self.request_id
        if effective_request_id is None:
            raise RuntimeError("FlowRun subscribe() requires request_id when run has no active request.")
        try:
            return subscribe_fn(
                handler,
                request_id=effective_request_id,
                from_seq=from_seq,
            )
        except TypeError:
            return subscribe_fn(handler)

    def _maybe_complete_locked(self) -> bool:
        if self._status in {"completed", "failed", "cancelled"}:
            return False
        if self._ingress_state != "closed":
            return False
        if self._request_tracker.inflight_requests() != 0:
            return False
        self._status = "completed"
        return True


class FlowRunRegistry:
    def __init__(self):
        # ARCH_NOTE:
        # - In-memory runtime index for submitted runs.
        # - Keyed by run_id only; no built-in durable flow-template namespace.
        self._runs: dict[str, FlowRunRef] = {}
        self._lock = Lock()

    def register(self, run: FlowRunRef) -> FlowRunRef:
        with self._lock:
            self._runs[run.run_id] = run
        return run

    def get(self, run_id: str) -> FlowRunRef | None:
        with self._lock:
            return self._runs.get(run_id)

    def remove(self, run_id: str) -> FlowRunRef | None:
        with self._lock:
            return self._runs.pop(run_id, None)


# Compatibility aliases for phased terminology migration.
FlowProcess = FlowRunRef
RequestRef = str


__all__ = [
    "RunStatus",
    "IngressState",
    "FlowRunRef",
    "FlowProcess",
    "RequestRef",
    "FlowRunRegistry",
]
