"""Security module for sanitizing sensitive data from data structures.

This module ensures that sensitive information like API keys, passwords, and tokens
are not accidentally leaked in logs, files, or API responses.

Includes helpers for filtering research metadata in API responses to prevent
settings_snapshot (which contains all application settings including API keys)
from being sent to the frontend.
"""

import json
from typing import Any, Set


# The placeholder a redacted value is replaced with. Single source of truth
# so that write-back guards (which must treat this sentinel as a no-op to
# avoid persisting it over a real secret on a redacted GET round-trip)
# cannot drift from what the redactor actually emits.
REDACTION_TEXT = "[REDACTED]"


class DataSanitizer:
    """Utility class for removing sensitive information from data structures."""

    # Public alias of the module-level sentinel (see REDACTION_TEXT above).
    REDACTION_TEXT: str = REDACTION_TEXT

    # Default set of sensitive key names to redact
    DEFAULT_SENSITIVE_KEYS: Set[str] = {
        "api_key",
        "apikey",
        "password",
        "secret",
        "access_token",
        "refresh_token",
        "private_key",
        "auth_token",
        "session_token",
        "csrf_token",
    }

    @staticmethod
    def is_sensitive_setting(
        key: str,
        ui_element: str | None = None,
        sensitive_keys: Set[str] | None = None,
    ) -> bool:
        """True when a setting holds a secret: it is ``ui_element ==
        "password"`` OR the last dotted segment of its key is a sensitive
        name (``llm.openai.api_key`` -> ``api_key``).

        Single source of truth for "is this a secret" so the GET redactor
        and the write-back no-op guards apply the SAME predicate — a value
        the redactor masks to the sentinel must also be one the guards
        refuse to overwrite, or a redacted GET could round-trip the
        sentinel back over the real secret.
        """
        if ui_element == "password":
            return True
        sens = {
            k.lower()
            for k in (sensitive_keys or DataSanitizer.DEFAULT_SENSITIVE_KEYS)
        }
        return key.rsplit(".", 1)[-1].lower() in sens

    @staticmethod
    def redact_value(
        key: str,
        ui_element: str | None = None,
        value: Any = None,
        sensitive_keys: Set[str] | None = None,
        redaction_text: str = REDACTION_TEXT,
    ) -> Any:
        """Redact a single setting's value when it holds a set secret.

        The single-value counterpart of ``redact_settings_snapshot``: it
        applies the SAME ``is_sensitive_setting`` predicate and the SAME
        empty-value rule, so every read path that ships a setting to the
        browser (the bulk GET, the singular GET, the run-time snapshot)
        masks identically. Returns ``redaction_text`` for a non-empty
        sensitive value, otherwise ``value`` unchanged.

        Empty values (``None``, ``""``, ``[]``, ``{}``) are left readable so
        the UI can tell "configured" from "not configured" without leaking
        that a secret is set.
        """
        if DataSanitizer.is_sensitive_setting(
            key, ui_element, sensitive_keys
        ) and value not in (None, "", [], {}):
            return redaction_text
        return value

    @staticmethod
    def sanitize(data: Any, sensitive_keys: Set[str] | None = None) -> Any:
        """
        Recursively remove sensitive keys from data structures.

        This method traverses dictionaries and lists, removing any keys that match
        the sensitive keys list (case-insensitive). This prevents accidental
        credential leakage in optimization results, logs, or API responses.

        Args:
            data: The data structure to sanitize (dict, list, or primitive)
            sensitive_keys: Set of key names to remove (case-insensitive).
                          If None, uses DEFAULT_SENSITIVE_KEYS.

        Returns:
            Sanitized copy of the data with sensitive keys removed

        Example:
            >>> sanitizer = DataSanitizer()
            >>> data = {"username": "user", "api_key": "secret123"}
            >>> sanitizer.sanitize(data)
            {"username": "user"}
        """
        if sensitive_keys is None:
            sensitive_keys = DataSanitizer.DEFAULT_SENSITIVE_KEYS

        # Convert to lowercase for case-insensitive comparison
        sensitive_keys_lower = {key.lower() for key in sensitive_keys}

        if isinstance(data, dict):
            return {
                k: DataSanitizer.sanitize(v, sensitive_keys)
                for k, v in data.items()
                if k.lower() not in sensitive_keys_lower
            }
        if isinstance(data, list):
            return [
                DataSanitizer.sanitize(item, sensitive_keys) for item in data
            ]
        # Return primitives unchanged
        return data

    @staticmethod
    def redact(
        data: Any,
        sensitive_keys: Set[str] | None = None,
        redaction_text: str = REDACTION_TEXT,
    ) -> Any:
        """
        Recursively redact (replace with placeholder) sensitive values in data structures.

        Unlike sanitize() which removes keys entirely, this method replaces their
        values with a redaction placeholder, preserving the structure.

        Args:
            data: The data structure to redact (dict, list, or primitive)
            sensitive_keys: Set of key names to redact (case-insensitive).
                          If None, uses DEFAULT_SENSITIVE_KEYS.
            redaction_text: Text to replace sensitive values with

        Returns:
            Copy of the data with sensitive values redacted

        Example:
            >>> sanitizer = DataSanitizer()
            >>> data = {"username": "user", "api_key": "secret123"}
            >>> sanitizer.redact(data)
            {"username": "user", "api_key": "[REDACTED]"}
        """
        if sensitive_keys is None:
            sensitive_keys = DataSanitizer.DEFAULT_SENSITIVE_KEYS

        # Convert to lowercase for case-insensitive comparison
        sensitive_keys_lower = {key.lower() for key in sensitive_keys}

        if isinstance(data, dict):
            return {
                k: (
                    redaction_text
                    if k.lower() in sensitive_keys_lower
                    else DataSanitizer.redact(v, sensitive_keys, redaction_text)
                )
                for k, v in data.items()
            }
        if isinstance(data, list):
            return [
                DataSanitizer.redact(item, sensitive_keys, redaction_text)
                for item in data
            ]
        # Return primitives unchanged
        return data

    @staticmethod
    def redact_settings_snapshot(
        snapshot: Any,
        sensitive_keys: Set[str] | None = None,
        redaction_text: str = REDACTION_TEXT,
    ) -> Any:
        """Redact secret values in a settings snapshot while preserving metadata.

        A settings snapshot from ``SettingsManager.get_all_settings()`` has the
        nested-with-metadata shape ``{dotted_key: {"value": ..., "ui_element":
        ..., "type": ..., ...}}``. The ordinary ``redact()`` method does not
        catch secrets in this shape: the outer dotted key (e.g.
        ``"llm.openai.api_key"``) is not in the sensitive-name set (only the
        suffix ``"api_key"`` is), and the inner key ``"value"`` is not
        sensitive — so the secret survives unredacted.
        ``redact_settings_snapshot`` handles the shape correctly:

        - Replaces ``entry["value"]`` with ``redaction_text`` when the entry
          is sensitive (``ui_element == "password"`` OR the last dotted
          segment of the outer key matches a sensitive name).
        - Preserves all metadata (``ui_element``, ``type``, ``description``,
          etc.) so YAML diffs can still show "this key existed."
        - Leaves empty values (``None``, ``""``, ``[]``, ``{}``) unredacted
          so diffs of "unset" settings stay readable.
        - Pure function: does not mutate the input.

        Entries that don't have the metadata-wrapper shape (e.g. mixed
        snapshots that contain bare values) are passed through untouched —
        this is intentional so the helper is safe to call on any dict
        without crashing.

        Args:
            snapshot: A settings snapshot dict.
            sensitive_keys: Override the default set of sensitive name
                suffixes. Defaults to ``DataSanitizer.DEFAULT_SENSITIVE_KEYS``.
            redaction_text: Replacement string for redacted values.

        Returns:
            New dict with secret values replaced.

        Example:
            >>> snap = {"llm.openai.api_key": {"value": "sk-x", "ui_element": "password"}}
            >>> DataSanitizer.redact_settings_snapshot(snap)
            {'llm.openai.api_key': {'value': '[REDACTED]', 'ui_element': 'password'}}
        """
        if not isinstance(snapshot, dict):
            return snapshot

        out: dict = {}
        for key, entry in snapshot.items():
            if not isinstance(entry, dict) or "value" not in entry:
                out[key] = entry
                continue
            new_entry = dict(entry)  # shallow copy preserves metadata
            # Delegate the per-value rule to redact_value so the snapshot,
            # the singular GET and the bulk GET can never mask differently.
            new_entry["value"] = DataSanitizer.redact_value(
                key,
                entry.get("ui_element"),
                entry.get("value"),
                sensitive_keys,
                redaction_text,
            )
            out[key] = new_entry
        return out


# Convenience functions for direct use
def sanitize_data(data: Any, sensitive_keys: Set[str] | None = None) -> Any:
    """
    Remove sensitive keys from data structures.

    Convenience function that calls DataSanitizer.sanitize().

    Args:
        data: The data structure to sanitize
        sensitive_keys: Optional set of sensitive key names

    Returns:
        Sanitized copy of the data
    """
    return DataSanitizer.sanitize(data, sensitive_keys)


def redact_data(
    data: Any,
    sensitive_keys: Set[str] | None = None,
    redaction_text: str = REDACTION_TEXT,
) -> Any:
    """
    Redact (replace) sensitive values in data structures.

    Convenience function that calls DataSanitizer.redact().

    Args:
        data: The data structure to redact
        sensitive_keys: Optional set of sensitive key names
        redaction_text: Text to replace sensitive values with

    Returns:
        Copy of the data with sensitive values redacted
    """
    return DataSanitizer.redact(data, sensitive_keys, redaction_text)


def filter_research_metadata(research_meta: Any) -> dict:
    """Filter research_meta to only safe fields for history list API responses.

    Uses an allowlist approach to prevent leaking settings_snapshot
    (which contains API keys, passwords, tokens) to the frontend.
    History list consumers only need is_news_search from metadata.

    Args:
        research_meta: Raw research metadata (dict, JSON string, or None)

    Returns:
        dict with only safe fields extracted (currently: is_news_search)
    """
    try:
        meta = research_meta or {}
        if isinstance(meta, str):
            meta = json.loads(meta)
        if not isinstance(meta, dict):
            return {"is_news_search": False}
        return {
            "is_news_search": bool(meta.get("is_news_search", False)),
        }
    except (json.JSONDecodeError, TypeError, AttributeError):
        return {"is_news_search": False}


def strip_settings_snapshot(research_meta: Any) -> dict:
    """Remove settings_snapshot from research_meta for API responses.

    settings_snapshot contains all application settings including API keys.
    This strips it while preserving all other metadata fields that the
    frontend needs (phase, error_type, processed_query, mode, duration, etc.).

    Args:
        research_meta: Raw research metadata (dict, JSON string, or None)

    Returns:
        Copy of the dict with settings_snapshot removed
    """
    try:
        meta = research_meta or {}
        if isinstance(meta, str):
            meta = json.loads(meta)
        if not isinstance(meta, dict):
            return {}
        return {k: v for k, v in meta.items() if k != "settings_snapshot"}
    except (json.JSONDecodeError, TypeError, AttributeError):
        return {}
