"""Observe config parsing helpers."""
from __future__ import annotations

import os
import re
from typing import Any

from logos_adk.errors import ConfigError
from logos_adk.observe import (
    ConsoleExporter,
    Exporter,
    InMemoryExporter,
    LangfuseExporter,
    NativeObservabilityExporter,
    OpenTelemetryExporter,
)


_ENV_REF_RE = re.compile(r"^\$\{([A-Z0-9_]+)\}$")


def resolve_config_secret(value: str, *, field_name: str) -> str:
    """Resolve env-backed secret strings for observe config fields."""
    if value.startswith("env:"):
        env_name = value[4:]
    else:
        match = _ENV_REF_RE.match(value)
        env_name = match.group(1) if match else None

    if not env_name:
        return value

    resolved = os.getenv(env_name)
    if not resolved:
        raise ConfigError(
            f"{field_name} references environment variable '{env_name}', but it is not set"
        )
    return resolved


def load_observers_from_config(config: Any) -> list[Exporter]:
    """Build observability exporters from YAML config."""
    if config is None:
        return []

    if isinstance(config, str):
        if config == "console":
            return [ConsoleExporter()]
        if config in {"memory", "in_memory", "inmemory"}:
            return [InMemoryExporter()]
        if config == "native":
            return [NativeObservabilityExporter()]
        raise ConfigError(f"Unsupported observe exporter: {config}")

    if not isinstance(config, dict):
        raise ConfigError("observe config must be a supported exporter name or a mapping")

    exporter_name = config.get("exporter")
    if exporter_name == "console":
        unknown_keys = set(config) - {"exporter", "log_level"}
        if unknown_keys:
            unknown = ", ".join(sorted(unknown_keys))
            raise ConfigError(f"Unknown observe config fields: {unknown}")
        return [ConsoleExporter()]
    if exporter_name == "native":
        unknown_keys = set(config) - {"exporter"}
        if unknown_keys:
            unknown = ", ".join(sorted(unknown_keys))
            raise ConfigError(f"Unknown observe config fields: {unknown}")
        return [NativeObservabilityExporter()]
    if exporter_name in {"memory", "in_memory", "inmemory"}:
        unknown_keys = set(config) - {"exporter"}
        if unknown_keys:
            unknown = ", ".join(sorted(unknown_keys))
            raise ConfigError(f"Unknown observe config fields: {unknown}")
        return [InMemoryExporter()]
    if exporter_name == "opentelemetry":
        endpoint = config.get("endpoint")
        if not isinstance(endpoint, str) or not endpoint:
            raise ConfigError("OpenTelemetry observe exporter requires a non-empty endpoint")
        unknown_keys = set(config) - {
            "exporter",
            "endpoint",
            "timeout",
            "batch_size",
            "max_retries",
            "backoff_seconds",
            "headers",
            "bearer_token",
            "compression",
            "log_level",
        }
        if unknown_keys:
            unknown = ", ".join(sorted(unknown_keys))
            raise ConfigError(f"Unknown observe config fields: {unknown}")
        timeout = config.get("timeout", 5.0)
        if not isinstance(timeout, (int, float)):
            raise ConfigError("OpenTelemetry observe timeout must be numeric")
        batch_size = config.get("batch_size", 100)
        if not isinstance(batch_size, int) or batch_size <= 0:
            raise ConfigError("OpenTelemetry observe batch_size must be a positive integer")
        max_retries = config.get("max_retries", 3)
        if not isinstance(max_retries, int) or max_retries < 0:
            raise ConfigError("OpenTelemetry observe max_retries must be a non-negative integer")
        backoff_seconds = config.get("backoff_seconds", 0.25)
        if not isinstance(backoff_seconds, (int, float)) or backoff_seconds < 0:
            raise ConfigError("OpenTelemetry observe backoff_seconds must be non-negative")
        headers = config.get("headers", {})
        if not isinstance(headers, dict) or not all(
            isinstance(key, str) and isinstance(value, str)
            for key, value in headers.items()
        ):
            raise ConfigError("OpenTelemetry observe headers must be a string-to-string mapping")
        resolved_headers = {
            key: resolve_config_secret(value, field_name=f"observe.headers.{key}")
            for key, value in headers.items()
        }
        bearer_token = config.get("bearer_token")
        if bearer_token is not None and (not isinstance(bearer_token, str) or not bearer_token):
            raise ConfigError("OpenTelemetry observe bearer_token must be a non-empty string")
        resolved_bearer_token = None
        if bearer_token is not None:
            resolved_bearer_token = resolve_config_secret(
                bearer_token,
                field_name="observe.bearer_token",
            )
        compression = config.get("compression", "none")
        if compression not in {"none", "gzip"}:
            raise ConfigError("OpenTelemetry observe compression must be 'none' or 'gzip'")
        return [
            OpenTelemetryExporter(
                endpoint=endpoint,
                timeout=float(timeout),
                batch_size=batch_size,
                max_retries=max_retries,
                backoff_seconds=float(backoff_seconds),
                headers=resolved_headers,
                bearer_token=resolved_bearer_token,
                compression=compression,
            )
        ]
    if exporter_name == "langfuse":
        base_url = config.get("base_url")
        public_key = config.get("public_key")
        secret_key = config.get("secret_key")
        if not isinstance(base_url, str) or not base_url:
            raise ConfigError("Langfuse observe exporter requires a non-empty base_url")
        if not isinstance(public_key, str) or not public_key:
            raise ConfigError("Langfuse observe exporter requires a non-empty public_key")
        if not isinstance(secret_key, str) or not secret_key:
            raise ConfigError("Langfuse observe exporter requires a non-empty secret_key")
        unknown_keys = set(config) - {
            "exporter",
            "base_url",
            "public_key",
            "secret_key",
            "ingestion_path",
            "timeout",
            "batch_size",
            "max_retries",
            "backoff_seconds",
            "headers",
            "compression",
            "log_level",
        }
        if unknown_keys:
            unknown = ", ".join(sorted(unknown_keys))
            raise ConfigError(f"Unknown observe config fields: {unknown}")
        headers = config.get("headers", {})
        if not isinstance(headers, dict) or not all(
            isinstance(key, str) and isinstance(value, str)
            for key, value in headers.items()
        ):
            raise ConfigError("Langfuse observe headers must be a string-to-string mapping")
        return [
            LangfuseExporter(
                base_url=base_url,
                public_key=resolve_config_secret(public_key, field_name="observe.public_key"),
                secret_key=resolve_config_secret(secret_key, field_name="observe.secret_key"),
                ingestion_path=str(config.get("ingestion_path", "/api/public/ingestion")),
                timeout=float(config.get("timeout", 5.0)),
                batch_size=int(config.get("batch_size", 100)),
                max_retries=int(config.get("max_retries", 3)),
                backoff_seconds=float(config.get("backoff_seconds", 0.25)),
                headers={
                    key: resolve_config_secret(value, field_name=f"observe.headers.{key}")
                    for key, value in headers.items()
                },
                compression=str(config.get("compression", "none")),
            )
        ]

    if exporter_name is None:
        raise ConfigError("observe config requires an 'exporter' field")

    raise ConfigError(f"Unsupported observe exporter: {exporter_name}")
