import logging
import os
import platform
from typing import Optional, Iterable

from opentelemetry.metrics import Meter, Observation, CallbackOptions

logger = logging.getLogger(__name__)

_collector: Optional["SystemMetricsCollector"] = None


class SystemMetricsCollector:
    """
    Collects system and process usage metrics via psutil, reported through
    OpenTelemetry observable instruments (gauges / counters).

    Metrics are collected lazily by the SDK's PeriodicExportingMetricReader,
    so no extra background thread is needed.
    """

    def __init__(self, meter: Meter, collect_per_cpu: bool = False):
        import psutil

        self._psutil = psutil
        self._meter = meter
        self._collect_per_cpu = collect_per_cpu
        self._process = psutil.Process(os.getpid())
        self._system = platform.system()
        self._register_system_metrics()
        self._register_process_metrics()
        logger.info("System metrics collector initialised")

    # ------------------------------------------------------------------
    # System-level metrics
    # ------------------------------------------------------------------

    def _register_system_metrics(self):
        self._meter.create_observable_gauge(
            name="system.cpu.utilization",
            callbacks=[self._observe_cpu_utilization],
            unit="%",
            description="System CPU utilization percentage",
        )

        self._meter.create_observable_gauge(
            name="system.cpu.count",
            callbacks=[self._observe_cpu_count],
            unit="{cpus}",
            description="Number of logical CPU cores",
        )

        self._meter.create_observable_gauge(
            name="system.cpu.load_average",
            callbacks=[self._observe_load_average],
            unit="1",
            description="System load average (1m, 5m, 15m)",
        )

        self._meter.create_observable_gauge(
            name="system.memory.usage",
            callbacks=[self._observe_memory_usage],
            unit="By",
            description="System memory usage in bytes",
        )

        self._meter.create_observable_gauge(
            name="system.memory.utilization",
            callbacks=[self._observe_memory_utilization],
            unit="%",
            description="System memory utilization percentage",
        )

        self._meter.create_observable_gauge(
            name="system.swap.usage",
            callbacks=[self._observe_swap_usage],
            unit="By",
            description="System swap usage in bytes",
        )

        self._meter.create_observable_gauge(
            name="system.disk.usage",
            callbacks=[self._observe_disk_usage],
            unit="By",
            description="Disk usage in bytes",
        )

        self._meter.create_observable_gauge(
            name="system.disk.utilization",
            callbacks=[self._observe_disk_utilization],
            unit="%",
            description="Disk utilization percentage",
        )

        self._meter.create_observable_counter(
            name="system.network.io.transmit",
            callbacks=[self._observe_network_bytes_sent],
            unit="By",
            description="Total bytes transmitted over network",
        )

        self._meter.create_observable_counter(
            name="system.network.io.receive",
            callbacks=[self._observe_network_bytes_recv],
            unit="By",
            description="Total bytes received over network",
        )

    # ------------------------------------------------------------------
    # Process-level metrics
    # ------------------------------------------------------------------

    def _register_process_metrics(self):
        self._meter.create_observable_gauge(
            name="process.cpu.utilization",
            callbacks=[self._observe_process_cpu],
            unit="%",
            description="Current process CPU utilization",
        )

        self._meter.create_observable_gauge(
            name="process.memory.rss",
            callbacks=[self._observe_process_rss],
            unit="By",
            description="Resident set size (physical memory) of the process",
        )

        self._meter.create_observable_gauge(
            name="process.memory.vms",
            callbacks=[self._observe_process_vms],
            unit="By",
            description="Virtual memory size of the process",
        )

        self._meter.create_observable_gauge(
            name="process.memory.utilization",
            callbacks=[self._observe_process_memory_pct],
            unit="%",
            description="Process memory utilization as a percentage of total RAM",
        )

        self._meter.create_observable_gauge(
            name="process.thread.count",
            callbacks=[self._observe_process_threads],
            unit="{threads}",
            description="Number of threads in the current process",
        )

        self._meter.create_observable_gauge(
            name="process.open_file_descriptor.count",
            callbacks=[self._observe_process_fds],
            unit="{fds}",
            description="Number of open file descriptors",
        )

        self._meter.create_observable_gauge(
            name="process.uptime",
            callbacks=[self._observe_process_uptime],
            unit="s",
            description="Process uptime in seconds",
        )

    # ------------------------------------------------------------------
    # Callbacks – System
    # ------------------------------------------------------------------

    def _observe_cpu_utilization(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        if self._collect_per_cpu:
            for idx, pct in enumerate(
                self._psutil.cpu_percent(interval=None, percpu=True)
            ):
                yield Observation(pct, {"cpu": idx})
        total = self._psutil.cpu_percent(interval=None)
        yield Observation(total, {"cpu": "total"})

    def _observe_cpu_count(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        logical = self._psutil.cpu_count(logical=True) or 0
        physical = self._psutil.cpu_count(logical=False) or 0
        yield Observation(logical, {"type": "logical"})
        yield Observation(physical, {"type": "physical"})

    def _observe_load_average(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        if self._system == "Windows":
            return
        avg1, avg5, avg15 = os.getloadavg()
        yield Observation(avg1, {"interval": "1m"})
        yield Observation(avg5, {"interval": "5m"})
        yield Observation(avg15, {"interval": "15m"})

    def _observe_memory_usage(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        mem = self._psutil.virtual_memory()
        yield Observation(mem.used, {"state": "used"})
        yield Observation(mem.available, {"state": "available"})
        yield Observation(mem.total, {"state": "total"})
        yield Observation(mem.cached if hasattr(mem, "cached") else 0, {"state": "cached"})
        yield Observation(mem.buffers if hasattr(mem, "buffers") else 0, {"state": "buffers"})

    def _observe_memory_utilization(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        yield Observation(self._psutil.virtual_memory().percent)

    def _observe_swap_usage(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        swap = self._psutil.swap_memory()
        yield Observation(swap.used, {"state": "used"})
        yield Observation(swap.free, {"state": "free"})
        yield Observation(swap.total, {"state": "total"})

    def _observe_disk_usage(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        try:
            disk = self._psutil.disk_usage("/")
            yield Observation(disk.used, {"state": "used", "mountpoint": "/"})
            yield Observation(disk.free, {"state": "free", "mountpoint": "/"})
            yield Observation(disk.total, {"state": "total", "mountpoint": "/"})
        except OSError:
            pass

    def _observe_disk_utilization(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        try:
            yield Observation(
                self._psutil.disk_usage("/").percent, {"mountpoint": "/"}
            )
        except OSError:
            pass

    def _observe_network_bytes_sent(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        counters = self._psutil.net_io_counters()
        if counters:
            yield Observation(counters.bytes_sent)

    def _observe_network_bytes_recv(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        counters = self._psutil.net_io_counters()
        if counters:
            yield Observation(counters.bytes_recv)

    # ------------------------------------------------------------------
    # Callbacks – Process
    # ------------------------------------------------------------------

    def _observe_process_cpu(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        try:
            yield Observation(self._process.cpu_percent(interval=None))
        except self._psutil.NoSuchProcess:
            pass

    def _observe_process_rss(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        try:
            yield Observation(self._process.memory_info().rss)
        except self._psutil.NoSuchProcess:
            pass

    def _observe_process_vms(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        try:
            yield Observation(self._process.memory_info().vms)
        except self._psutil.NoSuchProcess:
            pass

    def _observe_process_memory_pct(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        try:
            yield Observation(self._process.memory_percent())
        except self._psutil.NoSuchProcess:
            pass

    def _observe_process_threads(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        try:
            yield Observation(self._process.num_threads())
        except self._psutil.NoSuchProcess:
            pass

    def _observe_process_fds(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        try:
            if self._system != "Windows":
                yield Observation(self._process.num_fds())
        except (self._psutil.NoSuchProcess, AttributeError):
            pass

    def _observe_process_uptime(
        self, options: CallbackOptions
    ) -> Iterable[Observation]:
        try:
            import time

            yield Observation(time.time() - self._process.create_time())
        except self._psutil.NoSuchProcess:
            pass


def start_system_metrics(
    meter: Meter,
    collect_per_cpu: bool = False,
) -> "SystemMetricsCollector":
    """
    Create and start the system metrics collector.

    Args:
        meter: OpenTelemetry Meter to register instruments on.
        collect_per_cpu: If True, emit per-core CPU utilization in addition to
            the aggregate value.

    Returns:
        The initialised SystemMetricsCollector instance.
    """
    global _collector
    if _collector is not None:
        logger.debug("System metrics collector already running")
        return _collector

    _collector = SystemMetricsCollector(meter, collect_per_cpu=collect_per_cpu)
    return _collector


def get_system_metrics_collector() -> Optional["SystemMetricsCollector"]:
    """Return the active collector, or None if not started."""
    return _collector
