from __future__ import annotations

from dataclasses import dataclass, field
import os
import socket
import subprocess
import sys
import threading
import time
import uuid
from typing import Any

from flownet.config import FlownetConfig
from flownet.proactive.manager_actor import CONTAINER_EVENT_SERVICE_NAME
from flownet.runtime.kernel import RuntimeKernel, get_runtime_kernel
from flownet.runtime.transport.transport_selector import TransportSelector


@dataclass
class ContainerRecord:
    address: str
    pid: int
    created_at: float
    config: FlownetConfig
    process: subprocess.Popen
    tags: dict[str, str] = field(default_factory=dict)


class ContainerManagerActor:
    def __init__(self, runtime: Any | None = None, node_id: str | None = None):
        # runtime is kept for legacy callers; prefer runtime services for new code.
        self._runtime = runtime
        self._node_id = node_id
        self._lock = threading.Lock()
        self._containers: dict[str, ContainerRecord] = {}
        self._kernel: RuntimeKernel | None = None

    def _get_kernel(self) -> RuntimeKernel:
        if self._kernel is None:
            self._kernel = get_runtime_kernel()
        return self._kernel

    def _local_address(self) -> str:
        if self._runtime is not None:
            return self._runtime.local_address()
        return self._get_kernel().local_address()

    def _service_registry(self):
        return self._get_kernel().require_topics().service_registry

    def _cluster_view(self):
        if self._runtime is not None:
            return getattr(self._runtime, "cluster_view", None)
        try:
            return self._get_kernel().require_cluster().get_view()
        except Exception:
            return None

    def _sync_local_worker_summary(self) -> None:
        # Keep placement signal at node summary level; do not create container
        # membership rows in ClusterView.
        cluster_view = self._cluster_view()
        if cluster_view is None:
            return
        update_fn = getattr(cluster_view, "update_local_resource_summary", None)
        if not callable(update_fn):
            return
        with self._lock:
            records = list(self._containers.values())
        running = 0
        exited = 0
        for record in records:
            if record.process.poll() is None:
                running += 1
            else:
                exited += 1
        try:
            update_fn(
                {
                    "worker_containers_total": len(records),
                    "worker_containers_running": running,
                    "worker_containers_exited": exited,
                }
            )
        except Exception:
            # Never fail container lifecycle on summary observability update.
            return

    def _publish_event(self, event_type: str, record: ContainerRecord) -> None:
        # Container events are observability/control-plane signals.
        # They are intentionally not equivalent to ClusterView membership updates.
        # ClusterView remains node-centric; container state should be aggregated
        # into node-level summaries when needed.
        # Placement integration rule (freeze):
        # - container lifecycle can influence scheduler inputs only through
        #   node-level supply/resource summaries (not membership rows).
        registry = self._service_registry()
        if registry is None:
            return
        payload = {
            "address": record.address,
            "pid": record.pid,
            "created_at": record.created_at,
            "host": record.config.host,
            "port": record.config.port,
            "threads": record.config.threads,
            "node_id": record.config.node_id,
            "host_id": record.config.host_id,
            "uds_path": record.config.uds_path,
            "shm_path": record.config.shm_path,
            "transport_caps": list(record.config.transport_caps or ()),
        }
        registry.publish(
            CONTAINER_EVENT_SERVICE_NAME,
            event_type,
            payload=payload,
            tags=dict(record.tags),
        )

    def _pick_free_port(self, host: str) -> int:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.bind((host, 0))
            return sock.getsockname()[1]

    @staticmethod
    def _resolve_host_id(config: FlownetConfig) -> str | None:
        if isinstance(config.host_id, str) and config.host_id.strip():
            return config.host_id.strip()
        try:
            value = socket.gethostname().strip()
            return value or None
        except Exception:
            return None

    @staticmethod
    def _resolve_transport_caps(config: FlownetConfig) -> tuple[str, ...]:
        if config.transport_caps:
            normalized: list[str] = []
            for cap in config.transport_caps:
                if not isinstance(cap, str):
                    continue
                value = cap.strip().lower()
                if not value or value in normalized:
                    continue
                normalized.append(value)
            if normalized:
                if "tcp" not in normalized:
                    normalized.append("tcp")
                return tuple(normalized)
        mode = (config.transport_mode or "auto").strip().lower()
        caps = ["tcp"]
        if mode in {"auto", "uds_prefer", "hetero_auto"}:
            caps.append("uds")
        if mode in {"hetero_auto"}:
            caps.append("shm")
        return tuple(caps)

    @staticmethod
    def _cleanup_ipc_path(path: str | None) -> None:
        if not path:
            return
        try:
            if os.path.exists(path):
                os.unlink(path)
        except FileNotFoundError:
            pass
        except Exception:
            pass

    def _cleanup_runtime_ipc(self, config: FlownetConfig) -> None:
        self._cleanup_ipc_path(config.uds_path)
        self._cleanup_ipc_path(config.shm_path)

    async def spawn_container(
        self,
        config: FlownetConfig,
        tenant_id: str | None = None,
        flow_id: str | None = None,
        request_id: str | None = None,
    ) -> str:
        # TODO: Enforce auth, tenancy, and resource constraints before creation.
        print("[NodeRuntime] Spawning new container runtime...")
        host = config.host
        port = config.port
        threads = config.threads
        if port == 0:
            port = self._pick_free_port(host)

        host_id = self._resolve_host_id(config)
        transport_caps = self._resolve_transport_caps(config)
        selector = TransportSelector(
            local_host=host,
            local_port=port,
            transport_mode=config.transport_mode,
            local_ipc_dir=config.local_ipc_dir,
            local_host_id=host_id,
            local_transport_caps=transport_caps,
            shm_threshold_bytes=config.transport_shm_threshold_bytes,
        )
        uds_path = (config.uds_path or selector.local_uds_path) if "uds" in transport_caps else None
        shm_path = (config.shm_path or selector.local_shm_path) if "shm" in transport_caps else None

        used_config = FlownetConfig(
            host=host,
            port=port,
            threads=threads,
            node_id=self._node_id or config.node_id,
            transport_mode=config.transport_mode,
            local_ipc_dir=config.local_ipc_dir,
            transport_shm_threshold_bytes=config.transport_shm_threshold_bytes,
            transport_shm_max_payload_bytes=config.transport_shm_max_payload_bytes,
            transport_backend_cooldown_seconds=config.transport_backend_cooldown_seconds,
            transport_backend_cooldown_max_entries=config.transport_backend_cooldown_max_entries,
            transport_backend_cooldown_cleanup_interval=config.transport_backend_cooldown_cleanup_interval,
            host_id=host_id,
            uds_path=uds_path,
            shm_path=shm_path,
            transport_caps=transport_caps,
        )
        self._cleanup_runtime_ipc(used_config)

        call_uuid = uuid.uuid4().hex
        if self._runtime is not None:
            future = self._runtime.call_manager.register(call_uuid)
        else:
            future = self._get_kernel().require_io().register_call(call_uuid)
        creator = self._local_address()
        args = [
            sys.executable,
            "-u",
            "-m",
            "flownet.platform.start_container",
            f"--host={host}",
            f"--port={port}",
            f"--threads={threads}",
            f"--node-id={used_config.node_id}",
            f"--transport-mode={used_config.transport_mode}",
            f"--local-ipc-dir={used_config.local_ipc_dir}",
            f"--transport-shm-threshold-bytes={used_config.transport_shm_threshold_bytes}",
            f"--transport-shm-max-payload-bytes={used_config.transport_shm_max_payload_bytes}",
            f"--transport-backend-cooldown-seconds={used_config.transport_backend_cooldown_seconds}",
            f"--transport-backend-cooldown-max-entries={used_config.transport_backend_cooldown_max_entries}",
            f"--transport-backend-cooldown-cleanup-interval={used_config.transport_backend_cooldown_cleanup_interval}",
            f"--host-id={used_config.host_id or ''}",
            f"--uds-path={used_config.uds_path or ''}",
            f"--shm-path={used_config.shm_path or ''}",
            f"--transport-caps={','.join(used_config.transport_caps or ())}",
            f"--creator={creator}",
            f"--uuid={call_uuid}",
        ]
        # TODO: Replace with container backend (docker/containerd) for production.
        env = os.environ.copy()
        # Node-centric membership guardrail:
        # spawned worker containers must not join cluster gossip membership.
        env.pop("FLOWNET_CLUSTER_NODES", None)
        env["FLOWNET_CLUSTER_GOSSIP_ENABLED"] = "0"
        env["FLOWNET_RUNTIME_ROLE"] = "container"
        proc = subprocess.Popen(
            args,
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )

        def stream_output(stream, prefix):
            for line in iter(stream.readline, b""):
                print(f"{prefix} {line.decode().rstrip()}")

        threading.Thread(
            target=stream_output, args=(proc.stdout, f"[{port}][STDOUT]"), daemon=True
        ).start()
        threading.Thread(
            target=stream_output, args=(proc.stderr, f"[{port}][STDERR]"), daemon=True
        ).start()

        try:
            await future
        except Exception:
            if proc.poll() is None:
                proc.terminate()
            self._cleanup_runtime_ipc(used_config)
            raise

        tags: dict[str, str] = {}
        if tenant_id:
            tags["tenant_id"] = tenant_id
        if flow_id:
            tags["flow_id"] = flow_id
        if request_id:
            tags["request_id"] = request_id

        address = f"{host}:{port}"
        record = ContainerRecord(
            address=address,
            pid=proc.pid,
            created_at=time.time(),
            config=used_config,
            process=proc,
            tags=tags,
        )
        with self._lock:
            self._containers[address] = record

        self._sync_local_worker_summary()
        self._publish_event("created", record)
        print(f"[NodeRuntime] Spawned container runtime at {address}")
        return address

    def list_containers(self) -> list[dict[str, object]]:
        # TODO: Enforce auth and user scoping before exposing container metadata.
        with self._lock:
            records = list(self._containers.values())
        result: list[dict[str, object]] = []
        for record in records:
            exit_code = record.process.poll()
            status = "running" if exit_code is None else "exited"
            result.append(
                {
                    "address": record.address,
                    "pid": record.pid,
                    "status": status,
                    "exit_code": exit_code,
                    "created_at": record.created_at,
                    "host": record.config.host,
                    "port": record.config.port,
                    "threads": record.config.threads,
                    "node_id": record.config.node_id,
                    "host_id": record.config.host_id,
                    "uds_path": record.config.uds_path,
                    "shm_path": record.config.shm_path,
                    "transport_caps": list(record.config.transport_caps or ()),
                    "tags": dict(record.tags),
                }
            )
        return result

    def kill_container(self, address: str, timeout: float = 5.0) -> bool:
        # TODO: Enforce auth and user scoping before allowing container termination.
        with self._lock:
            record = self._containers.get(address)
        if record is None:
            return False

        proc = record.process
        if proc.poll() is not None:
            with self._lock:
                self._containers.pop(address, None)
            self._cleanup_runtime_ipc(record.config)
            self._sync_local_worker_summary()
            return False

        proc.terminate()
        try:
            proc.wait(timeout=timeout)
        except subprocess.TimeoutExpired:
            proc.kill()
            proc.wait(timeout=timeout)
        finally:
            with self._lock:
                self._containers.pop(address, None)
            self._cleanup_runtime_ipc(record.config)
            self._sync_local_worker_summary()
        self._publish_event("exited", record)
        return True


__all__ = [
    "ContainerManagerActor",
    "ContainerRecord",
]
