"""cortex/api/routes/admin/config_status.py — Config Observatory: fleet config status.

Implements GET /api/v1/admin/fleet/config-status, which returns Cortex's own
running configuration in a normalized, self-documenting JSON schema.

This is the data layer for the Config Observatory feature (#1074-#1077):
- No fleet integration (that's #1076) — Cortex reports its own config only.
- All config fields grouped into logical sections with full field metadata.
- Sensitive fields (API keys, secrets, tokens) are always redacted.
- Runtime info (Python version, package version) included for observability.
"""

from __future__ import annotations

import platform
import re
import sys
from dataclasses import fields as dataclass_fields
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Set

from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse

from cortex import __version__
from cortex.api.deps import PREFIX, get_plugin

# ---------------------------------------------------------------------------
# Schema version — bump when the response structure changes incompatibly.
# ---------------------------------------------------------------------------
SCHEMA_VERSION = "1.0"

# ---------------------------------------------------------------------------
# Sensitive field names — values are always redacted in the response.
# A field is treated as sensitive if its name matches any of these patterns.
#
# The regex approach catches compound names like ``voyage_key``,
# ``openai_key``, ``private_key``, ``api_secret``, etc. that the old
# substring list ("api_key", "secret", ...) would miss.  The ``_key``
# suffix pattern uses a word-boundary/underscore anchor to avoid false
# positives on words like "monkey" or "keyboard".
# ---------------------------------------------------------------------------
_SENSITIVE_SUBSTRINGS: Set[str] = {
    "api_key",
    "secret",
    "password",
    "credential",
    # Note: "token" was too broad (matched "retrieval_max_tokens",
    # "token_budget_*").  Suffix-based matching is handled by
    # _SENSITIVE_RE below.  Kept here only as "auth_token" for
    # backward compat with fields that embed "auth_token" as a substring.
    "auth_token",
}

# Compiled regex: matches fields ending with _key, _secret, _token,
# _password, _credential, or containing api_key/private_key anywhere.
_SENSITIVE_RE = re.compile(
    r"(?:_key|_secret|_token|_password|_credential)$"
    r"|api_key|private_key",
    re.IGNORECASE,
)

# ---------------------------------------------------------------------------
# Section mapping — each entry maps a section name to the list of config
# field name prefixes that belong to that section.  Order matters: the first
# matching prefix wins.
# ---------------------------------------------------------------------------
_SECTION_PREFIXES: List[tuple[str, list[str]]] = [
    ("storage", ["storage_", "db_pool_"]),
    ("embeddings", ["embedding_", "provider_embedding_"]),
    ("llm_providers", [
        "provider_llm_", "provider_extraction_", "provider_reconciliation_",
        "provider_dialectic_", "provider_mode", "provider_usage_",
        "llm_provider_priority",
    ]),
    ("extraction", ["extraction_", "extract_", "coding_noise_filter", "known_subjects", "max_claims_", "max_entities_", "max_concurrent_", "max_extraction_"]),
    ("reconciliation", ["reconciliation_"]),
    ("dialectic", ["dialectic_"]),
    ("rerank", ["retrieval_rerank_"]),  # more specific — must come before retrieval
    ("retrieval", ["retrieval_"]),
    ("token_budgets", ["token_budget_", "cornerstone_token_budget", "retrieval_token_budget", "total_memory_token_budget"]),
    ("cornerstones", ["cornerstone_", "anti_compression_"]),
    ("drift", ["drift_", "anti_compression_confidence"]),
    ("circadian", ["dream_cycle_", "auto_sleep_", "fatigue_", "deep_dream_"]),
    ("session", ["session_", "promoted_", "open_loop_"]),
    ("auth", ["auth_"]),
    ("rate_limiting", ["rate_limit_"]),
    ("working_memory", ["working_memory_"]),
    ("model_routing", ["model_routing_"]),
    ("context_compiler", ["context_compiler_"]),
    ("re_retrieval", ["re_retrieval_"]),
    ("circuit_breaker", ["circuit_breaker_"]),
    ("brain_graph", ["brain_graph_"]),
    ("metrics", ["metrics_", "quality_gates_", "feature_flags_", "stats_"]),
    ("webhooks", ["webhook_"]),
    ("aegis", ["aegis_"]),
    ("deprecation", ["deprecation_"]),
    ("api", ["max_remember_body_bytes", "max_page_size", "log_retention_days", "max_llm_calls_"]),
    ("misc", []),  # catch-all
]

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _is_sensitive(field_name: str) -> bool:
    """Return True if the field should have its value redacted.

    Uses both the legacy substring set (for backward compatibility) and a
    compiled regex that catches compound suffixes like ``voyage_key``,
    ``openai_key``, ``private_key``, ``api_secret``, etc.
    """
    lower = field_name.lower()
    if any(sub in lower for sub in _SENSITIVE_SUBSTRINGS):
        return True
    return bool(_SENSITIVE_RE.search(lower))


def _section_for_field(field_name: str) -> str:
    """Return the section name for the given field name.

    The first matching prefix in ``_SECTION_PREFIXES`` wins, so more-specific
    prefixes (e.g. ``retrieval_rerank_``) must appear before less-specific
    ones (e.g. ``retrieval_``).
    """
    for section, prefixes in _SECTION_PREFIXES:
        if section == "misc":
            continue
        for prefix in prefixes:
            if field_name.startswith(prefix) or field_name == prefix.rstrip("_"):
                return section
    return "misc"


def _type_name(type_hint: Any) -> str:
    """Convert a type annotation to a human-readable string."""
    if type_hint is None:
        return "unknown"
    if isinstance(type_hint, str):
        # Forward reference — return as-is
        return type_hint
    origin = getattr(type_hint, "__origin__", None)
    if origin is list:
        args = getattr(type_hint, "__args__", ())
        inner = _type_name(args[0]) if args else "Any"
        return f"list[{inner}]"
    if origin is dict:
        args = getattr(type_hint, "__args__", ())
        if len(args) == 2:
            return f"dict[{_type_name(args[0])}, {_type_name(args[1])}]"
        return "dict"
    # Optional[X] = Union[X, None]
    import typing
    if origin is typing.Union:
        args = getattr(type_hint, "__args__", ())
        non_none = [a for a in args if a is not type(None)]
        if len(non_none) == 1:
            return f"Optional[{_type_name(non_none[0])}]"
        return f"Union[{', '.join(_type_name(a) for a in non_none)}]"
    return getattr(type_hint, "__name__", str(type_hint))


def _field_default(f) -> Any:
    """Return the field default value (or None if no simple default)."""
    import dataclasses
    if f.default is not dataclasses.MISSING:
        return f.default
    if f.default_factory is not dataclasses.MISSING:  # type: ignore[misc]
        try:
            return f.default_factory()
        except Exception:
            return None
    return None


def _env_var_name(field_name: str) -> str:
    """Return the expected environment variable name for a config field.

    Convention: CORTEX_<FIELD_NAME_UPPER>, e.g. CORTEX_EMBEDDING_MODEL.
    Exceptions for well-known env vars are documented in the docstrings.
    """
    # Special-case env vars that deviate from the CORTEX_ convention
    _special: Dict[str, str] = {
        "auth_jwt_secret": "SUPABASE_JWT_SECRET",
        "auth_api_key": "CORTEX_API_KEY",
        "provider_llm_api_key": "OPENAI_API_KEY",
        "provider_embedding_api_key": "VOYAGE_API_KEY",
        "aegis_sentry_secret": "AEGIS_SENTRY_SECRET",
    }
    if field_name in _special:
        return _special[field_name]
    return f"CORTEX_{field_name.upper()}"


def _build_field_entry(f, cfg_value: Any) -> Dict[str, Any]:
    """Build the metadata dict for a single config field."""
    sensitive = _is_sensitive(f.name)
    value = "***" if sensitive and cfg_value else cfg_value
    if sensitive and cfg_value == "":
        value = ""  # empty string means "not configured" — safe to show

    default = _field_default(f)
    if _is_sensitive(f.name) and default:
        default = "***"

    # Extract description from field docstring convention (dataclass has no
    # built-in per-field docstring; Cortex uses the next-line string literal
    # pattern).  We can't easily recover it at runtime, so we skip it here.
    return {
        "name": f.name,
        "value": value,
        "default": default,
        "type": _type_name(f.type),
        "env_var": _env_var_name(f.name),
        "sensitive": sensitive,
    }


def _build_config_sections(cfg) -> Dict[str, Any]:
    """Return the full config as nested sections with field metadata."""
    sections: Dict[str, List[Dict[str, Any]]] = {
        section: [] for section, _ in _SECTION_PREFIXES
    }

    cfg_values = {f.name: getattr(cfg, f.name, None) for f in dataclass_fields(cfg)}

    for f in dataclass_fields(cfg):
        section = _section_for_field(f.name)
        entry = _build_field_entry(f, cfg_values[f.name])
        sections.setdefault(section, []).append(entry)

    # Drop empty sections
    return {k: v for k, v in sections.items() if v}


# ---------------------------------------------------------------------------
# Route registration
# ---------------------------------------------------------------------------

def _register(router: APIRouter, verify_api_key) -> None:
    """Register config-status routes on the given router."""

    @router.get(
        f"{PREFIX}/admin/fleet/config-status",
        summary="Config Observatory — Cortex config status",
        tags=["admin"],
        operation_id="get_fleet_config_status",
    )
    async def get_fleet_config_status(
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Return Cortex's running configuration in a normalized, self-documenting schema.

        Provides a complete snapshot of all active config knobs grouped by
        functional section, with field metadata (name, value, default, type,
        env_var, sensitive flag).

        This is the data foundation for the Config Observatory dashboard and
        Eva's config-observatory skill.  Fleet-level aggregation (comparing
        configs across nodes) is out of scope here — see issue #1076.

        **Sensitive fields** (api_key, secret, password, credential, token) are
        always redacted; their values appear as `"***"` in the response.  Empty
        sensitive fields (i.e. "not configured") are shown as `""`.

        **Schema version:** Increment ``SCHEMA_VERSION`` when the response shape
        changes incompatibly so consumers can adapt gracefully.
        """
        cfg = plugin.config

        return JSONResponse(content={
            "schema_version": SCHEMA_VERSION,
            "node": "cortex",
            "version": __version__,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "runtime": {
                "python_version": platform.python_version(),
                "python_impl": platform.python_implementation(),
                "platform": platform.system(),
                "machine": platform.machine(),
            },
            "config": _build_config_sections(cfg),
            "_meta": {
                "description": (
                    "Each config section contains an array of field objects. "
                    "Fields with sensitive=true have their value redacted to '***'. "
                    "env_var is the environment variable that overrides this field."
                ),
                "field_schema": {
                    "name": "string — config field name",
                    "value": "any — current runtime value (redacted for sensitive fields)",
                    "default": "any — package default value",
                    "type": "string — Python type annotation",
                    "env_var": "string — environment variable name",
                    "sensitive": "boolean — true if value is redacted",
                },
            },
        })
