"""
Observability and monitoring for Meta Ads MCP server.

This module provides Prometheus metrics, OpenTelemetry tracing,
and structured logging for comprehensive monitoring.
"""

import os
from typing import Any, Dict, List, Optional
from dataclasses import dataclass
from contextlib import asynccontextmanager
from enum import Enum

from .logging import get_logger

logger = get_logger(__name__)


def _parse_bool(value: str | None) -> bool | None:
    """Parse an environment-style boolean value."""
    if value is None:
        return None
    normalized = value.strip().lower()
    if normalized in {"1", "true", "yes", "on"}:
        return True
    if normalized in {"0", "false", "no", "off"}:
        return False
    return None


def _resolve_otlp_insecure(endpoint: str, explicit: bool | None = None) -> bool:
    """Resolve OTLP insecure mode with explicit and env overrides."""
    if explicit is not None:
        return explicit

    env_override = _parse_bool(os.getenv("OTEL_EXPORTER_OTLP_INSECURE"))
    if env_override is not None:
        return env_override

    return not endpoint.strip().lower().startswith("https://")

try:
    from prometheus_client import Counter, Histogram, Gauge, Info, CollectorRegistry, generate_latest
    PROMETHEUS_AVAILABLE = True
except ImportError:
    PROMETHEUS_AVAILABLE = False
    logger.warning("Prometheus client not available. Install with: pip install prometheus-client")

try:
    from opentelemetry import trace, metrics
    from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
    from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
    from opentelemetry.sdk.trace import TracerProvider
    from opentelemetry.sdk.trace.export import BatchSpanProcessor
    from opentelemetry.sdk.metrics import MeterProvider
    from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
    from opentelemetry.sdk.resources import Resource
    OPENTELEMETRY_AVAILABLE = True
except ImportError:
    OPENTELEMETRY_AVAILABLE = False
    logger.warning("OpenTelemetry not available. Install with: pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp")


class MetricType(str, Enum):
    """Types of metrics."""
    COUNTER = "counter"
    HISTOGRAM = "histogram"
    GAUGE = "gauge"
    INFO = "info"


@dataclass
class MetricConfig:
    """Configuration for a metric."""
    name: str
    description: str
    metric_type: MetricType
    labels: List[str] = None
    
    def __post_init__(self):
        if self.labels is None:
            self.labels = []


class MetricsCollector:
    """Collects and exports Prometheus metrics."""
    
    def __init__(self, registry: Optional[CollectorRegistry] = None):
        self.registry = registry or CollectorRegistry()
        self._metrics: Dict[str, Any] = {}
        self._initialized = False
        
        if PROMETHEUS_AVAILABLE:
            self._initialize_metrics()
    
    def _initialize_metrics(self):
        """Initialize Prometheus metrics."""
        if self._initialized:
            return
        
        # API Request Metrics
        self._metrics["api_requests_total"] = Counter(
            "meta_ads_api_requests_total",
            "Total number of API requests",
            ["method", "endpoint", "status_code", "ad_account_id"],
            registry=self.registry
        )
        
        self._metrics["api_request_duration"] = Histogram(
            "meta_ads_api_request_duration_seconds",
            "API request duration in seconds",
            ["method", "endpoint", "ad_account_id"],
            registry=self.registry
        )
        
        # Campaign Metrics
        self._metrics["campaigns_total"] = Gauge(
            "meta_ads_campaigns_total",
            "Total number of campaigns",
            ["ad_account_id", "status"],
            registry=self.registry
        )
        
        self._metrics["campaign_spend"] = Gauge(
            "meta_ads_campaign_spend_usd",
            "Campaign spend in USD",
            ["campaign_id", "ad_account_id"],
            registry=self.registry
        )
        
        # Performance Metrics
        self._metrics["cache_hits_total"] = Counter(
            "meta_ads_cache_hits_total",
            "Total number of cache hits",
            ["cache_type", "endpoint"],
            registry=self.registry
        )
        
        self._metrics["cache_misses_total"] = Counter(
            "meta_ads_cache_misses_total",
            "Total number of cache misses",
            ["cache_type", "endpoint"],
            registry=self.registry
        )
        
        # Rate Limiting Metrics
        self._metrics["rate_limit_remaining"] = Gauge(
            "meta_ads_rate_limit_remaining",
            "Remaining rate limit",
            ["endpoint", "user_id"],
            registry=self.registry
        )
        
        self._metrics["rate_limit_exceeded_total"] = Counter(
            "meta_ads_rate_limit_exceeded_total",
            "Total number of rate limit exceeded events",
            ["endpoint", "user_id"],
            registry=self.registry
        )
        
        # Error Metrics
        self._metrics["errors_total"] = Counter(
            "meta_ads_errors_total",
            "Total number of errors",
            ["error_type", "endpoint", "ad_account_id"],
            registry=self.registry
        )
        
        # System Info
        self._metrics["system_info"] = Info(
            "meta_ads_system_info",
            "System information",
            registry=self.registry
        )
        
        self._initialized = True
        logger.info("Prometheus metrics initialized")
    
    def record_api_request(
        self,
        method: str,
        endpoint: str,
        status_code: int,
        duration: float,
        ad_account_id: Optional[str] = None,
    ):
        """Record API request metrics."""
        if not PROMETHEUS_AVAILABLE or not self._initialized:
            return
        
        labels = {
            "method": method,
            "endpoint": endpoint,
            "status_code": str(status_code),
            "ad_account_id": ad_account_id or "unknown",
        }
        
        self._metrics["api_requests_total"].labels(**labels).inc()
        self._metrics["api_request_duration"].labels(
            method=method,
            endpoint=endpoint,
            ad_account_id=ad_account_id or "unknown"
        ).observe(duration)
    
    def record_campaign_metrics(
        self,
        campaign_id: str,
        ad_account_id: str,
        spend: float,
        status: str,
    ):
        """Record campaign metrics."""
        if not PROMETHEUS_AVAILABLE or not self._initialized:
            return
        
        self._metrics["campaign_spend"].labels(
            campaign_id=campaign_id,
            ad_account_id=ad_account_id
        ).set(spend)
        
        self._metrics["campaigns_total"].labels(
            ad_account_id=ad_account_id,
            status=status
        ).inc()
    
    def record_cache_metrics(
        self,
        cache_type: str,
        endpoint: str,
        hit: bool,
    ):
        """Record cache metrics."""
        if not PROMETHEUS_AVAILABLE or not self._initialized:
            return
        
        labels = {"cache_type": cache_type, "endpoint": endpoint}
        
        if hit:
            self._metrics["cache_hits_total"].labels(**labels).inc()
        else:
            self._metrics["cache_misses_total"].labels(**labels).inc()
    
    def record_rate_limit_metrics(
        self,
        endpoint: str,
        remaining: int,
        user_id: Optional[str] = None,
    ):
        """Record rate limit metrics."""
        if not PROMETHEUS_AVAILABLE or not self._initialized:
            return
        
        labels = {
            "endpoint": endpoint,
            "user_id": user_id or "unknown",
        }
        
        self._metrics["rate_limit_remaining"].labels(**labels).set(remaining)
    
    def record_rate_limit_exceeded(
        self,
        endpoint: str,
        user_id: Optional[str] = None,
    ):
        """Record rate limit exceeded event."""
        if not PROMETHEUS_AVAILABLE or not self._initialized:
            return
        
        labels = {
            "endpoint": endpoint,
            "user_id": user_id or "unknown",
        }
        
        self._metrics["rate_limit_exceeded_total"].labels(**labels).inc()
    
    def record_error(
        self,
        error_type: str,
        endpoint: str,
        ad_account_id: Optional[str] = None,
    ):
        """Record error metrics."""
        if not PROMETHEUS_AVAILABLE or not self._initialized:
            return
        
        labels = {
            "error_type": error_type,
            "endpoint": endpoint,
            "ad_account_id": ad_account_id or "unknown",
        }
        
        self._metrics["errors_total"].labels(**labels).inc()
    
    def set_system_info(self, info: Dict[str, str]):
        """Set system information."""
        if not PROMETHEUS_AVAILABLE or not self._initialized:
            return
        
        self._metrics["system_info"].info(info)
    
    def get_metrics(self) -> str:
        """Get metrics in Prometheus format."""
        if not PROMETHEUS_AVAILABLE:
            return "# Prometheus client not available\n"
        
        return generate_latest(self.registry).decode('utf-8')


class TracingProvider:
    """Provides OpenTelemetry tracing."""
    
    def __init__(
        self,
        service_name: str = "meta-ads-mcp",
        service_version: str = "1.0.0",
        otlp_endpoint: str | None = None,
        otlp_insecure: bool | None = None,
    ):
        self.service_name = service_name
        self.service_version = service_version
        self.otlp_endpoint = otlp_endpoint
        self.otlp_insecure = otlp_insecure
        self.tracer = None
        self._initialized = False
        
        if OPENTELEMETRY_AVAILABLE:
            self._initialize_tracing()
    
    def _initialize_tracing(self):
        """Initialize OpenTelemetry tracing."""
        if self._initialized:
            return
        
        try:
            # Create resource
            resource = Resource.create({
                "service.name": self.service_name,
                "service.version": self.service_version,
            })
            
            # Create tracer provider
            tracer_provider = TracerProvider(resource=resource)
            trace.set_tracer_provider(tracer_provider)
            
            # Create OTLP exporter (can be configured for different backends)
            endpoint = self.otlp_endpoint or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") or "http://localhost:4317"
            insecure = _resolve_otlp_insecure(endpoint, self.otlp_insecure)
            otlp_exporter = OTLPSpanExporter(
                endpoint=endpoint,
                insecure=insecure,
            )
            
            # Create span processor
            span_processor = BatchSpanProcessor(otlp_exporter)
            tracer_provider.add_span_processor(span_processor)
            
            # Get tracer
            self.tracer = trace.get_tracer(__name__)
            
            self._initialized = True
            logger.info(
                "OpenTelemetry tracing initialized endpoint=%s insecure=%s",
                endpoint,
                insecure,
            )
            
        except Exception as e:
            logger.error("Failed to initialize OpenTelemetry tracing: %s", e)
    
    @asynccontextmanager
    async def trace_operation(
        self,
        operation_name: str,
        attributes: Optional[Dict[str, Any]] = None,
    ):
        """Context manager for tracing operations."""
        if not OPENTELEMETRY_AVAILABLE or not self.tracer:
            yield
            return
        
        span = self.tracer.start_span(operation_name)
        
        if attributes:
            for key, value in attributes.items():
                span.set_attribute(key, value)
        
        try:
            yield span
        except Exception as e:
            span.record_exception(e)
            span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
            raise
        finally:
            span.end()
    
    def add_span_event(
        self,
        span: Any,
        name: str,
        attributes: Optional[Dict[str, Any]] = None,
    ):
        """Add an event to a span."""
        if not OPENTELEMETRY_AVAILABLE or not span:
            return
        
        span.add_event(name, attributes or {})


class MetricsProvider:
    """Provides OpenTelemetry metrics."""
    
    def __init__(
        self,
        service_name: str = "meta-ads-mcp",
        service_version: str = "1.0.0",
        otlp_endpoint: str | None = None,
        otlp_insecure: bool | None = None,
    ):
        self.service_name = service_name
        self.service_version = service_version
        self.otlp_endpoint = otlp_endpoint
        self.otlp_insecure = otlp_insecure
        self.meter = None
        self._initialized = False
        
        if OPENTELEMETRY_AVAILABLE:
            self._initialize_metrics()
    
    def _initialize_metrics(self):
        """Initialize OpenTelemetry metrics."""
        if self._initialized:
            return
        
        try:
            # Create resource
            resource = Resource.create({
                "service.name": self.service_name,
                "service.version": self.service_version,
            })
            
            # Create meter provider
            endpoint = self.otlp_endpoint or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") or "http://localhost:4317"
            insecure = _resolve_otlp_insecure(endpoint, self.otlp_insecure)
            meter_provider = MeterProvider(
                resource=resource,
                metric_readers=[
                    PeriodicExportingMetricReader(
                        OTLPMetricExporter(
                            endpoint=endpoint,
                            insecure=insecure,
                        ),
                        export_interval_millis=5000,
                    )
                ],
            )
            metrics.set_meter_provider(meter_provider)
            
            # Get meter
            self.meter = metrics.get_meter(__name__)
            
            self._initialized = True
            logger.info(
                "OpenTelemetry metrics initialized endpoint=%s insecure=%s",
                endpoint,
                insecure,
            )
            
        except Exception as e:
            logger.error("Failed to initialize OpenTelemetry metrics: %s", e)
    
    def create_counter(
        self,
        name: str,
        description: str,
        unit: str = "1",
    ):
        """Create a counter metric."""
        if not OPENTELEMETRY_AVAILABLE or not self.meter:
            return None
        
        return self.meter.create_counter(
            name=name,
            description=description,
            unit=unit,
        )
    
    def create_histogram(
        self,
        name: str,
        description: str,
        unit: str = "s",
    ):
        """Create a histogram metric."""
        if not OPENTELEMETRY_AVAILABLE or not self.meter:
            return None
        
        return self.meter.create_histogram(
            name=name,
            description=description,
            unit=unit,
        )
    
    def create_gauge(
        self,
        name: str,
        description: str,
        unit: str = "1",
    ):
        """Create a gauge metric."""
        if not OPENTELEMETRY_AVAILABLE or not self.meter:
            return None
        
        return self.meter.create_up_down_counter(
            name=name,
            description=description,
            unit=unit,
        )


class ObservabilityManager:
    """Manages all observability features."""
    
    def __init__(
        self,
        service_name: str = "meta-ads-mcp",
        service_version: str = "1.0.0",
        enable_prometheus: bool = True,
        enable_tracing: bool = True,
        enable_metrics: bool = True,
        otlp_endpoint: str | None = None,
        otlp_insecure: bool | None = None,
    ):
        self.service_name = service_name
        self.service_version = service_version
        
        self.metrics_collector = None
        self.tracing_provider = None
        self.metrics_provider = None
        
        if enable_prometheus:
            self.metrics_collector = MetricsCollector()
        
        if enable_tracing:
            self.tracing_provider = TracingProvider(
                service_name,
                service_version,
                otlp_endpoint=otlp_endpoint,
                otlp_insecure=otlp_insecure,
            )
        
        if enable_metrics:
            self.metrics_provider = MetricsProvider(
                service_name,
                service_version,
                otlp_endpoint=otlp_endpoint,
                otlp_insecure=otlp_insecure,
            )
        
        # Set system info
        if self.metrics_collector:
            self.metrics_collector.set_system_info({
                "service_name": service_name,
                "service_version": service_version,
                "python_version": "3.8+",
            })
    
    async def trace_api_request(
        self,
        method: str,
        endpoint: str,
        ad_account_id: Optional[str] = None,
    ):
        """Trace an API request."""
        if not self.tracing_provider:
            return
        
        attributes = {
            "http.method": method,
            "http.url": endpoint,
            "meta.ad_account_id": ad_account_id or "unknown",
        }
        
        async with self.tracing_provider.trace_operation(
            f"api_request_{method.lower()}",
            attributes=attributes,
        ) as span:
            return span
    
    def record_api_metrics(
        self,
        method: str,
        endpoint: str,
        status_code: int,
        duration: float,
        ad_account_id: Optional[str] = None,
    ):
        """Record API request metrics."""
        if self.metrics_collector:
            self.metrics_collector.record_api_request(
                method=method,
                endpoint=endpoint,
                status_code=status_code,
                duration=duration,
                ad_account_id=ad_account_id,
            )
    
    def record_cache_metrics(
        self,
        cache_type: str,
        endpoint: str,
        hit: bool,
    ):
        """Record cache metrics."""
        if self.metrics_collector:
            self.metrics_collector.record_cache_metrics(
                cache_type=cache_type,
                endpoint=endpoint,
                hit=hit,
            )
    
    def record_rate_limit_metrics(
        self,
        endpoint: str,
        remaining: int,
        user_id: Optional[str] = None,
    ):
        """Record rate limit metrics."""
        if self.metrics_collector:
            self.metrics_collector.record_rate_limit_metrics(
                endpoint=endpoint,
                remaining=remaining,
                user_id=user_id,
            )
    
    def record_error_metrics(
        self,
        error_type: str,
        endpoint: str,
        ad_account_id: Optional[str] = None,
    ):
        """Record error metrics."""
        if self.metrics_collector:
            self.metrics_collector.record_error(
                error_type=error_type,
                endpoint=endpoint,
                ad_account_id=ad_account_id,
            )
    
    def get_prometheus_metrics(self) -> str:
        """Get Prometheus metrics."""
        if self.metrics_collector:
            return self.metrics_collector.get_metrics()
        return "# Prometheus not enabled\n"


# Global observability manager instance
_observability_manager: Optional[ObservabilityManager] = None


def get_observability_manager() -> ObservabilityManager:
    """Get the global observability manager instance."""
    global _observability_manager
    if _observability_manager is None:
        endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
        otlp_insecure = _parse_bool(os.getenv("OTEL_EXPORTER_OTLP_INSECURE"))
        _observability_manager = ObservabilityManager(
            enable_tracing=bool(endpoint),
            enable_metrics=bool(endpoint),
            otlp_endpoint=endpoint,
            otlp_insecure=otlp_insecure,
        )
    return _observability_manager


def setup_observability(
    service_name: str = "meta-ads-mcp",
    service_version: str = "1.0.0",
    enable_prometheus: bool = True,
    enable_tracing: bool = True,
    enable_metrics: bool = True,
    otlp_endpoint: str | None = None,
    otlp_insecure: bool | None = None,
) -> ObservabilityManager:
    """Setup observability with custom configuration."""
    global _observability_manager
    _observability_manager = ObservabilityManager(
        service_name=service_name,
        service_version=service_version,
        enable_prometheus=enable_prometheus,
        enable_tracing=enable_tracing,
        enable_metrics=enable_metrics,
        otlp_endpoint=otlp_endpoint,
        otlp_insecure=otlp_insecure,
    )
    return _observability_manager
