"""
Enhanced enumeration classes and utility functions for CausalIQ Core.

This module provides extended enumeration functionality, mathematical
utilities, and other utility classes commonly used across the CausalIQ
ecosystem.
"""

import json
from enum import Enum
from json import dump, load
from math import floor
from pathlib import Path
from platform import uname
from time import time
from typing import Any, Dict, Optional, Union

import numpy as np
from cpuinfo import get_cpu_info  # type: ignore[import-untyped]
from platformdirs import user_cache_dir
from psutil import virtual_memory


class EnumWithAttrs(Enum):
    """
    Base class for enumerations with additional read-only attributes.

    This class extends the standard Python Enum to support enums that carry
    additional attributes such as human-readable labels. Sub-classes can
    extend this pattern to include more attributes.

    Example:
        >>> class Status(EnumWithAttrs):
        ...     PENDING = 'pending', 'Pending Review'
        ...     APPROVED = 'approved', 'Approved for Use'
        ...     REJECTED = 'rejected', 'Rejected - Needs Changes'
        >>>
        >>> print(Status.PENDING)  # 'pending'
        >>> print(Status.PENDING.label)  # 'Pending Review'

    Note:
        Values should be set as tuples where the first element is the enum
        value and subsequent elements are the additional attributes.
        The base class provides a `label` attribute from the second tuple
        element.
    """

    def __new__(cls, *args: Any, **kwargs: Any) -> "EnumWithAttrs":
        """
        Create a new enum instance with additional attributes.

        Args:
            *args: The enum value and additional attribute values
            **kwargs: Additional keyword arguments (unused)

        Returns:
            New enum instance with the value set to the first argument
        """
        obj = object.__new__(cls)
        obj._value_ = args[0]
        return obj

    def __init__(self, _: str, label: str) -> None:
        """
        Initialise the enum instance with a label attribute.

        Args:
            _: The enum value (already set in __new__)
            label: Human-readable label for this enum value
        """
        self._label_ = label

    def __str__(self) -> str:
        """
        Return the string representation of the enum value.

        Returns:
            The enum's value as a string
        """
        return str(self.value)

    @property
    def label(self) -> str:
        """
        Get the human-readable label for this enum value.

        Returns:
            The label string for this enum value
        """
        return self._label_


def environment(cache_dir: Optional[str] = None) -> Dict[str, Any]:
    """
    Obtain details of the hardware and software environment.

    For efficiency, this is obtained from a cached file "environment.json"
    if one modified in the last 24 hours is available, otherwise the OS
    is queried and a new cache file created.

    :param str cache_dir: Optional cache directory. If None, uses standard
                         user cache directory for the platform.

    :returns dict: Environment information including os, cpu, python, ram
    """
    if cache_dir is None:
        cache_dir = user_cache_dir("causaliq-core", "causaliq")

    cache_path = Path(cache_dir)
    cache_path.mkdir(parents=True, exist_ok=True)

    envfile_path = cache_path / "environment.json"

    # Check if cache exists and is fresh (< 24 hours old)
    if (
        not envfile_path.exists()
        or time() - envfile_path.stat().st_mtime > 24 * 3600
    ):
        # Query OS for fresh environment data
        env = {
            "os": uname().system + " v" + uname().version,
            "cpu": get_cpu_info()["brand_raw"],
            "python": get_cpu_info()["python_version"],
            "ram": round(virtual_memory().total / (1024 * 1024 * 1024)),
        }

        # Cache the results
        with open(envfile_path, "w") as file:
            dump(env, file)
    else:
        # Load from cache
        try:
            with open(envfile_path, "r") as file:
                env = load(file)
        except (FileNotFoundError, PermissionError, json.JSONDecodeError):
            # If cache is corrupted or inaccessible, query fresh data
            env = {
                "os": uname().system + " v" + uname().version,
                "cpu": get_cpu_info()["brand_raw"],
                "python": get_cpu_info()["python_version"],
                "ram": round(virtual_memory().total / (1024 * 1024 * 1024)),
            }

            # Try to cache the results
            try:
                with open(envfile_path, "w") as file:
                    dump(env, file)
            except (PermissionError, OSError):
                # If we can't write cache, just return the data
                pass

    return env


# Mathematical utility functions (migrated from math.py)


def rndsf(x: Union[int, float], sf: int, zero: Optional[float] = None) -> str:
    """
    Round number to specified significant figures.

    Args:
        x: Number to round
        sf: Number of significant figures (2-10)
        zero: Optional zero threshold (default: 10^-sf)

    Returns:
        Formatted string representation with specified significant figures

    Raises:
        TypeError: If arguments have invalid types
        ValueError: If arguments have invalid values

    Examples:
        >>> rndsf(1.234567, 3)
        '1.23'
        >>> rndsf(0.001234, 3)
        '0.00123'
        >>> rndsf(1234567, 3)
        '1230000'
    """
    if (
        not isinstance(x, (float, int))
        or isinstance(x, bool)
        or not isinstance(sf, int)
        or isinstance(sf, bool)
        or (zero is not None and not isinstance(zero, float))
    ):
        raise TypeError("rndsf bad arg types")

    zero = zero if zero is not None else 10 ** (-sf)
    if sf < 2 or sf > 10 or zero < 10**-20 or zero > 0.1:
        raise ValueError("rndsf bad arg values")
    if -zero < x < zero:
        return "0.0"

    exp = int(floor(np.log10(abs(x))))
    x_rounded = round(x, sf - exp - 1)
    result_str = "{:.{}f}".format(x_rounded, max(1, sf - exp - 1))
    result_str = (
        result_str if result_str.endswith(".0") else result_str.rstrip("0")
    )
    result_str = result_str + "0" if result_str.endswith(".") else result_str
    return result_str


def ln(x: float, base: Union[int, str] = "e") -> float:
    """
    Return logarithm to specified base.

    Args:
        x: Number to obtain logarithm of
        base: Base to use - 2, 10, or 'e' for natural logarithm

    Returns:
        Logarithm of x to the specified base

    Raises:
        TypeError: If arguments have invalid types
        ValueError: If arguments have invalid values

    Example:
        >>> ln(10, 10)
        1.0
        >>> ln(8, 2)
        3.0
        >>> ln(2.718281828459045)  # e
        1.0
    """
    if (
        not isinstance(base, (str, int))
        or isinstance(base, bool)
        or not isinstance(x, (float, int))
        or isinstance(x, bool)
    ):
        raise TypeError("ln bad argument type")

    if base not in [2, 10, "e"]:
        raise ValueError("ln bad argument value")

    if base == 2:
        return float(np.log2(x))
    elif base == 10:
        return float(np.log10(x))
    else:  # base == 'e'
        return float(np.log(x))


__all__ = [
    "EnumWithAttrs",
    "environment",
    "rndsf",
    "ln",
]
