"""
Brynq Runtime — Privacy Filter.

Whitelist-based sanitization for execution reports. Only known-safe fields
pass through. Everything else is dropped silently.

Design principle: WHITELIST, not blacklist. We enumerate exactly which fields
are safe to transmit. Any field not on the whitelist is stripped. This is
intentionally conservative — a new field added to StepMetrics will NOT be
sent until someone explicitly adds it to the whitelist here.

Why this matters:
    - User prompts, model outputs, and API keys live in the runtime's memory
      during execution. A careless ``json.dumps(step.__dict__)`` could leak them.
    - This module is the SINGLE chokepoint between the runtime and the network.
      Every outbound report passes through ``sanitize_report()`` before transmission.
    - The whitelist is auditable: ``SAFE_STEP_FIELDS`` and ``SAFE_PLAN_FIELDS``
      are the complete list of what can leave the user's machine.

Blocked heuristic:
    - Any string value longer than ``MAX_SAFE_STRING_LENGTH`` (100 chars) is
      replaced with ``"<redacted>"``. This catches prompt fragments, outputs,
      or error messages that might contain user content.
"""

from __future__ import annotations

import copy
import logging
from dataclasses import asdict, dataclass, field
from typing import Any

log = logging.getLogger("brynq.runtime.privacy")

# ── Whitelist definitions ──────────────────────────────────────────────

MAX_SAFE_STRING_LENGTH: int = 100

# Fields allowed in per-step reports. Order doesn't matter.
SAFE_STEP_FIELDS: frozenset[str] = frozenset({
    "step_id",
    "elapsed_ms",
    "tokens_used",
    "success",
    "error_type",
    "model_name",
    "backend",
    "task_type",
})

# Fields allowed in the top-level plan report.
SAFE_PLAN_FIELDS: frozenset[str] = frozenset({
    "plan_id",
    "total_elapsed_ms",
    "steps_completed",
    "steps_failed",
    "steps",
    "reported_at",
})

# Fields that MUST NEVER pass through, even if someone adds them to the
# whitelist by accident. This is a safety net.
PERMANENTLY_BLOCKED: frozenset[str] = frozenset({
    "prompt",
    "output",
    "input",
    "context",
    "api_key",
    "secret",
    "token",
    "password",
    "original_input",
    "response",
    "content",
    "message",
    "user_input",
    "system_prompt",
})


# ── Sanitized output types ─────────────────────────────────────────────

@dataclass
class SanitizedStepReport:
    """Sanitized per-step report — guaranteed free of user content.

    Attributes:
        step_id: Identifier for the step within the plan.
        elapsed_ms: Wall clock execution time in milliseconds.
        tokens_used: Total tokens consumed (input + output).
        success: Whether the step completed without error.
        error_type: Categorical error label, or None if success.
        model_name: Which model ran this step (e.g., "llama3", "claude-sonnet").
        backend: Execution backend (e.g., "ollama", "claude", "gemini").
        task_type: Task classification (e.g., "summarization", "analysis").
    """

    step_id: str = ""
    elapsed_ms: float = 0.0
    tokens_used: int = 0
    success: bool = False
    error_type: str | None = None
    model_name: str = ""
    backend: str = ""
    task_type: str = ""


@dataclass
class SanitizedReport:
    """Sanitized plan-level report — the ONLY thing sent over the wire.

    Attributes:
        plan_id: Which execution plan was run.
        total_elapsed_ms: End-to-end wall clock time.
        steps_completed: Number of steps that succeeded.
        steps_failed: Number of steps that failed.
        steps: Per-step sanitized reports.
        reported_at: Unix timestamp when the report was generated.
    """

    plan_id: str = ""
    total_elapsed_ms: float = 0.0
    steps_completed: int = 0
    steps_failed: int = 0
    steps: list[SanitizedStepReport] = field(default_factory=list)
    reported_at: float = 0.0


# ── Sanitization logic ─────────────────────────────────────────────────

def _is_safe_value(value: Any) -> bool:
    """Check whether a value is safe to transmit.

    Safe values:
        - Numbers (int, float) — always safe
        - Booleans — always safe
        - None — always safe
        - Strings <= MAX_SAFE_STRING_LENGTH — safe (short identifiers, error labels)
        - Strings > MAX_SAFE_STRING_LENGTH — BLOCKED (may contain prompts/outputs)
        - Lists — checked recursively
        - Dicts — checked recursively

    Args:
        value: Any value to check.

    Returns:
        True if the value is safe to include in the report.
    """
    if value is None:
        return True
    if isinstance(value, (bool, int, float)):
        return True
    if isinstance(value, str):
        return len(value) <= MAX_SAFE_STRING_LENGTH
    if isinstance(value, list):
        return all(_is_safe_value(item) for item in value)
    if isinstance(value, dict):
        return all(
            _is_safe_value(k) and _is_safe_value(v)
            for k, v in value.items()
        )
    # Unknown types are blocked
    return False


def _sanitize_value(value: Any) -> Any:
    """Sanitize a single value, replacing unsafe content with '<redacted>'.

    Args:
        value: The value to sanitize.

    Returns:
        The original value if safe, or '<redacted>' if not.
    """
    if _is_safe_value(value):
        return value
    if isinstance(value, str):
        return "<redacted>"
    # For complex types that fail the check, redact entirely
    return "<redacted>"


def _sanitize_step(step_data: dict[str, Any]) -> SanitizedStepReport:
    """Sanitize a single step report dict into a SanitizedStepReport.

    Only fields in SAFE_STEP_FIELDS pass through. Fields in
    PERMANENTLY_BLOCKED are always dropped even if whitelisted.
    String values exceeding MAX_SAFE_STRING_LENGTH are redacted.

    Args:
        step_data: Raw step report dict (may contain unsafe fields).

    Returns:
        SanitizedStepReport with only safe fields populated.
    """
    safe: dict[str, Any] = {}

    for key, value in step_data.items():
        # Hard block: never let these through
        if key in PERMANENTLY_BLOCKED:
            log.debug("Blocked permanently-banned field: %s", key)
            continue

        # Whitelist check
        if key not in SAFE_STEP_FIELDS:
            log.debug("Dropped non-whitelisted field: %s", key)
            continue

        # Value safety check
        safe[key] = _sanitize_value(value)

    return SanitizedStepReport(**safe)


def sanitize_report(report: dict[str, Any]) -> SanitizedReport:
    """Sanitize a raw execution report into a SanitizedReport.

    This is the main entry point. Every report MUST pass through this
    function before being queued for transmission.

    The function:
        1. Deep-copies the input (never mutate the caller's data)
        2. Strips all fields not in SAFE_PLAN_FIELDS
        3. Strips all step fields not in SAFE_STEP_FIELDS
        4. Redacts any string value > MAX_SAFE_STRING_LENGTH
        5. Hard-blocks any field in PERMANENTLY_BLOCKED

    Args:
        report: Raw report dict. Expected keys: plan_id, total_elapsed_ms,
                steps_completed, steps_failed, steps (list of step dicts),
                reported_at.

    Returns:
        SanitizedReport guaranteed free of user content.
    """
    # Deep copy to avoid mutating caller's data
    data = copy.deepcopy(report)

    # Extract and sanitize steps separately
    raw_steps = data.pop("steps", [])
    sanitized_steps = [_sanitize_step(s) for s in raw_steps]

    # Filter plan-level fields
    safe_plan: dict[str, Any] = {}
    for key, value in data.items():
        if key in PERMANENTLY_BLOCKED:
            log.debug("Blocked permanently-banned plan field: %s", key)
            continue
        if key not in SAFE_PLAN_FIELDS:
            log.debug("Dropped non-whitelisted plan field: %s", key)
            continue
        if key == "steps":
            continue  # Handled above
        safe_plan[key] = _sanitize_value(value)

    safe_plan["steps"] = sanitized_steps

    return SanitizedReport(**safe_plan)


def to_wire_dict(report: SanitizedReport) -> dict[str, Any]:
    """Convert a SanitizedReport to a plain dict suitable for JSON serialization.

    This is a convenience function for the reporter to call before
    ``json.dumps()``.

    Args:
        report: A sanitized report.

    Returns:
        Plain dict with only safe fields.
    """
    return asdict(report)
