"""
Runtime configuration store.

Secret Policy
=============
1. Config payloads MUST NOT contain raw secrets.
2. Validation rejects direct secret fields (api_key, password, etc.).
3. Secrets are materialized at runtime by SecretProvider only.
4. Secrets MUST NOT appear in logs, Redis messages, config cache, or CLI output.
5. Debug commands redact sensitive values by default (``--show-secrets`` to reveal).
"""
from __future__ import annotations

from dataclasses import dataclass, field
from threading import Lock
from urllib.parse import urlparse
import json
import logging
import os
import re
from typing import Any, Dict, List, Optional

from actor.schemas import load_schema

logger = logging.getLogger(__name__)


def parse_config_payload(msg: "Dict[str, Any] | str") -> Dict[str, Any]:
    """Parse a raw Redis config message into a dict with decoded nested sections.

    Redis XADD stores values as strings; the orchestrator may JSON-encode nested
    objects.  This helper decodes known config sections when they arrive as strings.
    """
    if isinstance(msg, str):
        try:
            msg = json.loads(msg)
        except Exception as exc:
            logger.error("Config message is not valid JSON: %r", msg, exc_info=exc)
            raise
    parsed = dict(msg)
    payload = parsed.get("payload")
    if isinstance(payload, str):
        try:
            payload_obj = json.loads(payload)
            if isinstance(payload_obj, dict):
                parsed.update(payload_obj)
        except Exception as exc:
            logger.warning("Failed to parse config payload field as JSON: %r", payload, exc_info=exc)
    for key in ("secret_store", "database", "llm"):
        val = parsed.get(key)
        if isinstance(val, str):
            try:
                parsed[key] = json.loads(val)
            except Exception as exc:
                logger.warning(
                    "Failed to parse config section '%s' as JSON (type=%s). Raw value: %r",
                    key,
                    type(val).__name__,
                    val,
                    exc_info=exc,
                )
                # Leave as-is if parsing fails; validation will catch it.
    _sanitize_config(parsed)
    _normalize_config(parsed)
    return parsed


def _normalize_config(cfg: Dict[str, Any]) -> None:
    """Normalize config fields and aliases in-place."""
    db = cfg.get("database")
    if isinstance(db, dict):
        auth_type = db.get("auth_type")
        if auth_type == "keypair":
            db["auth_type"] = "key_pair"
        db_type = db.get("type")
        if isinstance(db_type, str):
            db_type_map = {
                "postgres": "postgresql",
                "mssql": "sqlserver",
            }
            db["type"] = db_type_map.get(db_type, db_type)
        alias_map = {
            "user": "username",
            "db": "database",
            "db_name": "database",
            "hostname": "host",
            "address": "host",
            "aws_region": "region",
            "project": "project_id",
            "credentials_json": "credentials_path",
            "service_account_json": "credentials_path",
            "aws_access_key_id": "access_key_id",
        }
        for src, dst in alias_map.items():
            if src in db and dst not in db:
                db[dst] = db.pop(src)
        smap = db.get("secret_mapping")
        if isinstance(smap, dict):
            key_aliases = {
                "user": "username",
                "db": "database",
                "db_name": "database",
                "hostname": "host",
                "address": "host",
                "aws_access_key_id": "access_key_id",
                "aws_secret_access_key": "secret_access_key",
                "credentials_json": "credentials_path",
                "service_account_json": "credentials_path",
                "jwt": "jwt_token",
                "access_token": "token",
            }
            for src, dst in key_aliases.items():
                if src in smap and dst not in smap:
                    smap[dst] = smap.pop(src)
    llm = cfg.get("llm")
    if isinstance(llm, dict):
        smap = llm.get("secret_mapping")
        if isinstance(smap, dict):
            if "bearer_token" in smap and "token" not in smap:
                smap["token"] = smap.pop("bearer_token")


def _sanitize_config(cfg: Dict[str, Any]) -> None:
    """Strip non-config keys and normalize secret store shapes."""
    for section_key in ("database", "llm"):
        section = cfg.get(section_key)
        if isinstance(section, dict):
            _strip_internal_keys(section)
            # Secret store override may be empty or missing provider; drop if empty.
            sso = section.get("secret_store_override")
            if isinstance(sso, dict):
                sso_clean = _normalize_secret_store(sso)
                if sso_clean is None:
                    section.pop("secret_store_override", None)
                else:
                    section["secret_store_override"] = sso_clean
            # Component-level secret_store may be present; normalize it too.
            ss = section.get("secret_store")
            if isinstance(ss, dict):
                ss_clean = _normalize_secret_store(ss)
                if ss_clean is None:
                    section.pop("secret_store", None)
                else:
                    section["secret_store"] = ss_clean
    secret_store = cfg.get("secret_store")
    if isinstance(secret_store, dict):
        clean = _normalize_secret_store(secret_store)
        if clean is None:
            cfg.pop("secret_store", None)
        else:
            cfg["secret_store"] = clean


def _strip_internal_keys(section: Dict[str, Any]) -> None:
    """Remove Mongoose/internal metadata keys from a section."""
    for key in list(section.keys()):
        if key.startswith("$") or key in {"_doc", "__v"}:
            section.pop(key, None)
    # If a nested _doc exists, prefer its contents.
    doc = section.get("_doc")
    if isinstance(doc, dict):
        for k, v in doc.items():
            if k not in section:
                section[k] = v
        section.pop("_doc", None)


def _normalize_secret_store(store: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    """Prune empty secret store configs and strip unrelated provider configs."""
    provider = store.get("provider")
    if not provider:
        # Empty or invalid: drop entirely.
        return None
    provider = str(provider)
    cleaned = dict(store)
    if provider == "env":
        cleaned.pop("vault", None)
        cleaned.pop("file", None)
        cleaned.pop("k8s", None)
    if provider != "vault":
        cleaned.pop("vault", None)
    if provider != "file":
        cleaned.pop("file", None)
    if provider != "k8s":
        cleaned.pop("k8s", None)
    return cleaned


def validate_config_schema(cfg: Dict[str, Any]) -> None:
    """Validate config payload against JSON schema."""
    try:
        import jsonschema
    except Exception as exc:  # pragma: no cover - external dep
        raise ValueError("jsonschema library required for config validation") from exc
    schema = load_schema("config_message.schema.json")
    jsonschema.validate(instance=cfg, schema=schema)


@dataclass
class LLMConfig:
    endpoint: str
    auth_type: str  # api_key|basic_auth|bearer|azure_key|no_auth
    api_key: Optional[str] = None
    username: Optional[str] = None
    password: Optional[str] = None
    api_version: Optional[str] = None
    deployment_name: Optional[str] = None
    model: str = ""
    token_limit: int = 4096
    secret_store: Optional[Dict[str, Any]] = None
    secret_mapping: Optional[Dict[str, str]] = None


@dataclass
class DatabaseConfig:
    database_type: Optional[str] = None
    url: Optional[str] = None
    connect_args: Dict[str, Any] = field(default_factory=dict)
    default_schema: Optional[str] = None
    auth_type: Optional[str] = None
    secret_mapping: Optional[Dict[str, str]] = None
    secret_store: Optional[Dict[str, Any]] = None
    literals: Dict[str, Any] = field(default_factory=dict)


class RuntimeConfig:
    """
    In-memory runtime configuration for DB and LLM connectors.
    Updated via SaaS-delivered config messages.
    """

    def __init__(self) -> None:
        self._lock = Lock()
        self.llm: Optional[LLMConfig] = None
        self.database: Optional[DatabaseConfig] = None
        self.llm_passthrough: bool = True
        self.version: int = 0
        self.strict_reachability: bool = False

    def update_from_config_message(self, cfg: Dict[str, Any]) -> None:
        with self._lock:
            llm = cfg.get("llm") or {}
            if llm.get("enabled") and not llm.get("use_saas"):
                auth_type = llm.get("auth_type") or "api_key"
                self.llm = LLMConfig(
                    endpoint=llm.get("endpoint", ""),
                    auth_type=auth_type,
                    api_key=llm.get("api_key"),
                    username=llm.get("username"),
                    password=llm.get("password"),
                    api_version=llm.get("api_version"),
                    deployment_name=llm.get("deployment_name"),
                    model=llm.get("model", ""),
                    token_limit=llm.get("token_limit", 4096),
                    secret_store=llm.get("secret_store_override") or llm.get("secret_store") or cfg.get("secret_store"),
                    secret_mapping=llm.get("secret_mapping"),
                )
            self.llm_passthrough = llm.get("llm_passthrough", True)
            database = cfg.get("database") or {}
            if database.get("enabled"):
                database_type = database.get("type")
                literals = {
                    k: v for k, v in database.items()
                    if k not in {
                        "enabled", "type", "auth_type", "secret_mapping",
                        "secret_store", "secret_store_override", "url",
                        "connect_args", "default_schema",
                    }
                }
                url = database.get("url")
                connect_args = database.get("connect_args") or {}
                default_schema = database.get("default_schema")
                auth_type = database.get("auth_type")
                secret_mapping = database.get("secret_mapping")
                secret_store = database.get("secret_store_override") or database.get("secret_store") or cfg.get("secret_store")
                self.database = DatabaseConfig(
                    database_type=database_type,
                    url=url,
                    connect_args=connect_args,
                    default_schema=default_schema,
                    auth_type=auth_type,
                    secret_mapping=secret_mapping,
                    secret_store=secret_store,
                    literals=literals,
                )
            incoming_version = cfg.get("version")
            if incoming_version is not None:
                self.version = int(incoming_version)
            else:
                self.version += 1

    def get_llm(self) -> Optional[LLMConfig]:
        with self._lock:
            return self.llm

    def get_database(self) -> Optional[DatabaseConfig]:
        with self._lock:
            return self.database

    def get_version(self) -> int:
        with self._lock:
            return self.version


_API_AUTH_REQUIRED = {
    "api_key": {"api_key"},
    "azure_key": {"api_key", "api_version", "deployment_name"},
    "bearer": {"token"},
    "basic_auth": {"username", "password"},
    "no_auth": set(),
}

_SENSITIVE_API_FIELDS = {
    "api_key",
    "password",
    "secret_key",
    "token",
    "bearer_token",
    "jwt",
    "passphrase",
    "username",
}

_DB_AUTH_REQUIRED = {
    "password": {"username", "password"},
    "scram": {"username", "password"},
    "ssl_verify": {"username", "password"},
    "ssl_cert": {"sslcert", "sslkey"},
    "service_account": {"credentials_path"},
    "iam_role": {"db_user", "cluster_identifier", "region"},
    "key_pair": {"username", "private_key"},
    "token": {"token"},
}

_DB_TYPE_COMMON_REQUIRED = {
    "postgresql": {"host", "port", "database", "schema"},
    "mysql": {"host", "port", "database"},
    "snowflake": {"account", "warehouse", "database", "schema"},
    "redshift": {"host", "port", "database", "schema"},
    "bigquery": {"project_id"},
    "sqlserver": {"host", "port", "database", "driver"},
    "oracle": {"host", "port", "service_name"},
    "clickhouse": {"host", "port", "database"},
    "trino": {"host", "port", "catalog", "schema"},
    "databricks": {"host", "http_path", "schema"},
}

_SENSITIVE_DB_FIELDS = {
    "password",
    "api_key",
    "secret_key",
    "aws_secret_access_key",
    "token",
    "jwt",
    "cert_content",
    "key_content",
    "passphrase",
    "bearer_token",
}


def _validate_api_config(
    section_name: str,
    section: Dict[str, Any],
    cfg: Dict[str, Any],
    errors: List[str],
    endpoint_pattern: Optional["re.Pattern[str]"],
    model_allowlist: List[str],
) -> None:
    """Shared validation for LLM config sections."""
    secret_mapping = section.get("secret_mapping") or {}
    secret_store = section.get("secret_store_override") or section.get("secret_store") or cfg.get("secret_store")
    if secret_mapping and not secret_store:
        errors.append(f"{section_name} requires secret_store when secret_mapping is provided")
    for key in _SENSITIVE_API_FIELDS:
        if key in section:
            errors.append(f"{section_name}.{key} must be provided via secret_mapping")
    auth_type = section.get("auth_type") or "api_key"
    if section_name == "llm" and not section.get("use_saas"):
        for key in ("endpoint", "model"):
            if key not in section and key not in secret_mapping:
                errors.append(f"{section_name} missing required field: {key}")
    if auth_type in _API_AUTH_REQUIRED:
        required = _API_AUTH_REQUIRED[auth_type]
        for key in required:
            if key in _SENSITIVE_API_FIELDS:
                if key not in secret_mapping:
                    errors.append(f"{section_name}.secret_mapping missing sensitive key: {key}")
            else:
                if key not in section and key not in secret_mapping:
                    errors.append(f"{section_name} missing required field: {key}")
    if section.get("endpoint"):
        parsed = urlparse(section["endpoint"])
        if parsed.scheme not in ("http", "https") or not parsed.netloc:
            errors.append(f"{section_name}.endpoint must be http/https with host")
        if endpoint_pattern and not endpoint_pattern.match(section["endpoint"]):
            errors.append(f"{section_name}.endpoint does not match allowed pattern")
    if section.get("model") and len(section["model"]) > 128:
        errors.append(f"{section_name}.model too long")
    if section.get("model") and model_allowlist and section["model"] not in model_allowlist:
        errors.append(f"{section_name}.model not in allowed list")


def _validate_database_config(
    database: Dict[str, Any],
    cfg: Dict[str, Any],
    errors: List[str],
) -> None:
    """Validate the database section of a config payload."""
    if not database.get("type"):
        errors.append("database.type required when database enabled")
    if not database.get("auth_type"):
        errors.append("database.auth_type required when database enabled")
    for key in _SENSITIVE_DB_FIELDS:
        if key in database:
            errors.append(f"database.{key} must be provided via secret_mapping")
    secret_mapping = database.get("secret_mapping") or {}
    secret_store = database.get("secret_store_override") or database.get("secret_store") or cfg.get("secret_store")
    if secret_mapping and not secret_store:
        errors.append("secret_store required when database.secret_mapping is provided")
    db_type = database.get("type")
    if db_type in _DB_TYPE_COMMON_REQUIRED:
        for key in _DB_TYPE_COMMON_REQUIRED[db_type]:
            if key in _SENSITIVE_DB_FIELDS:
                if key not in secret_mapping:
                    errors.append(f"database.secret_mapping missing sensitive key: {key}")
            else:
                if key not in database and key not in secret_mapping:
                    errors.append(f"database missing required field: {key}")
    auth_type = database.get("auth_type")
    if auth_type in _DB_AUTH_REQUIRED:
        required = _DB_AUTH_REQUIRED[auth_type]
        for key in required:
            if key in _SENSITIVE_DB_FIELDS:
                if key not in secret_mapping:
                    errors.append(f"database.secret_mapping missing sensitive key: {key}")
            else:
                if key not in database and key not in secret_mapping:
                    errors.append(f"database missing required field: {key}")
    if auth_type == "ssl_cert":
        cert_keys = ("sslcert", "ssl_cert")
        key_keys = ("sslkey", "ssl_key")
        has_cert = any(k in database or k in secret_mapping for k in cert_keys)
        has_key = any(k in database or k in secret_mapping for k in key_keys)
        if not has_cert:
            errors.append("database missing required ssl cert (sslcert or ssl_cert)")
        if not has_key:
            errors.append("database missing required ssl key (sslkey or ssl_key)")


def _compile_endpoint_pattern(name: str, errors: List[str], max_len: int = 200) -> Optional["re.Pattern[str]"]:
    """Compile an endpoint pattern env var or append errors."""
    pat_src = os.getenv(name)
    if not pat_src:
        return None
    if len(pat_src) > max_len:
        errors.append(f"{name} exceeds {max_len} chars (ReDoS guard)")
        return None
    try:
        return re.compile(pat_src)
    except re.error as exc:
        errors.append(f"{name} is not a valid regex: {exc}")
        return None


def _summarize_section(section: Dict[str, Any], sensitive_keys: set[str]) -> Dict[str, Any]:
    summary: Dict[str, Any] = {"keys": sorted(section.keys())}
    if "secret_mapping" in section and isinstance(section["secret_mapping"], dict):
        summary["secret_mapping_keys"] = sorted(section["secret_mapping"].keys())
    for key in ("type", "auth_type", "endpoint", "model", "url"):
        if key in section:
            summary[key] = "<redacted>" if key in sensitive_keys else section.get(key)
    return summary


def validate_config_payload(cfg: Dict[str, Any]) -> None:
    """Validate that enabled sections contain required fields."""
    errors: List[str] = []
    try:
        validate_config_schema(cfg)
    except Exception as exc:
        errors.append(f"config schema invalid: {exc}")
    strict_reachability = bool(os.getenv("STRICT_REACHABILITY", "").lower() in ("1", "true", "yes"))
    compiled_llm_pat = _compile_endpoint_pattern("LLM_ENDPOINT_PATTERN", errors)
    llm_model_allowlist = (os.getenv("LLM_MODEL_ALLOWLIST") or "").split(",") if os.getenv("LLM_MODEL_ALLOWLIST") else []
    # -- secret store validation -----------------------------------------
    secret_store = cfg.get("secret_store") or {}
    provider = secret_store.get("provider")
    if provider == "vault":
        vault = secret_store.get("vault") or {}
        if not vault.get("url") or not vault.get("token_path"):
            errors.append("secret_store.vault requires url and token_path")
        token_path = vault.get("token_path")
        if token_path and not os.path.exists(token_path):
            errors.append(f"vault token_path not found: {token_path}")
        if strict_reachability and token_path and not os.access(token_path, os.R_OK):
            errors.append(f"vault token_path not readable: {token_path}")
    if provider == "file":
        file_cfg = secret_store.get("file") or {}
        if not file_cfg.get("path"):
            errors.append("secret_store.file requires path")
        path = file_cfg.get("path")
        if path and not os.path.exists(path):
            errors.append(f"secret_store.file.path not found: {path}")
        if strict_reachability and path and not os.access(path, os.R_OK):
            errors.append(f"secret_store.file.path not readable: {path}")
    if provider == "k8s":
        k8s_cfg = secret_store.get("k8s") or {}
        if not k8s_cfg.get("namespace"):
            errors.append("secret_store.k8s requires namespace")
        if not k8s_cfg.get("default_secret") and not k8s_cfg.get("context") and not k8s_cfg.get("kubeconfig_path"):
            errors.append("secret_store.k8s requires at least one of: default_secret, context, or kubeconfig_path")
    # -- llm -------------------------------------------------------------
    llm = cfg.get("llm") or {}
    if llm.get("enabled") and not llm.get("use_saas"):
        _validate_api_config("llm", llm, cfg, errors, compiled_llm_pat, llm_model_allowlist)
    # -- database --------------------------------------------------------
    database = cfg.get("database") or {}
    if database.get("enabled"):
        _validate_database_config(database, cfg, errors)
    if errors:
        try:
            db_summary = _summarize_section(database, _SENSITIVE_DB_FIELDS) if isinstance(database, dict) else {}
            llm_summary = _summarize_section(llm, _SENSITIVE_API_FIELDS) if isinstance(llm, dict) else {}
            logger.warning(
                "Config validation failed. db=%s llm=%s errors=%s",
                db_summary,
                llm_summary,
                errors,
            )
        except Exception:
            pass
        raise ValueError(f"Invalid config payload: {', '.join(errors)}")
