# Vendor model integration bridge for MCA SDK.
# Provides base class for polling vendor APIs and converting JSON metrics to OTLP format.

import logging
import threading
import time
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional

from ..core.client import MCAClient

logger = logging.getLogger(__name__)


class VendorBridge(ABC):
    """Base class for polling vendor APIs and converting metrics to OTLP format.

    VendorBridge provides a framework for integrating third-party ML model APIs
    that don't natively support OpenTelemetry. It handles:

    - Background polling with configurable intervals
    - Exponential backoff retry logic for transient failures
    - Metric transformation from vendor JSON to OTLP format
    - Thread-safe operation with graceful shutdown
    - Health checks and statistics tracking

    Subclasses must implement:
        - fetch_metrics(): Retrieve metrics from vendor API
        - transform_metrics(): Convert vendor format to OTLP

    Thread Safety:
        All public methods are thread-safe. The polling loop runs in a
        background daemon thread that can be started/stopped independently.

    Example:
        >>> class MyVendorBridge(VendorBridge):
        ...     def fetch_metrics(self):
        ...         response = requests.get(self.endpoint)
        ...         return response.json()
        ...
        ...     def transform_metrics(self, data):
        ...         return {
        ...             "gauges": {"accuracy": data["acc"]},
        ...             "counters": {"predictions": data["count"]}
        ...         }
        >>>
        >>> bridge = MyVendorBridge(
        ...     service_name="vendor-model",
        ...     vendor_name="acme-ml",
        ...     endpoint="https://api.acme.com/metrics",
        ...     poll_interval=30.0
        ... )
        >>> bridge.start()
        >>> bridge.stop()

    Args:
        service_name: Unique identifier for this model
        vendor_name: Vendor identifier (e.g., "aws-sagemaker")
        endpoint: Vendor API endpoint URL (must use HTTPS for production)
        poll_interval: Seconds between polls (default: 30.0)
        model_type: Model type for taxonomy (default: "vendor")
        team_name: Owning team name (optional)
        max_iterations: Max polls before stopping, None for continuous (default: None)
        retry_max_attempts: Max retry attempts per poll (default: 3)
        retry_base_delay: Base delay for exponential backoff (default: 1.0s)
        retry_max_delay: Max delay between retries (default: 30.0s)
        request_timeout: HTTP request timeout (default: 10.0s)
        **client_kwargs: Additional arguments passed to MCAClient
    """

    def __init__(
        self,
        service_name: str,
        vendor_name: str,
        endpoint: str,
        poll_interval: float = 30.0,
        model_type: str = "vendor",
        team_name: Optional[str] = None,
        max_iterations: Optional[int] = None,
        retry_max_attempts: int = 3,
        retry_base_delay: float = 1.0,
        retry_max_delay: float = 30.0,
        request_timeout: float = 10.0,
        **client_kwargs,
    ):
        self.service_name = service_name
        self.vendor_name = vendor_name
        self.endpoint = endpoint
        self.poll_interval = poll_interval
        self.max_iterations = max_iterations
        self.retry_max_attempts = retry_max_attempts
        self.retry_base_delay = retry_base_delay
        self.retry_max_delay = retry_max_delay
        self.request_timeout = request_timeout

        self.client = MCAClient(
            service_name=service_name,
            model_type=model_type,
            vendor_name=vendor_name,
            team_name=team_name,
            **client_kwargs,
        )

        self._running = False
        self._worker_thread: Optional[threading.Thread] = None
        self._lock = threading.Lock()
        self._stop_event = threading.Event()

        self._poll_count = 0
        self._success_count = 0
        self._error_count = 0
        self._retry_count = 0
        self._consecutive_errors = 0
        self._error_backoff_base = 5.0  # Start with 5s backoff
        self._error_backoff_max = 300.0  # Max 5 minutes
        self._last_successful_poll_time: Optional[float] = None

    @abstractmethod
    def fetch_metrics(self) -> Dict[str, Any]:
        """Fetch metrics from vendor API."""

    @abstractmethod
    def transform_metrics(self, data: Dict[str, Any]) -> Dict[str, Dict[str, float]]:
        """Transform vendor metrics to OTLP format."""

    def start(self):
        """Start background polling loop."""
        with self._lock:
            if self._running:
                logger.warning(f"{self.service_name}: Bridge already running")
                return

            self._running = True
            self._stop_event.clear()
            self._worker_thread = threading.Thread(
                target=self._poll_loop, name=f"VendorBridge-{self.service_name}", daemon=True
            )
            self._worker_thread.start()
            mode = f"{self.max_iterations} iterations" if self.max_iterations else "continuous"
            logger.info(
                f"{self.service_name}: Started vendor bridge "
                f"(poll interval: {self.poll_interval}s, mode: {mode})"
            )

    def stop(self):
        """Stop background polling loop."""
        with self._lock:
            if not self._running:
                return
            self._running = False
            self._stop_event.set()

        if self._worker_thread and self._worker_thread.is_alive():
            self._worker_thread.join(timeout=5.0)
            logger.info(f"{self.service_name}: Stopped vendor bridge")

        self.client.shutdown()

    def _fetch_with_retry(self) -> Optional[Dict[str, Any]]:
        """Fetch metrics with exponential backoff retry logic."""
        last_error = None
        for attempt in range(self.retry_max_attempts):
            try:
                return self.fetch_metrics()
            except Exception as e:
                last_error = e
                self._retry_count += 1
                if attempt < self.retry_max_attempts - 1:
                    delay = min(self.retry_base_delay * (2**attempt), self.retry_max_delay)
                    logger.warning(
                        f"{self.service_name}: Fetch attempt {attempt + 1} failed, "
                        f"retrying in {delay:.1f}s: {e}"
                    )
                    if self._stop_event.wait(timeout=delay):
                        return None
                else:
                    logger.error(
                        f"{self.service_name}: All {self.retry_max_attempts} "
                        f"fetch attempts failed: {last_error}"
                    )
        return None

    def _poll_loop(self, single_run: bool = False):
        """Background polling loop with exponential backoff on errors."""
        logger.debug(f"{self.service_name}: Poll loop started")
        iteration = 0

        while self._running:
            if self.max_iterations is not None and iteration >= self.max_iterations:
                logger.info(
                    f"{self.service_name}: Reached max iterations ({self.max_iterations}), stopping"
                )
                break

            try:
                self._poll_count += 1
                iteration += 1
                logger.debug(f"{self.service_name}: Fetching metrics (poll #{self._poll_count})")
                raw_data = self._fetch_with_retry()

                if raw_data is None:
                    self._error_count += 1
                    self._consecutive_errors += 1
                else:
                    metrics = self.transform_metrics(raw_data)

                    # Record metrics using explicit types
                    for name, value in metrics.get("gauges", {}).items():
                        self.client.record_gauge(name, value, vendor=self.vendor_name)
                    for name, value in metrics.get("counters", {}).items():
                        self.client.record_counter(name, value, vendor=self.vendor_name)

                    self._success_count += 1
                    self._consecutive_errors = 0  # Reset on success
                    self._last_successful_poll_time = time.time()
                    logger.debug(
                        f"{self.service_name}: Successfully recorded "
                        f"{len(metrics.get('gauges', {})) + len(metrics.get('counters', {}))} metrics"
                    )

            except Exception as e:
                self._error_count += 1
                self._consecutive_errors += 1
                logger.error(
                    f"{self.service_name}: Error in poll cycle (consecutive errors: "
                    f"{self._consecutive_errors}): {e}"
                )
                if single_run:
                    raise

                # Apply exponential backoff if errors are consecutive
                if self._consecutive_errors >= 3:
                    backoff_delay = min(
                        self._error_backoff_base * (2 ** (self._consecutive_errors - 3)),
                        self._error_backoff_max,
                    )
                    logger.warning(
                        f"{self.service_name}: {self._consecutive_errors} consecutive errors, "
                        f"applying backoff delay of {backoff_delay:.1f}s before next poll"
                    )
                    if self._stop_event.wait(timeout=backoff_delay):
                        break
                    continue  # Skip normal poll interval

            finally:
                if single_run:
                    break

            # Normal poll interval
            if self._stop_event.wait(timeout=self.poll_interval):
                break

        logger.debug(f"{self.service_name}: Poll loop stopped")
        if self.max_iterations is not None and iteration >= self.max_iterations:
            with self._lock:
                self._running = False

    def get_stats(self) -> Dict[str, int]:
        """Get bridge statistics."""
        return {
            "poll_count": self._poll_count,
            "success_count": self._success_count,
            "error_count": self._error_count,
            "retry_count": self._retry_count,
        }

    def is_running(self) -> bool:
        """Check if bridge is currently running."""
        with self._lock:
            return self._running

    def health_check(self) -> bool:
        """Perform a health check based on internal state.

        Returns:
            bool: True if bridge is operationally healthy, False otherwise.

        Checks performed:
            1. Worker thread is alive
            2. Recent successful polls (within 2x poll_interval)
            3. Error rate is not excessive

        Note:
            This method does NOT make external API calls to avoid:
            - Rate limiting from health probe spam
            - Clearing vendor API delta counters
            - Incurring costs on metered APIs
        """
        # Check if bridge is running (operational status)
        if not self.is_running():
            logger.debug(
                f"{self.service_name}: Health check - bridge is not running (stopped state)."
            )
            return False

        if not self._worker_thread or not self._worker_thread.is_alive():
            logger.error(
                f"{self.service_name}: Health check failed - worker thread is not running "
                f"(thread exists: {self._worker_thread is not None}, "
                f"alive: {self._worker_thread.is_alive() if self._worker_thread else False})"
            )
            return False

        # Check for recent successful polls
        if self._last_successful_poll_time is None:
            logger.warning(
                f"{self.service_name}: Health check warning - no successful polls yet "
                f"(poll_count: {self._poll_count}, error_count: {self._error_count})"
            )
            # Allow grace period for first poll
            return self._poll_count < 3
        else:
            time_since_last_success = time.time() - self._last_successful_poll_time
            max_allowed_time = self.poll_interval * 2
            if time_since_last_success > max_allowed_time:
                logger.error(
                    f"{self.service_name}: Health check failed - last successful poll was "
                    f"{time_since_last_success:.1f}s ago (threshold: {max_allowed_time}s)"
                )
                return False

        # Check error rate
        if self._poll_count > 0:
            error_rate = self._error_count / self._poll_count
            if error_rate > 0.5:  # More than 50% errors
                logger.error(
                    f"{self.service_name}: Health check failed - high error rate "
                    f"({error_rate:.1%}, {self._error_count}/{self._poll_count})"
                )
                return False

        logger.debug(
            f"{self.service_name}: Health check passed "
            f"(polls: {self._poll_count}, successes: {self._success_count}, errors: {self._error_count})"
        )
        return True

    def wait_for_completion(self, timeout: Optional[float] = None) -> bool:
        """Wait for polling to complete (for finite mode)."""
        if self._worker_thread is None:
            return True
        self._worker_thread.join(timeout=timeout)
        return not self._worker_thread.is_alive()
