"""Telemetry v2 — shapes-only protocol for privacy-first monitoring.

Controls exactly what leaves the device. Transforms raw events into
privacy-safe shapes before transmission.

The key principle: we transmit the SHAPE of behavior, not the CONTENT.
- What type of file was accessed (not which file)
- What category of URL was fetched (not which URL)
- What kind of command ran (not the command itself)

Telemetry modes (from config):
    "full"    — Send everything including target (legacy, opt-in)
    "shapes"  — Replace target with shape, strip PII (default, recommended)
    "minimal" — Only send action, result, detected_patterns (most private)
    "off"     — Don't send anything (return None)

Zero external dependencies (stdlib only).
"""
import hashlib
import re


# Fields that should NEVER leave the device in shapes mode
_PII_FIELDS = {"target"}

# Fields that are always safe to transmit (no PII possible)
_SAFE_FIELDS = {
    "agent_id", "session_id", "action", "result",
    "detected_patterns", "target_hash",
}

# Metadata fields that are safe to transmit
_SAFE_METADATA = {
    "hook_event", "sdk_version", "cwd",
    "target_category", "local_detection",
    "baseline_deviations", "baseline_summary",
    "content_scan", "obfuscation_patterns",
    "injection_patterns_client", "indirect_injection_patterns",
    "mcp_poisoning_patterns", "mcp_server", "mcp_tool",
    "destination", "exit_code", "stdout_lines", "stderr_lines",
    "stdout_size", "stderr_size", "response_hash", "response_size",
    "success", "match_count", "tool_use_id",
    "behavioral_flag", "detection_status",
}

# Metadata fields that may contain PII and need scrubbing
_PII_METADATA = {
    "cwd",          # working directory may reveal username
    "destination",  # URLs may contain credentials, usernames, tokens
}

# Maximum length for any individual metadata string value (shapes mode)
_MAX_METADATA_VALUE_LEN = 500


# ---- PII scrubbing patterns --------------------------------------------------
# Each tuple: (compiled_regex, replacement)

_PII_PATTERNS = [
    # URL-embedded credentials: https://user:pass@host.com → https://[CRED]@host.com
    # Must come before email pattern to avoid partial matches
    (re.compile(r'://[^@\s/:]{1,64}:[^@\s/:]{1,64}@'), '://[CRED]@'),
    # Home directory paths (handle optional surrounding quotes)
    (re.compile(r'["\']?/home/[^/\s"\']+["\']?'), '[HOME]'),
    (re.compile(r'["\']?/Users/[^/\s"\']+["\']?'), '[HOME]'),
    (re.compile(r'["\']?C:\\Users\\[^\\\s"\']+["\']?'), '[HOME]'),
    # Email addresses
    (re.compile(r'[a-zA-Z0-9._%+-]{1,128}@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'), '[EMAIL]'),
    # IP addresses (IPv4)
    (re.compile(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b'), '[IP]'),
    # SSN (US format: XXX-XX-XXXX)
    (re.compile(r'\b\d{3}-\d{2}-\d{4}\b'), '[SSN]'),
    # Phone numbers (US formats: (555) 123-4567, 555-123-4567, +1-555-123-4567)
    (re.compile(r'(?:\+1[-\s]?)?\(?\d{3}\)?[-\s]?\d{3}[-\s]?\d{4}\b'), '[PHONE]'),
    # API keys/tokens (common prefixes)
    (re.compile(r'(?:sk-|ghp_|gho_|ghs_|glpat-|xox[bpors]-|AKIA|ob_(?:live|test)_)[A-Za-z0-9_-]{10,}'), '[REDACTED]'),
    (re.compile(r'Bearer\s+\S+'), 'Bearer [REDACTED]'),
    # JWT tokens (three base64 segments separated by dots)
    (re.compile(r'\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}'), '[JWT]'),
    # Private key markers
    (re.compile(r'-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----'), '[PRIVATE_KEY]'),
    # Cloud resource IDs
    (re.compile(r'\bi-[0-9a-f]{8,17}\b'), '[AWS_INSTANCE]'),      # AWS EC2 instance
    (re.compile(r'\bvol-[0-9a-f]{8,17}\b'), '[AWS_VOLUME]'),      # AWS EBS volume
    (re.compile(r'\bsg-[0-9a-f]{8,17}\b'), '[AWS_SG]'),           # AWS security group
    (re.compile(r'(?:container|image)[_\s:]+[0-9a-f]{12,64}'), '[CONTAINER_REF]'),  # Docker container/image ref
]


# ---- Internal helpers --------------------------------------------------------

def _hash_target(target: str) -> str:
    """SHA-256 prefix of target for deduplication without revealing content.

    Returns 16-char hex prefix — enough for deduplication, not reversible.
    """
    if not target:
        return ""
    return hashlib.sha256(target.encode()).hexdigest()[:16]


# ---- Public API --------------------------------------------------------------

def strip_pii(text: str, max_len: int = 200) -> str:
    """Remove PII patterns from a text string.

    Replaces sensitive patterns with placeholders:
    - File paths -> category label
    - IP addresses -> "[IP]"
    - Email addresses -> "[EMAIL]"
    - API keys/tokens -> "[REDACTED]"

    Used as a safety net for any metadata fields that might contain
    leaked PII.

    Args:
        text: Input text to scrub.
        max_len: Maximum length of returned string.

    Returns:
        Scrubbed text, truncated to max_len. Returns empty string
        for falsy input. Never raises exceptions.
    """
    if not text:
        return text
    try:
        result = str(text)
        for pattern, replacement in _PII_PATTERNS:
            result = pattern.sub(replacement, result)
        return result[:max_len]
    except Exception:
        return str(text)[:max_len]


def compute_target_shape(tool_name: str, target: str,
                         api_url: str = None,
                         allowed_destinations: list = None) -> dict:
    """Compute the privacy-safe shape of a target.

    Delegates to categories.categorize_target() for classification,
    then wraps the result with a SHA-256 hash prefix for deduplication.

    Args:
        tool_name: The tool/action name (e.g. "Read", "Bash", "WebFetch").
        target: The raw target string (file path, URL, command).
        api_url: Obol API URL (for self-API detection in URL categorization).
        allowed_destinations: User-configured allowed domains.

    Returns:
        dict with keys:
            "shape": category name (e.g. "credential_ssh", "source_code")
            "sensitivity": level (e.g. "critical", "none")
            "hash": SHA-256 prefix for deduplication (16 chars)
            "risk": URL risk level (for network targets only)
            "domain_hash": domain hash (for network targets only)

    Never raises exceptions. Returns "unknown" shape on error.
    """
    if not target:
        return {"shape": "unknown", "sensitivity": "none", "hash": ""}

    shape = {
        "hash": _hash_target(target),
    }

    try:
        from obol.hooks.categories import categorize_target
        cat = categorize_target(
            tool_name, target,
            api_url=api_url,
            allowed_destinations=allowed_destinations or [],
        )
        shape["shape"] = cat.get("category", "unknown")
        shape["sensitivity"] = cat.get("sensitivity", "none")
        if "risk" in cat:
            shape["risk"] = cat["risk"]
        if "domain_hash" in cat:
            shape["domain_hash"] = cat["domain_hash"]
    except ImportError:
        # categories.py not available — degrade gracefully
        shape["shape"] = "unknown"
        shape["sensitivity"] = "none"

    return shape


def shape_event(event: dict, config: dict = None) -> dict:
    """Transform a raw event into a shapes-only event for transmission.

    Replaces PII-containing fields with privacy-safe shapes.
    The shaped event can be safely transmitted to the server.

    This is the main entry point — called from claude_code.py before
    the API send.

    Args:
        event: Raw event dict from claude_code.py handler.
        config: Obol config dict (for telemetry_mode and API settings).

    Returns:
        Shaped event dict ready for API transmission, or None if
        telemetry_mode is "off". Returns the event unchanged if
        telemetry_mode is "full" (legacy default).

    Telemetry modes (from config):
        "full"    - Send everything including target (legacy, opt-in)
        "shapes"  - Replace target with shape, strip PII (default)
        "minimal" - Only send action, result, detected_patterns
        "off"     - Don't send anything (return None)

    Never raises exceptions.
    """
    if not event:
        return event

    config = config or {}

    try:
        mode = config.get("telemetry_mode", "shapes")
    except (AttributeError, TypeError):
        mode = "full"

    if mode == "off":
        return None

    if mode == "full":
        # Legacy mode — send everything (existing behavior)
        return event

    try:
        if mode == "minimal":
            # Most private — only behavioral shapes
            src_meta = event.get("metadata", {})
            return {
                "agent_id": event.get("agent_id"),
                "session_id": event.get("session_id"),
                "action": event.get("action"),
                "result": event.get("result"),
                "detected_patterns": event.get("detected_patterns", []),
                "target_hash": event.get("target_hash"),
                "metadata": {
                    "hook_event": src_meta.get("hook_event") if isinstance(src_meta, dict) else None,
                    "sdk_version": src_meta.get("sdk_version") if isinstance(src_meta, dict) else None,
                },
            }

        # mode == "shapes" — recommended
        shaped = {}

        # Copy safe fields directly
        for field in _SAFE_FIELDS:
            if field in event:
                shaped[field] = event[field]

        # Replace target with shape
        if "target" in event and event["target"]:
            target_shape = compute_target_shape(
                event.get("action", ""),
                event["target"],
                api_url=config.get("api_url"),
                allowed_destinations=config.get("allowed_destinations", []),
            )
            shaped["target_shape"] = target_shape.get("shape", "unknown")
            shaped["target_sensitivity"] = target_shape.get("sensitivity", "none")
            if "risk" in target_shape:
                shaped["target_risk"] = target_shape["risk"]

        # Shape metadata — keep safe fields, scrub PII fields, drop unknown
        src_meta = event.get("metadata", {})
        shaped_meta = {}
        if isinstance(src_meta, dict):
            for key, val in src_meta.items():
                if key in _SAFE_METADATA:
                    if key in _PII_METADATA:
                        shaped_meta[key] = strip_pii(str(val) if val is not None else "",
                                                     max_len=_MAX_METADATA_VALUE_LEN)
                    elif isinstance(val, str) and len(val) > _MAX_METADATA_VALUE_LEN:
                        shaped_meta[key] = val[:_MAX_METADATA_VALUE_LEN]
                    else:
                        shaped_meta[key] = val
        shaped["metadata"] = shaped_meta

        return shaped

    except Exception:
        # Never break the event pipeline — fall back to MINIMAL mode (not full)
        # to preserve privacy even when shaping fails
        try:
            src_meta = event.get("metadata", {})
            return {
                "agent_id": event.get("agent_id"),
                "session_id": event.get("session_id"),
                "action": event.get("action"),
                "result": event.get("result"),
                "detected_patterns": event.get("detected_patterns", []),
                "target_hash": event.get("target_hash"),
                "metadata": {
                    "hook_event": src_meta.get("hook_event") if isinstance(src_meta, dict) else None,
                    "sdk_version": src_meta.get("sdk_version") if isinstance(src_meta, dict) else None,
                    "detection_status": "shaping_failed",
                },
            }
        except Exception:
            # Absolute last resort — drop target at minimum
            safe = dict(event)
            safe.pop("target", None)
            return safe
