"""Random iperf3 traffic generation between node pairs.

Spawns iperf3 clients as background processes in containers. The iperf3
server is already running in each container (started by the Dockerfile
entrypoint).
"""

from __future__ import annotations

import json
import logging
import random
import time
from dataclasses import dataclass, field

from .docker_exec import docker_exec_quiet
from .scenario import TrafficConfig
from .topology import SimTopology

log = logging.getLogger(__name__)


@dataclass
class TrafficSession:
    client_node: str
    server_node: str
    started_at: float
    duration_secs: int
    container: str
    result_file: str = ""


class TrafficManager:
    """Manages random iperf3 sessions across the mesh."""

    def __init__(
        self,
        topology: SimTopology,
        config: TrafficConfig,
        rng: random.Random,
        down_nodes: set[str] | None = None,
        npub_cache: dict[str, str] | None = None,
    ):
        self.topology = topology
        self.config = config
        self.rng = rng
        self.down_nodes = down_nodes or set()
        self.npub_cache = npub_cache or {}
        self.active_sessions: list[TrafficSession] = []
        self.completed_results: list[dict] = []

    @property
    def active_count(self) -> int:
        return len(self.active_sessions)

    def maybe_spawn(self):
        """Spawn a new iperf3 session if under the concurrency limit."""
        if self.active_count >= self.config.max_concurrent:
            log.debug(
                "At max_concurrent (%d), skipping traffic spawn",
                self.config.max_concurrent,
            )
            return

        node_ids = [nid for nid in self.topology.nodes if nid not in self.down_nodes]
        if len(node_ids) < 2:
            return

        # Pick random client and server (different nodes, both up)
        client, server = self.rng.sample(node_ids, 2)
        server_npub = self.npub_cache.get(server, self.topology.nodes[server].npub)
        container = self.topology.container_name(client)

        duration = int(
            self.rng.uniform(
                self.config.duration_secs.min,
                self.config.duration_secs.max,
            )
        )
        streams = self.config.parallel_streams

        # Result file inside the container for JSON capture
        ts = int(time.time())
        result_file = f"/tmp/iperf3-{client}-{server}-{ts}.json"

        # Start iperf3 in background with JSON output
        cmd = (
            f"nohup iperf3 -c {server_npub}.fips -t {duration} "
            f"-P {streams} --json > {result_file} 2>&1 &"
        )
        result = docker_exec_quiet(container, cmd)
        if result is not None:
            session = TrafficSession(
                client_node=client,
                server_node=server,
                started_at=time.time(),
                duration_secs=duration,
                container=container,
                result_file=result_file,
            )
            self.active_sessions.append(session)
            log.info(
                "Traffic: %s -> %s (%ds, %d streams)",
                client,
                server,
                duration,
                streams,
            )
        else:
            log.warning("Failed to start iperf3 on %s", container)

    def cleanup_expired(self):
        """Remove sessions that have completed (based on time)."""
        now = time.time()
        grace = 5  # seconds after expected completion
        still_active = []
        for s in self.active_sessions:
            if now - s.started_at >= s.duration_secs + grace:
                self._collect_result(s)
            else:
                still_active.append(s)
        removed = len(self.active_sessions) - len(still_active)
        self.active_sessions = still_active
        if removed > 0:
            log.debug("Cleaned up %d expired traffic sessions", removed)

    def _collect_result(self, session: TrafficSession):
        """Retrieve iperf3 JSON result from container."""
        if not session.result_file:
            return
        if session.client_node in self.down_nodes:
            return
        stdout = docker_exec_quiet(
            session.container,
            f"cat {session.result_file} 2>/dev/null; rm -f {session.result_file}",
        )
        if stdout is None:
            return
        try:
            data = json.loads(stdout.strip())
        except (json.JSONDecodeError, ValueError):
            log.debug("Could not parse iperf3 result for %s -> %s",
                       session.client_node, session.server_node)
            return
        data["_meta"] = {
            "client": session.client_node,
            "server": session.server_node,
            "duration_secs": session.duration_secs,
        }
        self.completed_results.append(data)

    def collect_results(self) -> list[dict]:
        """Return all completed iperf3 JSON results."""
        # Collect any remaining active sessions that may have finished
        for s in self.active_sessions:
            self._collect_result(s)
        return self.completed_results

    def stop_all(self):
        """Kill all iperf3 client processes in running containers."""
        seen = set()
        for session in self.active_sessions:
            if session.container not in seen:
                if session.client_node not in self.down_nodes:
                    docker_exec_quiet(
                        session.container,
                        "killall iperf3 2>/dev/null; true",
                    )
                seen.add(session.container)
        # Collect results before clearing (iperf3 writes partial JSON on kill)
        for s in self.active_sessions:
            self._collect_result(s)
        self.active_sessions.clear()
