"""SDK safety layer: memory guard and PII redaction.

Memory guard: tracks RSS at SDK init and disables event capture if
process memory grows by more than a configured threshold (default 50 MB).
This prevents the SDK from contributing to OOM kills in customer workers.

PII redaction: task arguments are never captured by the SDK event pipeline.
The ``redact_task_args`` utility exists as defense-in-depth for any future
opt-in args capture feature — V0 simply never includes args in events.
"""

from __future__ import annotations

import logging
import os
import sys
import threading
from pathlib import Path
from typing import Any

logger = logging.getLogger("sluice")

# Default memory growth threshold in bytes (50 MB).
_DEFAULT_MEMORY_THRESHOLD_BYTES = 50 * 1024 * 1024

# How often the memory guard checks RSS (seconds).
_MEMORY_CHECK_INTERVAL_S = 30.0

# Env var to override the memory threshold (in MB).
_MEMORY_THRESHOLD_ENV = "SLUICE_MEMORY_THRESHOLD_MB"

# Sentinel for redacted arguments.
_REDACTED = "[redacted]"


class MemoryGuard:
    """Monitors process RSS and disables event capture on excessive growth.

    Thread-safe: ``is_disabled`` uses ``threading.Event`` so it can be
    safely read from any thread (including free-threaded Python 3.13+).

    Usage::

        guard = MemoryGuard(threshold_bytes=50 * 1024 * 1024)
        guard.start()

        if guard.is_disabled:
            # Skip event capture
            ...

        guard.stop()
    """

    def __init__(
        self,
        *,
        threshold_bytes: int = _DEFAULT_MEMORY_THRESHOLD_BYTES,
        check_interval_s: float = _MEMORY_CHECK_INTERVAL_S,
    ) -> None:
        self._threshold_bytes = threshold_bytes
        self._check_interval_s = check_interval_s
        self._baseline_rss: int = 0
        self._disabled_event = threading.Event()
        self._stopping = threading.Event()
        self._thread: threading.Thread | None = None

    @property
    def is_disabled(self) -> bool:
        """True if the memory guard has disabled event capture."""
        return self._disabled_event.is_set()

    @property
    def baseline_rss(self) -> int:
        """RSS at init time, in bytes."""
        return self._baseline_rss

    def start(self) -> None:
        """Record baseline RSS and start the monitoring thread."""
        self._baseline_rss = _get_rss_bytes()
        if self._baseline_rss == 0:
            logger.debug("[sluice] Could not read process RSS. Memory guard disabled.")
            return

        self._stopping.clear()
        self._disabled_event.clear()
        self._thread = threading.Thread(
            target=self._check_loop,
            name="sluice-memguard",
            daemon=True,
        )
        self._thread.start()
        logger.debug(
            "[sluice] Memory guard started. Baseline RSS: %.1f MB, threshold: %.1f MB.",
            self._baseline_rss / (1024 * 1024),
            self._threshold_bytes / (1024 * 1024),
        )

    def stop(self) -> None:
        """Stop the monitoring thread."""
        self._stopping.set()
        if self._thread is not None and self._thread.is_alive():
            self._thread.join(timeout=2.0)
            self._thread = None

    def check_once(self) -> bool:
        """Run a single memory check.

        Returns True if capture should continue, False if disabled.
        """
        if self._disabled_event.is_set():
            return False

        current_rss = _get_rss_bytes()
        if current_rss == 0:
            return True

        growth = current_rss - self._baseline_rss
        if growth > self._threshold_bytes:
            self._disabled_event.set()
            logger.warning(
                "[sluice] Memory growth %.1f MB exceeds "
                "%.1f MB threshold. "
                "Event capture disabled to protect worker "
                "stability. "
                "See: https://docs.sluice.sh/troubleshooting/"
                "memory",
                growth / (1024 * 1024),
                self._threshold_bytes / (1024 * 1024),
            )
            return False

        return True

    def _check_loop(self) -> None:
        """Periodic memory check loop."""
        while not self._stopping.wait(timeout=self._check_interval_s):
            try:
                self.check_once()
            except Exception:
                logger.debug(
                    "[sluice] Error in memory guard check.",
                    exc_info=True,
                )


def _get_rss_bytes() -> int:
    """Get current process RSS in bytes. Returns 0 if unavailable.

    On Linux, reads ``/proc/self/statm`` for accurate current RSS.
    On macOS/other, falls back to ``resource.getrusage().ru_maxrss``
    which reports **peak** (high-water mark) RSS, not current. This
    is a best-effort approximation — production Celery workers
    typically run on Linux where the primary path is accurate.
    """
    try:
        # Primary: /proc/self/statm on Linux.
        # Field 1 (index 1) is RSS in pages.
        proc_statm = Path("/proc/self/statm")
        if proc_statm.exists():
            parts = proc_statm.read_text().split()
            rss_pages = int(parts[1])
            page_size = os.sysconf("SC_PAGE_SIZE")
            return rss_pages * page_size
    except Exception:
        pass

    try:
        # Fallback: resource module (macOS and most Unixes).
        # NOTE: ru_maxrss is peak RSS, not current. See docstring.
        import resource

        usage = resource.getrusage(resource.RUSAGE_SELF)
        rss = usage.ru_maxrss
        if sys.platform == "darwin":
            return rss  # Bytes on macOS
        return rss * 1024  # KB -> bytes on Linux
    except Exception:
        pass

    return 0


def resolve_memory_threshold() -> int:
    """Resolve the memory threshold from env var or default.

    Reads ``SLUICE_MEMORY_THRESHOLD_MB`` env var (in megabytes).
    Falls back to 50 MB default.
    """
    env_val = os.environ.get(_MEMORY_THRESHOLD_ENV)
    if env_val is not None:
        try:
            mb = int(env_val)
            if mb > 0:
                return mb * 1024 * 1024
        except ValueError:
            logger.warning(
                "[sluice] Invalid %s value '%s'. Using default 50 MB.",
                _MEMORY_THRESHOLD_ENV,
                env_val,
            )
    return _DEFAULT_MEMORY_THRESHOLD_BYTES


def redact_task_args(
    args: Any = None,
    kwargs: Any = None,
) -> dict[str, str]:
    """Redact task arguments for safe logging.

    Always returns redacted placeholders. Task arguments can contain
    passwords, API keys, PII, credit card numbers — never log them.

    Note: V0 signal handlers never capture task args in the first place.
    This function exists as defense-in-depth for any future opt-in
    args capture feature.
    """
    result: dict[str, str] = {}
    if args is not None:
        result["args"] = _REDACTED
    if kwargs is not None:
        result["kwargs"] = _REDACTED
    return result
