"""
Internal utilities shared across the SDK.

Provides exception-safe helpers, debug logging, safe string operations,
encryption, and the critical ``capture_internal`` context-manager that
prevents SDK bugs from crashing the host application.
"""

import os
import sys
import threading
import traceback as _tb

from overcast_sdk.consts import MAX_STRING_LENGTH

# ── Originals registry ────────────────────────────────────────────────────────
# Stores original objects (sys.stdout, sys.excepthook, etc.) before patching.
_originals = {}

# ── Thread-local sending guard ────────────────────────────────────────────────
_local = threading.local()


def is_sending():
    """Return True if the current thread is inside a transport send."""
    return getattr(_local, "sending", False)


def set_sending(value):
    """Mark the current thread as sending (or not)."""
    _local.sending = value


# ── Exception isolation ───────────────────────────────────────────────────────

class _CaptureInternalError:
    """Context manager that catches and logs SDK-internal errors.

    The host application must *never* crash because of the SDK.  Wrap
    every SDK-internal operation with ``with capture_internal: ...``.
    """
    __slots__ = ()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            log_internal_error(exc_type, exc_val, exc_tb)
        return True  # always suppress


capture_internal = _CaptureInternalError()


def log_internal_error(exc_type, exc_val, exc_tb):
    """Write an SDK-internal error to stderr (only when debug=True)."""
    try:
        stderr = _originals.get("stderr") or sys.__stderr__
        if _get_debug():
            stderr.write(
                f"[Overcast SDK Error] {exc_type.__name__}: {exc_val}\n"
            )
            _tb.print_exception(exc_type, exc_val, exc_tb, file=stderr)
            stderr.flush()
    except Exception:
        pass  # last resort — never crash


def _get_debug():
    """Check whether debug mode is enabled (lazy, avoids circular import)."""
    try:
        from overcast_sdk.client import get_client
        client = get_client()
        return client is not None and client.options.get("debug", False)
    except Exception:
        return False


def debug_log(msg):
    """Print a ``[Overcast]`` debug message to stderr."""
    try:
        if _get_debug():
            stderr = _originals.get("stderr") or sys.__stderr__
            stderr.write(f"[Overcast] {msg}\n")
            stderr.flush()
    except Exception:
        pass


# ── Safe string helpers ───────────────────────────────────────────────────────

def safe_str(value, max_length=MAX_STRING_LENGTH):
    """Convert *value* to str without raising."""
    try:
        s = str(value)
    except Exception:
        try:
            s = repr(value)
        except Exception:
            return "<unrepresentable>"
    return s[:max_length] if len(s) > max_length else s


def safe_repr(value, max_length=MAX_STRING_LENGTH):
    """Return repr(*value*) without raising."""
    try:
        s = repr(value)
    except Exception:
        return "<broken repr>"
    return s[:max_length] if len(s) > max_length else s


def truncate(data, max_len=2000):
    """Truncate strings / bytes to *max_len* characters."""
    if data is None:
        return None
    if isinstance(data, bytes):
        try:
            data = data.decode("utf-8", errors="ignore")
        except Exception:
            return "<binary>"
    if isinstance(data, str):
        return data[:max_len] if len(data) > max_len else data
    return safe_str(data, max_len)


def truncate_traceback(tb_string, max_length=8192):
    """Truncate a traceback, keeping the beginning and end."""
    if not isinstance(tb_string, str) or len(tb_string) <= max_length:
        return tb_string
    half = max_length // 2
    return tb_string[:half] + "\n  ... truncated ...\n" + tb_string[-half:]


# ── Package introspection ─────────────────────────────────────────────────────

def get_package_version(package_name):
    """Return the installed version of *package_name* as a tuple, or None."""
    try:
        from importlib.metadata import version as _ver
        raw = _ver(package_name)
        return tuple(int(x) for x in raw.split(".")[:3])
    except Exception:
        return None


# ── Encryption ────────────────────────────────────────────────────────────────

def encrypt_payload(plaintext, config):
    """AES-256-GCM encrypt *plaintext* if configured, else return as-is."""
    if not config.get("encrypt_payload") or not config.get("encryption_key"):
        return plaintext
    try:
        import base64
        import json
        try:
            from Crypto.Cipher import AES
            key = bytes.fromhex(config["encryption_key"])
            cipher = AES.new(key, AES.MODE_GCM)
            ct, tag = cipher.encrypt_and_digest(plaintext.encode("utf-8"))
            return json.dumps({
                "encrypted": True,
                "algorithm": "aes-256-gcm",
                "iv": base64.b64encode(cipher.nonce).decode(),
                "auth_tag": base64.b64encode(tag).decode(),
                "data": base64.b64encode(ct).decode(),
            })
        except ImportError:
            from cryptography.hazmat.primitives.ciphers.aead import AESGCM
            key = bytes.fromhex(config["encryption_key"])
            aesgcm = AESGCM(key)
            nonce = os.urandom(12)
            ct = aesgcm.encrypt(nonce, plaintext.encode("utf-8"), None)
            return json.dumps({
                "encrypted": True,
                "algorithm": "aes-256-gcm",
                "iv": base64.b64encode(nonce).decode(),
                "data": base64.b64encode(ct).decode(),
            })
    except Exception as e:
        debug_log(f"Encryption failed, sending unencrypted: {e}")
        return plaintext


# ── Sample rate validation ────────────────────────────────────────────────────

def is_valid_sample_rate(rate):
    """Return True if *rate* is a number in [0.0, 1.0]."""
    if not isinstance(rate, (int, float)):
        return False
    return 0.0 <= rate <= 1.0
