from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path
from typing import Final

import yaml

from k1link.simulation.contracts import ControlProfile
from k1link.simulation.process_supervisor import ProcessSpec
from k1link.simulation.provider_contract import (
    ProviderRole,
    SimulationClockDescriptor,
    SimulationProviderDescriptor,
    SimulationProviderProfile,
)

LIFECYCLE_PROFILE_SCHEMA: Final = "missioncore.stock-rover-lifecycle/v1"
EXPECTED_D_ROOT: Final = Path("/mnt/d/NDC_MISSIONCORE/simulation")
GAZEBO_STATE_PROVIDER_ID: Final = "gazebo"
PX4_COMMAND_PROVIDER_ID: Final = "px4-ros2-offboard"
STOCK_ROVER_PROVIDER_PROFILE: Final = SimulationProviderProfile(
    profile_id="stock-rover-gazebo-px4-s1d",
    providers=(
        SimulationProviderDescriptor(
            provider_id=GAZEBO_STATE_PROVIDER_ID,
            roles=(
                ProviderRole.WORLD,
                ProviderRole.PHYSICS,
                ProviderRole.STATE,
                ProviderRole.SENSOR,
            ),
            capabilities=(
                "clock.simulation",
                "state.vehicle-pose",
                "truth.ground-truth",
                "sensor.virtual",
            ),
        ),
        SimulationProviderDescriptor(
            provider_id=PX4_COMMAND_PROVIDER_ID,
            roles=(ProviderRole.CONTROLLER,),
            capabilities=("command.rover-speed-steering/v1",),
        ),
        SimulationProviderDescriptor(
            provider_id="micro-xrce-dds-agent",
            roles=(ProviderRole.TRANSPORT,),
            capabilities=("transport.ros2",),
        ),
    ),
    clock=SimulationClockDescriptor(
        provider_id=GAZEBO_STATE_PROVIDER_ID,
        domain="gazebo:/clock",
    ),
    control_profiles=(ControlProfile.ROVER_SPEED_STEERING_V1,),
)


class StockRoverProfileError(ValueError):
    """The reviewed stock-rover lifecycle profile is unsafe or incomplete."""


@dataclass(frozen=True, slots=True)
class StockRoverLifecycleProfile:
    profile_id: str
    speed_factor: int
    dwell_seconds: float
    agent_ready_log_patterns: tuple[str, ...]
    px4_ready_log_patterns: tuple[str, ...]


@dataclass(frozen=True, slots=True)
class StockRoverTargetPaths:
    d_root: Path
    px4_root: Path
    agent_prefix: Path
    process_home: Path
    xdg_cache_home: Path

    @classmethod
    def from_d_root(cls, d_root: Path) -> StockRoverTargetPaths:
        root = d_root.expanduser().resolve()
        if root != EXPECTED_D_ROOT:
            raise StockRoverProfileError(
                f"stock-rover lifecycle requires exact D root {EXPECTED_D_ROOT}"
            )
        return cls(
            d_root=root,
            px4_root=root / "source/PX4-Autopilot",
            agent_prefix=root / "runtime/micro-xrce-dds-agent/2.4.3",
            process_home=root / "runtime/s1/home",
            xdg_cache_home=root / "cache/s1/xdg",
        )

    @property
    def agent_binary(self) -> Path:
        return self.agent_prefix / "bin/MicroXRCEAgent"

    @property
    def scenario_path(self) -> Path:
        return self.px4_root / "Tools/simulation/gz/worlds/rover.sdf"


def load_stock_rover_lifecycle_profile(path: Path) -> StockRoverLifecycleProfile:
    try:
        document = yaml.safe_load(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeDecodeError, yaml.YAMLError) as exc:
        raise StockRoverProfileError("stock-rover profile is not valid UTF-8 YAML") from exc
    if not isinstance(document, dict) or any(not isinstance(key, str) for key in document):
        raise StockRoverProfileError("stock-rover profile must be a YAML mapping")
    expected_keys = {
        "schema_version",
        "profile_id",
        "speed_factor",
        "dwell_seconds",
        "authority",
        "agent_ready_log_patterns",
        "px4_ready_log_patterns",
    }
    if set(document) != expected_keys:
        raise StockRoverProfileError("stock-rover profile keys do not match the v1 schema")
    if document["schema_version"] != LIFECYCLE_PROFILE_SCHEMA:
        raise StockRoverProfileError("stock-rover profile schema is incompatible")
    authority = document["authority"]
    if authority != {
        "simulation_or_shadow_only": True,
        "actuator_authority": False,
        "navigation_or_safety_accepted": False,
        "direct_actuator_setpoints_allowed": False,
    }:
        raise StockRoverProfileError("stock-rover lifecycle cannot grant actuator authority")
    profile_id = document["profile_id"]
    speed_factor = document["speed_factor"]
    dwell_seconds = document["dwell_seconds"]
    if not isinstance(profile_id, str) or not profile_id:
        raise StockRoverProfileError("stock-rover profile id must be nonempty")
    if speed_factor != 1:
        raise StockRoverProfileError("S1B lifecycle admits only speed factor 1")
    if not isinstance(dwell_seconds, int | float) or isinstance(dwell_seconds, bool):
        raise StockRoverProfileError("stock-rover dwell must be numeric")
    if not 1 <= float(dwell_seconds) <= 30:
        raise StockRoverProfileError("stock-rover dwell must be between 1 and 30 seconds")
    return StockRoverLifecycleProfile(
        profile_id=profile_id,
        speed_factor=speed_factor,
        dwell_seconds=float(dwell_seconds),
        agent_ready_log_patterns=_patterns(
            document["agent_ready_log_patterns"],
            "agent",
        ),
        px4_ready_log_patterns=_patterns(
            document["px4_ready_log_patterns"],
            "PX4",
        ),
    )


def stock_rover_process_environment(
    paths: StockRoverTargetPaths,
) -> dict[str, str]:
    paths.process_home.mkdir(mode=0o700, parents=True, exist_ok=True)
    paths.xdg_cache_home.mkdir(mode=0o700, parents=True, exist_ok=True)
    return {
        "HOME": str(paths.process_home),
        "LANG": os.environ.get("LANG", "C.UTF-8"),
        "LOGNAME": "missioncore",
        "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
        "USER": "missioncore",
        "XDG_CACHE_HOME": str(paths.xdg_cache_home),
    }


def stock_rover_process_specs(
    paths: StockRoverTargetPaths,
    profile: StockRoverLifecycleProfile,
) -> tuple[ProcessSpec, ...]:
    return (
        ProcessSpec(
            process_id="micro-xrce-dds-agent",
            argv=(
                str(paths.agent_binary),
                "udp4",
                "-p",
                "8888",
                "-v",
                "4",
            ),
            start_order=10,
            shutdown_order=30,
            environment=(("LD_LIBRARY_PATH", str(paths.agent_prefix / "lib")),),
            startup_grace_seconds=0,
            ready_log_patterns=profile.agent_ready_log_patterns,
            health_timeout_seconds=15,
            health_poll_interval_seconds=0.1,
            interrupt_timeout_seconds=3,
            terminate_timeout_seconds=2,
        ),
        ProcessSpec(
            process_id="px4-gazebo-stock-rover",
            argv=(
                "/usr/bin/make",
                "-C",
                str(paths.px4_root),
                "px4_sitl",
                "gz_rover_ackermann",
            ),
            start_order=20,
            shutdown_order=40,
            environment=(
                ("HEADLESS", "1"),
                ("PX4_SIM_SPEED_FACTOR", str(profile.speed_factor)),
            ),
            startup_grace_seconds=0,
            ready_log_patterns=profile.px4_ready_log_patterns,
            health_timeout_seconds=120,
            health_poll_interval_seconds=0.25,
            keep_stdin_open=True,
            interrupt_timeout_seconds=10,
            terminate_timeout_seconds=5,
        ),
    )


def _patterns(value: object, label: str) -> tuple[str, ...]:
    if (
        not isinstance(value, list)
        or not value
        or any(not isinstance(item, str) or not item or "\x00" in item for item in value)
    ):
        raise StockRoverProfileError(f"{label} ready patterns must be nonempty strings")
    patterns = tuple(value)
    if len(patterns) != len(set(patterns)):
        raise StockRoverProfileError(f"{label} ready patterns must be unique")
    return patterns
