#!/usr/bin/env python3
"""CPU monitoring functionality."""

import platform
import re
from dataclasses import dataclass
from typing import ClassVar, Dict, List, Tuple

import psutil


@dataclass
class CPUStats:
    """CPU statistics."""

    # CPU identification
    vendor: str  # CPU vendor (e.g., Intel, AMD)
    model: str  # CPU model name
    # Overall CPU utilization percentage
    total_percent: float
    # Per-core CPU utilization percentages
    core_percents: List[float]
    # Frequency information
    current_freq: float  # Current frequency in MHz
    min_freq: float  # Minimum frequency in MHz
    max_freq: float  # Maximum frequency in MHz
    # Temperature if available
    temperature: float  # CPU temperature in Celsius
    # Load averages
    load_1min: float
    load_5min: float
    load_15min: float

    _VENDOR_KEYS: ClassVar[Tuple[str, ...]] = (
        "vendor_id",
        "vendor",
        "model vendor",
        "cpu implementer",
        "system type",
        "platform",
    )
    _MODEL_KEYS: ClassVar[Tuple[str, ...]] = (
        "model name",
        "cpu model",
        "hardware",
        "processor",
        "model",
        "cpu",
    )
    _ARM_IMPLEMENTER_VENDOR_MAP: ClassVar[Dict[int, str]] = {
        # Synced with util-linux lscpu ARM vendor mapping.
        0x41: "ARM",
        0x42: "Broadcom",
        0x43: "Cavium",
        0x44: "DEC",
        0x46: "Fujitsu",
        0x48: "HiSilicon",
        0x49: "Infineon",
        0x4D: "Motorola",
        0x4E: "NVIDIA",
        0x50: "APM",
        0x51: "Qualcomm",
        0x53: "Samsung",
        0x56: "Marvell",
        0x61: "Apple",
        0x66: "Faraday",
        0x69: "Intel",
        0x70: "Phytium",
        0xC0: "Ampere",
    }

    @staticmethod
    def _parse_cpuinfo(cpuinfo: str) -> Dict[str, str]:
        """Parse /proc/cpuinfo into normalized first-seen key/value pairs."""
        parsed: Dict[str, str] = {}
        for line in cpuinfo.splitlines():
            if ":" not in line:
                continue
            key, value = line.split(":", 1)
            normalized_key = key.strip().lower()
            normalized_value = value.strip()
            if not normalized_key or not normalized_value:
                continue
            parsed.setdefault(normalized_key, normalized_value)
        return parsed

    @staticmethod
    def _map_cpu_implementer_vendor(implementer: str) -> str:
        """Map ARM CPU implementer IDs to vendor labels."""
        raw_value = implementer.strip()
        try:
            code = int(raw_value, 0)
        except ValueError:
            return raw_value or "Unknown"

        vendor = CPUStats._ARM_IMPLEMENTER_VENDOR_MAP.get(code)
        if vendor:
            return vendor
        return f"Unknown ({raw_value})"

    @staticmethod
    def _looks_like_model(value: str) -> bool:
        """Filter out numeric-only processor index values."""
        return bool(value) and not re.fullmatch(r"\d+", value)

    @staticmethod
    def _extract_identity(cpuinfo: str, fallback_model: str) -> Tuple[str, str]:
        """Extract vendor/model from cpuinfo with cross-architecture key support."""
        fields = CPUStats._parse_cpuinfo(cpuinfo)

        vendor = ""
        for key in CPUStats._VENDOR_KEYS:
            value = fields.get(key, "")
            if not value:
                continue
            if key == "cpu implementer":
                vendor = CPUStats._map_cpu_implementer_vendor(value)
            else:
                vendor = value
            break
        if not vendor:
            vendor = "Unknown"

        model = ""
        for key in CPUStats._MODEL_KEYS:
            value = fields.get(key, "")
            if not value:
                continue
            if key == "processor" and not CPUStats._looks_like_model(value):
                continue
            model = value
            break

        if not model:
            architecture = fields.get("cpu architecture", "")
            cpu_part = fields.get("cpu part", "")
            if architecture and cpu_part:
                model = f"{architecture} part {cpu_part}"

        if not model:
            model = fallback_model or "Unknown"

        return vendor, model

    @staticmethod
    def get_stats() -> "CPUStats":
        """Get current CPU statistics.

        Returns:
            CPUStats object with current CPU information
        """
        # Get CPU utilization
        # Use single call for per-core data to avoid race condition
        # Calculate total from cores to ensure consistency
        core_percents = psutil.cpu_percent(interval=None, percpu=True)
        total_percent = (
            sum(core_percents) / len(core_percents) if core_percents else 0.0
        )

        # Get CPU frequency
        freq = psutil.cpu_freq(percpu=False)
        if freq:
            # Handle potential None values in frequency attributes
            current_freq = freq.current if freq.current is not None else 0.0
            min_freq = freq.min if freq.min is not None else 0.0
            max_freq = freq.max if freq.max is not None else 0.0
        else:
            current_freq = min_freq = max_freq = 0.0

        # Get CPU temperature if available
        try:
            temps = psutil.sensors_temperatures()
            # Try common temperature sensor names
            for name in ["coretemp", "k10temp", "cpu_thermal"]:
                if name in temps and temps[name]:
                    temperature = temps[name][0].current
                    break
            else:
                temperature = 0.0
        except (AttributeError, KeyError):
            temperature = 0.0

        # Get load averages normalized by CPU count
        try:
            # Get CPU count first
            cpu_count = psutil.cpu_count(logical=True)  # Include logical cores
            # Proper None check - psutil.cpu_count() can return None
            if cpu_count is None or cpu_count < 1:
                cpu_count = 1

            # Get load averages
            raw_loads = psutil.getloadavg()
            if not raw_loads or len(raw_loads) != 3:
                load_1 = load_5 = load_15 = 0.0
            else:
                # Normalize by CPU count and ensure non-negative values
                load_1, load_5, load_15 = [max(0.0, x / cpu_count) for x in raw_loads]
        except Exception:
            load_1 = load_5 = load_15 = 0.0

        # Get CPU vendor and model information
        cpu_info = platform.processor()
        fallback_model = cpu_info or "Unknown"

        # Try to get more detailed info on Linux
        try:
            with open("/proc/cpuinfo", "r", encoding="utf-8", errors="ignore") as f:
                cpuinfo = f.read()
            vendor, model = CPUStats._extract_identity(cpuinfo, fallback_model)
        except (IOError, OSError):
            # Fallback to platform.processor()
            vendor = "Unknown"
            model = fallback_model

        return CPUStats(
            vendor=vendor,
            model=model,
            total_percent=total_percent,
            core_percents=core_percents,
            current_freq=current_freq,
            min_freq=min_freq,
            max_freq=max_freq,
            temperature=temperature,
            load_1min=load_1,
            load_5min=load_5,
            load_15min=load_15,
        )
