# engine_runtime.py - Production-ready runtime-aware resource management

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, Tuple, Any, Set
import os
import math
import pathlib
import platform
import logging
import time
from functools import lru_cache

log = logging.getLogger(__name__)

# ============================================================================
# RUNTIME PROFILES & DETECTION
# ============================================================================


class RuntimeProfile(str, Enum):
    """Deployment environment profiles."""

    DESKTOP = "desktop"  # Single-tenant user machine
    SERVER_SHARED = "server_shared"  # Multi-tenant API worker
    SERVER_DEDICATED = "server_dedicated"  # Dedicated batch/ETL node
    WEB_APP = "web_app"  # Browser-facing requests
    SERVERLESS = "serverless"  # Lambda/Cloud Run/Functions
    GSHEETS = "gsheets"  # Google Sheets add-on


class ConcurrencyModel(str, Enum):
    """How to parallelize work."""

    PROCESS = "process"  # Multiprocessing only
    THREAD = "thread"  # Threading only (no fork/spawn)
    HYBRID = "hybrid"  # Processes with RF threads


@dataclass
class MachineQuota:
    """Detected resource quotas from cgroups/system."""

    cpu_quota_cores: Optional[float] = None
    cpu_set_cores: Optional[int] = None
    mem_limit_bytes: Optional[int] = None
    detected_method: str = "none"
    profile_source: str = "auto"  # "auto", "env_override", "cgroups", "default"


@dataclass
class ResourcePolicy:
    """Complete resource management policy for a runtime profile."""

    # Concurrency
    concurrency_model: ConcurrencyModel
    max_workers: int
    rf_threads_per_proc: int

    # Memory management - Using Optional[int] for disabled thresholds
    mem_fraction: float
    mm_rows_threshold: Optional[int]  # None = disabled
    mm_ops_threshold: Optional[int]  # None = disabled
    speed_max_rows: int
    chunk_size: int

    # Disk & cache
    enable_cache: bool
    enable_mm_files: bool
    tmp_dir: Optional[str]

    # Progress & monitoring
    progress_mode: str  # "items", "blocks", "percentage"
    enable_telemetry: bool

    # Time management
    max_execution_time: Optional[int] = None
    soft_timeout_fraction: float = 0.8  # Start degrading at 80% of time

    # Degradation steps when under pressure
    degrade_steps: Tuple[str, ...] = field(
        default_factory=lambda: (
            "disable_soft_widen",
            "reduce_algorithms",
            "lower_parallel_threshold",
            "sample_large_blocks",
        )
    )


# ============================================================================
# QUOTA DETECTION
# ============================================================================


@lru_cache(maxsize=1)
def read_cgroup_quotas() -> MachineQuota:
    """
    Detect CPU and memory quotas from cgroups v1/v2.
    Enhanced with sched_getaffinity for better CPU detection.
    """
    quota = MachineQuota()

    # CPU Quota Detection
    try:
        # cgroups v2
        cpu_max_path = pathlib.Path("/sys/fs/cgroup/cpu.max")
        if cpu_max_path.exists():
            q, p = cpu_max_path.read_text().strip().split()
            if q != "max":
                quota.cpu_quota_cores = float(q) / float(p)
                quota.detected_method = "cgroups_v2"
        else:
            # cgroups v1
            quota_path = pathlib.Path("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")
            period_path = pathlib.Path("/sys/fs/cgroup/cpu/cpu.cfs_period_us")
            if quota_path.exists() and period_path.exists():
                q = int(quota_path.read_text().strip())
                p = int(period_path.read_text().strip())
                if q > 0 and p > 0:
                    quota.cpu_quota_cores = q / p
                    quota.detected_method = "cgroups_v1"
    except Exception as e:
        log.debug(f"CPU quota detection failed: {e}")

    # CPU Set Detection (allowed cores)
    try:
        # Try sched_getaffinity first (most accurate)
        affinity_cores = len(os.sched_getaffinity(0))
        quota.cpu_set_cores = affinity_cores
        if quota.detected_method == "none":
            quota.detected_method = "sched_affinity"
    except (AttributeError, OSError):
        # Fall back to cpuset parsing
        try:
            cpuset_path = pathlib.Path("/sys/fs/cgroup/cpuset/cpuset.cpus")
            if not cpuset_path.exists():
                cpuset_path = pathlib.Path("/sys/fs/cgroup/cpuset.cpus")

            if cpuset_path.exists():
                cpuset_str = cpuset_path.read_text().strip()
                # Parse ranges like "0-3,7-8" -> count = 6
                core_count = 0
                for part in cpuset_str.split(","):
                    if "-" in part:
                        start, end = map(int, part.split("-"))
                        core_count += end - start + 1
                    else:
                        core_count += 1
                quota.cpu_set_cores = core_count
                if quota.detected_method == "none":
                    quota.detected_method = "cpuset"
        except Exception as e:
            log.debug(f"CPU set detection failed: {e}")

    # Memory Limit Detection
    try:
        # cgroups v2
        mem_max_path = pathlib.Path("/sys/fs/cgroup/memory.max")
        if mem_max_path.exists():
            v = mem_max_path.read_text().strip()
            if v != "max":
                quota.mem_limit_bytes = int(v)
        else:
            # cgroups v1
            mem_limit_path = pathlib.Path("/sys/fs/cgroup/memory/memory.limit_in_bytes")
            if mem_limit_path.exists():
                v = int(mem_limit_path.read_text().strip())
                # Check for "unlimited" (huge values)
                if v < (1 << 60):
                    quota.mem_limit_bytes = v
    except Exception as e:
        log.debug(f"Memory limit detection failed: {e}")

    return quota


# ============================================================================
# PROFILE DETECTION
# ============================================================================


def detect_runtime_profile() -> Tuple[RuntimeProfile, str]:
    """
    Auto-detect the runtime environment.
    Returns (profile, source) where source is how it was detected.
    """
    # 1. Check explicit override
    env_profile = os.getenv("FM_RUNTIME", "").lower()
    if env_profile:
        try:
            return RuntimeProfile(env_profile), "env_override"
        except ValueError:
            log.warning(f"Invalid FM_RUNTIME value: {env_profile}")

    # 2. Google Sheets detection
    if os.getenv("GOOGLE_SHEETS_ADDON") or os.getenv("FM_GSHEETS"):
        return RuntimeProfile.GSHEETS, "env_detection"

    # 3. Serverless detection
    if any(
        os.getenv(k)
        for k in [
            "AWS_LAMBDA_FUNCTION_NAME",
            "FUNCTION_TARGET",
            "K_SERVICE",
            "FUNCTIONS_SIGNATURE_TYPE",
        ]
    ):
        return RuntimeProfile.SERVERLESS, "env_detection"

    # 4. Web app detection
    if os.getenv("FM_WEB_APP") or os.getenv("WEB_CONCURRENCY"):
        return RuntimeProfile.WEB_APP, "env_detection"

    # 5. Container/Kubernetes detection
    quotas = read_cgroup_quotas()
    if quotas.detected_method != "none" or os.getenv("KUBERNETES_SERVICE_HOST"):
        # Differentiate between shared and dedicated
        if quotas.cpu_quota_cores and quotas.cpu_quota_cores > 4:
            return RuntimeProfile.SERVER_DEDICATED, "cgroups"
        return RuntimeProfile.SERVER_SHARED, "cgroups"

    # 6. Default to desktop
    return RuntimeProfile.DESKTOP, "default"


# ============================================================================
# POLICY DERIVATION
# ============================================================================


def derive_policy(
    profile: Optional[RuntimeProfile] = None, profile_source: Optional[str] = None
) -> ResourcePolicy:
    """
    Derive resource policy for the given or detected profile.
    Applies environment variable overrides with sanity checks.
    """
    if profile is None:
        profile, profile_source = detect_runtime_profile()

    # Get machine quotas for informed decisions
    quotas = read_cgroup_quotas()
    quotas.profile_source = profile_source or "unknown"

    try:
        import psutil

        physical_cpus = os.cpu_count() or 1
        mem_total = psutil.virtual_memory().total
        mem_available = psutil.virtual_memory().available
    except ImportError:
        physical_cpus = os.cpu_count() or 1
        mem_total = 8 * (1024**3)  # Assume 8GB if psutil unavailable
        mem_available = mem_total // 2

    # Effective CPU calculation with sched_getaffinity
    if quotas.cpu_quota_cores:
        effective_cpus = max(1, math.floor(quotas.cpu_quota_cores))
    elif quotas.cpu_set_cores:
        effective_cpus = quotas.cpu_set_cores
    else:
        effective_cpus = physical_cpus

    # Try sched_getaffinity for most accurate count
    try:
        effective_cpus = min(effective_cpus, len(os.sched_getaffinity(0)))
    except (AttributeError, OSError):
        pass

    # Effective memory
    effective_mem = quotas.mem_limit_bytes or mem_total

    # Base policies by profile
    if profile == RuntimeProfile.DESKTOP:
        policy = ResourcePolicy(
            concurrency_model=ConcurrencyModel.HYBRID,
            max_workers=min(4, max(1, effective_cpus - 1)),
            rf_threads_per_proc=1,
            mem_fraction=0.70,
            mm_rows_threshold=1_500_000,
            mm_ops_threshold=50_000_000,
            speed_max_rows=1_000_000,
            chunk_size=50_000,
            enable_cache=True,
            enable_mm_files=True,
            tmp_dir=None,
            progress_mode="blocks",
            enable_telemetry=False,
            max_execution_time=None,
        )

    elif profile == RuntimeProfile.SERVER_SHARED:
        policy = ResourcePolicy(
            concurrency_model=ConcurrencyModel.HYBRID,
            max_workers=min(2, max(1, math.floor(effective_cpus * 0.5))),
            rf_threads_per_proc=1,
            mem_fraction=0.60,
            mm_rows_threshold=2_000_000,
            mm_ops_threshold=75_000_000,
            speed_max_rows=1_000_000,
            chunk_size=25_000,
            enable_cache=True,
            enable_mm_files=True,
            tmp_dir=os.getenv("FM_TMPDIR", "/tmp"),
            progress_mode="blocks",
            enable_telemetry=True,
            max_execution_time=300,  # 5 minutes default
        )

    elif profile == RuntimeProfile.SERVER_DEDICATED:
        policy = ResourcePolicy(
            concurrency_model=ConcurrencyModel.HYBRID,
            max_workers=min(6, max(2, effective_cpus - 1)),
            rf_threads_per_proc=2,
            mem_fraction=0.75,
            mm_rows_threshold=3_000_000,
            mm_ops_threshold=100_000_000,
            speed_max_rows=1_000_000,
            chunk_size=100_000,
            enable_cache=True,
            enable_mm_files=True,
            tmp_dir=os.getenv("FM_TMPDIR", "/mnt/nvme/tmp"),
            progress_mode="blocks",
            enable_telemetry=True,
            max_execution_time=3600,  # 1 hour default
        )

    elif profile == RuntimeProfile.WEB_APP:
        policy = ResourcePolicy(
            concurrency_model=ConcurrencyModel.PROCESS,
            max_workers=2,
            rf_threads_per_proc=1,
            mem_fraction=0.50,
            mm_rows_threshold=None,  # Disabled
            mm_ops_threshold=None,  # Disabled
            speed_max_rows=50_000,
            chunk_size=10_000,
            enable_cache=False,
            enable_mm_files=False,
            tmp_dir=None,
            progress_mode="percentage",
            enable_telemetry=True,
            max_execution_time=30,
            degrade_steps=("sample_large_blocks", "reduce_algorithms"),
        )

    elif profile == RuntimeProfile.SERVERLESS:
        policy = ResourcePolicy(
            concurrency_model=ConcurrencyModel.THREAD,
            max_workers=1,
            rf_threads_per_proc=max(1, min(4, effective_cpus)),
            mem_fraction=0.50,
            mm_rows_threshold=None,  # Disabled
            mm_ops_threshold=None,  # Disabled
            speed_max_rows=100_000,
            chunk_size=5_000,
            enable_cache=False,
            enable_mm_files=False,
            tmp_dir=None,
            progress_mode="percentage",
            enable_telemetry=True,
            max_execution_time=540,  # 9 minutes for Lambda
        )

    elif profile == RuntimeProfile.GSHEETS:
        policy = ResourcePolicy(
            concurrency_model=ConcurrencyModel.THREAD,
            max_workers=1,
            rf_threads_per_proc=1,
            mem_fraction=0.30,
            mm_rows_threshold=None,  # Disabled
            mm_ops_threshold=None,  # Disabled
            speed_max_rows=10_000,
            chunk_size=1_000,
            enable_cache=False,
            enable_mm_files=False,
            tmp_dir=None,
            progress_mode="percentage",
            enable_telemetry=False,
            max_execution_time=30,
            degrade_steps=("sample_large_blocks",),
        )

    else:
        raise ValueError(f"Unknown profile: {profile}")

    # Apply environment overrides with sanity checks
    policy = apply_env_overrides(policy, profile)

    # Windows-specific adjustments
    if platform.system() == "Windows" and policy.max_workers > 4:
        log.info("Windows detected: capping max_workers at 4")
        policy.max_workers = min(4, policy.max_workers)

    # Enforce memory fraction for chunk sizing
    if mem_available and policy.mem_fraction:
        max_chunk_memory = int(mem_available * policy.mem_fraction / policy.max_workers)
        # Assume ~100 bytes per row average
        max_chunk_rows = max_chunk_memory // 100
        if max_chunk_rows < policy.chunk_size:
            log.info(
                f"Reducing chunk_size from {policy.chunk_size:,} to {max_chunk_rows:,} based on memory"
            )
            policy.chunk_size = max(1000, max_chunk_rows)  # Keep minimum of 1000

    return policy


# ============================================================================
# ENVIRONMENT OVERRIDES WITH SANITY
# ============================================================================


def apply_env_overrides(
    policy: ResourcePolicy, profile: RuntimeProfile
) -> ResourcePolicy:
    """Apply environment variable overrides with sanity checks."""

    # Helper to get typed env values
    def get_env_int(key: str, default: Optional[int]) -> Optional[int]:
        val = os.getenv(key)
        if val:
            try:
                return int(val)
            except ValueError:
                log.warning(f"Invalid {key} value: {val}")
        return default

    def get_env_float(key: str, default: float) -> float:
        val = os.getenv(key)
        if val:
            try:
                return float(val)
            except ValueError:
                log.warning(f"Invalid {key} value: {val}")
        return default

    # Apply overrides
    policy.max_workers = get_env_int("FM_MAX_WORKERS", policy.max_workers)

    # Special handling for RF threads - 0 means "auto"
    rf_env = os.getenv("FM_RF_THREADS")
    if rf_env is not None:
        try:
            rf_val = int(rf_env)
            if rf_val > 0:
                policy.rf_threads_per_proc = rf_val
            else:
                # 0 means auto - keep the computed policy default
                log.info(
                    "FM_RF_THREADS=0 -> auto mode; keeping computed policy default"
                )
        except ValueError:
            log.warning(f"Invalid FM_RF_THREADS value: {rf_env}")

    # Final sanity clamp - never allow 0 or negative threads
    if not policy.rf_threads_per_proc or policy.rf_threads_per_proc <= 0:
        try:
            cpus = os.cpu_count() or 1
            policy.rf_threads_per_proc = min(
                2, max(1, cpus // max(1, policy.max_workers))
            )
            log.info(
                f"Clamped rf_threads_per_proc to {policy.rf_threads_per_proc} (was 0 or negative)"
            )
        except Exception:
            policy.rf_threads_per_proc = 1

    # Also ensure max_workers is never 0
    if not policy.max_workers or policy.max_workers <= 0:
        policy.max_workers = 1
        log.warning("Clamped max_workers to 1 (was 0 or negative)")

    policy.speed_max_rows = get_env_int("FM_SPEED_MAX_ROWS", policy.speed_max_rows)
    policy.mm_rows_threshold = get_env_int(
        "FM_MM_ROWS_THRESHOLD", policy.mm_rows_threshold
    )
    policy.mm_ops_threshold = get_env_int(
        "FM_MM_OPS_THRESHOLD", policy.mm_ops_threshold
    )
    policy.chunk_size = get_env_int("FM_CHUNK_SIZE", policy.chunk_size)
    policy.mem_fraction = get_env_float("FM_MEM_FRACTION", policy.mem_fraction)

    if os.getenv("FM_TIMEOUT_SECS"):
        policy.max_execution_time = get_env_int(
            "FM_TIMEOUT_SECS", policy.max_execution_time
        )

    if os.getenv("FM_TMPDIR"):
        policy.tmp_dir = os.getenv("FM_TMPDIR")

    # Concurrency model override with sanity checks
    if os.getenv("FM_CONCURRENCY_MODEL"):
        model = os.getenv("FM_CONCURRENCY_MODEL").lower()
        if model in [m.value for m in ConcurrencyModel]:
            old_model = policy.concurrency_model
            policy.concurrency_model = ConcurrencyModel(model)

            # Sanity check: if switching to thread mode, limit workers
            if old_model != ConcurrencyModel.THREAD and model == "thread":
                if policy.max_workers > 2:
                    log.warning(
                        f"Switching to thread model: reducing max_workers from {policy.max_workers} to 2"
                    )
                    policy.max_workers = min(2, policy.max_workers)

            # If switching from thread to process in serverless, warn
            if (
                profile in [RuntimeProfile.SERVERLESS, RuntimeProfile.GSHEETS]
                and model != "thread"
            ):
                log.warning(
                    f"Profile {profile.value} works best with thread model, but overridden to {model}"
                )

    return policy


# ============================================================================
# OBSERVABILITY
# ============================================================================


def _fmt_threshold(value: Optional[int]) -> str:
    """Format threshold values, handling None and infinity."""
    if value is None:
        return "disabled"
    if isinstance(value, float) and math.isinf(value):
        return "disabled"
    return f"{int(value):,}"


def _fmt_bytes(b: Optional[int]) -> str:
    """Format bytes for readability."""
    if b is None:
        return "unlimited"
    gb = b / (1024**3)
    if gb >= 1:
        return f"{gb:.1f}GiB"
    mb = b / (1024**2)
    return f"{mb:.0f}MiB"


def log_runtime_banner(
    profile: RuntimeProfile, policy: ResourcePolicy, quotas: MachineQuota
):
    """Log a comprehensive runtime configuration banner."""

    # Calculate effective parallelism
    if policy.concurrency_model == ConcurrencyModel.THREAD:
        effective = policy.rf_threads_per_proc
        concurrency_detail = f"threads={policy.rf_threads_per_proc}"
    elif policy.concurrency_model == ConcurrencyModel.HYBRID:
        effective = policy.max_workers * policy.rf_threads_per_proc
        concurrency_detail = (
            f"workers={policy.max_workers} rf_threads={policy.rf_threads_per_proc}"
        )
    else:  # PROCESS
        effective = policy.max_workers
        concurrency_detail = f"workers={policy.max_workers}"

    # Build banner with profile source
    banner_parts = [f"profile={profile.value}"]
    banner_parts.append(f"source={quotas.profile_source}")

    if quotas.cpu_quota_cores:
        banner_parts.append(f"cpu_quota={quotas.cpu_quota_cores:.2f}")
    if quotas.cpu_set_cores:
        banner_parts.append(f"cpuset={quotas.cpu_set_cores}")
    banner_parts.append(f"mem_limit={_fmt_bytes(quotas.mem_limit_bytes)}")

    if quotas.detected_method != "none":
        banner_parts.append(f"detected_via={quotas.detected_method}")

    log.info(f"[runtime] {' '.join(banner_parts)}")

    log.info(
        f"[policy] concurrency={policy.concurrency_model.value} {concurrency_detail} "
        f"effective_parallelism={effective}"
    )

    log.info(
        f"[policy] speed_max_rows={policy.speed_max_rows:,} "
        f"mm_rows={_fmt_threshold(policy.mm_rows_threshold)} "
        f"mm_ops={_fmt_threshold(policy.mm_ops_threshold)} "
        f"chunk_size={policy.chunk_size:,} "
        f"mem_fraction={policy.mem_fraction:.0%}"
    )

    if policy.tmp_dir:
        log.info(f"[policy] tmp_dir={policy.tmp_dir}")

    if policy.max_execution_time:
        log.info(f"[policy] max_execution_time={policy.max_execution_time}s")

    if not policy.enable_mm_files:
        log.info("[policy] memory_mapped_files=disabled")


# ============================================================================
# RUNTIME MANAGER (Singleton with Progressive Degradation)
# ============================================================================


class RuntimeManager:
    """
    Singleton manager for runtime configuration.
    Tracks degradation steps and enforces policies.
    """

    _instance = None
    _initialized = False

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        if not self._initialized:
            self.profile, self.profile_source = detect_runtime_profile()
            self.quotas = read_cgroup_quotas()
            self.quotas.profile_source = self.profile_source
            self.policy = derive_policy(self.profile, self.profile_source)
            self.start_time = time.time()
            self._applied_degrades: Set[str] = set()  # Track applied degradations

            # Log configuration
            log_runtime_banner(self.profile, self.policy, self.quotas)

            # Set multiprocessing start method for Linux servers (before any pools)
            if platform.system() == "Linux" and self.profile in [
                RuntimeProfile.SERVER_SHARED,
                RuntimeProfile.SERVER_DEDICATED,
            ]:
                try:
                    import multiprocessing as mp

                    if mp.get_start_method(allow_none=True) is None:
                        mp.set_start_method("forkserver")
                        log.info(
                            "[runtime] Set multiprocessing start method to 'forkserver'"
                        )
                    else:
                        log.info(
                            f"[runtime] Multiprocessing start method already set to '{mp.get_start_method()}'"
                        )
                except RuntimeError as e:
                    log.warning(f"[runtime] Could not set start method: {e}")

            self._initialized = True

    def get_policy(self) -> ResourcePolicy:
        """Get the current resource policy."""
        return self.policy

    def should_use_memory_mapped(self, src_rows: int, ref_rows: int, ops: int) -> bool:
        """
        Determine if memory-mapped files should be used.
        Respects both enable flag and thresholds.
        """
        if not self.policy.enable_mm_files:
            return False

        # Check if thresholds are disabled (None)
        if (
            self.policy.mm_rows_threshold is None
            and self.policy.mm_ops_threshold is None
        ):
            return False

        # Check thresholds
        rows_exceed = False
        if self.policy.mm_rows_threshold is not None:
            rows_exceed = (
                src_rows > self.policy.mm_rows_threshold
                or ref_rows > self.policy.mm_rows_threshold
            )

        ops_exceed = False
        if self.policy.mm_ops_threshold is not None:
            ops_exceed = ops > self.policy.mm_ops_threshold

        return rows_exceed or ops_exceed

    def should_use_parallel(self, num_blocks: int, total_ops: int) -> bool:
        """Determine if parallel processing should be used."""
        if self.policy.concurrency_model == ConcurrencyModel.THREAD:
            return False  # Thread-only mode doesn't use process pools

        if self.policy.max_workers <= 1:
            return False

        # Dynamic threshold based on profile
        min_blocks = 10 if self.profile == RuntimeProfile.WEB_APP else 50
        min_ops = 100_000 if self.profile == RuntimeProfile.WEB_APP else 400_000

        return num_blocks > min_blocks and total_ops > min_ops

    def check_time_budget(self) -> Optional[str]:
        """
        Check if we're approaching time limits.
        Returns next degradation step to apply or None.
        Tracks applied steps to ensure progressive degradation.
        """
        if not self.policy.max_execution_time:
            return None

        elapsed = time.time() - self.start_time
        time_fraction = elapsed / self.policy.max_execution_time

        if time_fraction > 0.95:
            return "abort"  # Critical - stop now
        elif time_fraction > self.policy.soft_timeout_fraction:
            # Return next unapplied degradation step
            for step in self.policy.degrade_steps:
                if step not in self._applied_degrades:
                    self._applied_degrades.add(step)
                    log.info(
                        f"[runtime] Applying degradation step: {step} "
                        f"(time: {elapsed:.1f}s / {self.policy.max_execution_time}s)"
                    )
                    return step

            # All steps applied, getting critical
            if time_fraction > 0.90:
                return "abort"

        return None

    def get_chunk_size(self, total_rows: int) -> int:
        """Get appropriate chunk size for data processing."""
        base_chunk = self.policy.chunk_size

        # Adaptive based on total size
        if self.profile == RuntimeProfile.GSHEETS:
            return min(1000, total_rows, base_chunk)
        elif self.profile == RuntimeProfile.WEB_APP:
            return min(10_000, total_rows, base_chunk)
        else:
            # For larger profiles, adapt based on total size
            if total_rows < base_chunk * 2:
                # Small dataset, process in one chunk
                return total_rows
            return base_chunk

    def reset_degradation(self):
        """Reset degradation tracking (useful for testing or new jobs)."""
        self._applied_degrades.clear()
        self.start_time = time.time()


# ============================================================================
# INTEGRATION HELPERS
# ============================================================================

# Global singleton instance
_runtime_manager: Optional[RuntimeManager] = None


def get_runtime_manager() -> RuntimeManager:
    """Get or create the runtime manager singleton."""
    global _runtime_manager
    if _runtime_manager is None:
        _runtime_manager = RuntimeManager()
    return _runtime_manager


# Convenience functions for engine.py integration
def get_max_workers() -> int:
    """Get max worker processes for current runtime."""
    return get_runtime_manager().policy.max_workers


def get_rf_threads() -> int:
    """Get RapidFuzz threads per process for current runtime."""
    return get_runtime_manager().policy.rf_threads_per_proc


def should_use_memory_mapped(src_rows: int, ref_rows: int = 0, ops: int = 0) -> bool:
    """Check if memory-mapped files should be used."""
    return get_runtime_manager().should_use_memory_mapped(src_rows, ref_rows, ops)


def get_processing_mode(src_rows: int, ref_rows: int = 0) -> str:
    """Determine processing mode (speed vs scale)."""
    manager = get_runtime_manager()
    total_rows = src_rows + ref_rows

    if total_rows <= manager.policy.speed_max_rows:
        return "speed"
    return "scale"


def apply_degradation(step: str, config: Any) -> Any:
    """Apply a degradation step to the configuration."""
    if step == "disable_soft_widen":
        if hasattr(config, "enable_soft_widen"):
            config.enable_soft_widen = False
    elif step == "reduce_algorithms":
        config.algorithm = "WRatio"  # Fallback to fastest
        if hasattr(config, "enable_multi_algo"):
            config.enable_multi_algo = False
    elif step == "lower_parallel_threshold":
        if hasattr(config, "max_workers"):
            config.max_workers = max(1, config.max_workers // 2)
    elif step == "sample_large_blocks":
        if hasattr(config, "max_block_size"):
            config.max_block_size = min(1000, getattr(config, "max_block_size", 5000))
    elif step == "abort":
        raise TimeoutError("Execution time limit exceeded")

    log.warning(f"[runtime] Applied degradation: {step}")
    return config


# ============================================================================
# TESTING UTILITIES
# ============================================================================


def reset_runtime_manager():
    """Reset the runtime manager (useful for testing)."""
    global _runtime_manager
    if _runtime_manager:
        RuntimeManager._initialized = False
    _runtime_manager = None


def get_runtime_info() -> dict:
    """Get current runtime information for debugging/monitoring."""
    manager = get_runtime_manager()
    return {
        "profile": manager.profile.value,
        "profile_source": manager.profile_source,
        "quotas": {
            "cpu_quota_cores": manager.quotas.cpu_quota_cores,
            "cpu_set_cores": manager.quotas.cpu_set_cores,
            "mem_limit_bytes": manager.quotas.mem_limit_bytes,
            "detected_method": manager.quotas.detected_method,
        },
        "policy": {
            "concurrency_model": manager.policy.concurrency_model.value,
            "max_workers": manager.policy.max_workers,
            "rf_threads_per_proc": manager.policy.rf_threads_per_proc,
            "speed_max_rows": manager.policy.speed_max_rows,
            "chunk_size": manager.policy.chunk_size,
            "mem_fraction": manager.policy.mem_fraction,
            "mm_rows_threshold": manager.policy.mm_rows_threshold,
            "mm_ops_threshold": manager.policy.mm_ops_threshold,
            "enable_mm_files": manager.policy.enable_mm_files,
        },
        "elapsed_time": time.time() - manager.start_time if manager else 0,
        "applied_degrades": list(manager._applied_degrades) if manager else [],
    }
