"""Default callback implementation for adapters."""
from __future__ import annotations

import logging
import os
from pathlib import Path
from typing import Any

from evalhub.adapter.models.adapter import FrameworkAdapter

from ..models.api import JobStatus
from .models import (
    JobCallbacks,
    JobResults,
    JobSpec,
    JobStatusUpdate,
    OCIArtifactResult,
    OCIArtifactSpec,
)
from .oci import OCIArtifactPersister
from .oci.persister import OCIArtifactContext

logger = logging.getLogger(__name__)


class DefaultCallbacks(JobCallbacks):
    """Default callback implementation.

    This implementation:
    - Reports status updates to sidecar (if available) or logs them
    - Pushes OCI artifacts directly using OCIArtifactPersister

    This is the recommended callback implementation for both production and development.

    Example:
        ```python
        # Production (with evalhub for status updates)
        callbacks = DefaultCallbacks(
            job_id="my-job-123",
            benchmark_id="mmlu",
            benchmark_index=0,
            provider_id="lm_evaluation_harness",
            sidecar_url="http://localhost:8080",
            oci_auth_config_path=Path("~/.docker/config.json"),
        )

        # Local development (no evalhub, just logging)
        callbacks = DefaultCallbacks(
            job_id="my-job-123",
            benchmark_id="mmlu",
            benchmark_index=0,
            oci_insecure=True,
        )

        adapter = MyAdapter()
        results = adapter.run_benchmark_job(spec, callbacks)
        ```
    """

    def __init__(
        self,
        job_id: str,
        benchmark_id: str,
        provider_id: str | None = None,
        benchmark_index: int = 0,
        sidecar_url: str | None = None,
        insecure: bool = False,
        auth_token: str | None = None,
        auth_token_path: Path | str | None = None,
        ca_bundle_path: Path | str | None = None,
        events_path_template: str | None = None,
        oci_auth_config_path: Path | None = None,
        oci_insecure: bool = False,
    ):
        """Initialize default callbacks.

        Args:
            job_id: Job identifier for API endpoint construction.
            benchmark_id: Benchmark identifier for status event validation.
            provider_id: Provider identifier (optional). If not provided, status updates
                        will not include provider_id field.
            benchmark_index: Index of this benchmark within the job (default 0).
            sidecar_url: URL of evalhub service for status updates (optional).
                        If None, status updates are logged locally.
            insecure: Allow insecure HTTP connections (evalhub)
            auth_token: Explicit authentication token (overrides auto-detection)
            auth_token_path: Path to authentication token file (e.g., ServiceAccount token)
                           If not provided, auto-detects Kubernetes ServiceAccount token
            ca_bundle_path: Path to CA bundle for TLS verification
                          If not provided, auto-detects OpenShift/Kubernetes CA bundles
        """
        self.job_id = job_id
        self.benchmark_id = benchmark_id
        self.provider_id = provider_id
        self.benchmark_index = benchmark_index
        self.sidecar_url = sidecar_url.rstrip("/") if sidecar_url else None
        self._events_path_template = (
            events_path_template
            if events_path_template is not None
            else "/api/v1/evaluations/jobs/{job_id}/events"
        )

        # Initialize OCI persister
        self.persister = OCIArtifactPersister(
            context=OCIArtifactContext(
                job_id=job_id,
                benchmark_id=benchmark_id,
                provider_id=provider_id,
                benchmark_index=benchmark_index,
            ),
            oci_auth_config_path=oci_auth_config_path,
            oci_insecure=oci_insecure,
        )

        # Store insecure flag for evalhub communication
        self._insecure = insecure

        # Store auth token source for per-request reading
        self._explicit_auth_token = auth_token
        self._auth_token_path = self._resolve_auth_token_path(auth_token_path)

        # Auto-detect or load CA bundle (only if TLS verification is enabled)
        if insecure:
            self._ca_bundle = None
            logger.warning("TLS verification disabled - skipping CA bundle detection")
        else:
            self._ca_bundle = self._resolve_ca_bundle(ca_bundle_path)

        # Try to import httpx for sidecar communication
        self._httpx_available = False
        self._http_client: Any | None = None
        if self.sidecar_url:
            try:
                import httpx

                self.httpx = httpx
                self._httpx_available = True
                self._http_client = self._create_http_client()
            except ImportError:
                logger.warning(
                    "httpx not installed. Status updates will be logged locally. "
                    "Install with: pip install httpx"
                )

    @staticmethod
    def _resolve_auth_token_path(token_path: Path | str | None) -> Path | None:
        """Resolve the path to the authentication token file.

        Priority:
        1. Specified token path (if it exists)
        2. Auto-detected Kubernetes ServiceAccount token
        3. None (local mode, no authentication)

        Args:
            token_path: Path to token file

        Returns:
            Path to token file or None
        """
        if token_path:
            path = Path(token_path)
            if path.exists():
                return path
            logger.warning(f"Specified token path does not exist: {token_path}")

        default_token_path = Path("/var/run/secrets/kubernetes.io/serviceaccount/token")
        if default_token_path.exists():
            logger.debug("Auto-detected Kubernetes ServiceAccount token")
            return default_token_path

        logger.debug("No authentication token found - running in local mode")
        return None

    def _read_auth_token(self) -> str | None:
        """Read the authentication token fresh from disk (or return explicit token).

        Returns:
            Token string or None
        """
        if self._explicit_auth_token:
            return self._explicit_auth_token

        if self._auth_token_path:
            try:
                return self._auth_token_path.read_text().strip() or None
            except OSError:
                logger.warning(f"Failed to read token from {self._auth_token_path}")
                return None

        return None

    def _resolve_ca_bundle(self, ca_bundle_path: Path | str | None) -> Path | None:
        """Resolve CA bundle path with auto-detection.

        Priority:
        1. Explicitly specified CA bundle path
        2. Auto-detected OpenShift service-ca
        3. Auto-detected Kubernetes ServiceAccount CA
        4. None (use system defaults or insecure mode)

        Args:
            ca_bundle_path: Path to CA bundle file

        Returns:
            Path to CA bundle or None
        """
        # Use explicit CA bundle if provided
        if ca_bundle_path:
            path = Path(ca_bundle_path)
            if path.exists():
                return path
            logger.warning(f"Specified CA bundle does not exist: {ca_bundle_path}")

        # Try common CA bundle locations
        ca_paths = [
            Path("/etc/pki/ca-trust/source/anchors/service-ca.crt"),  # OpenShift
            Path(
                "/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt"
            ),  # OpenShift SA mount
            Path("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"),  # Kubernetes
        ]

        for path in ca_paths:
            if path.exists():
                logger.debug(f"Auto-detected CA bundle at: {path}")
                return path

        # No CA bundle found (use system defaults)
        logger.debug("No CA bundle found - using system defaults")
        return None

    @staticmethod
    def _resolve_namespace() -> str | None:
        """Read the pod namespace from the ServiceAccount mount."""
        ns_path = Path("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
        if ns_path.exists():
            try:
                return ns_path.read_text().strip() or None
            except OSError:
                return None
        return None

    def _request_headers(self) -> dict[str, str]:
        """Build per-request headers with fresh auth token and tenant namespace."""
        headers: dict[str, str] = {}

        token = self._read_auth_token()
        if token:
            headers["Authorization"] = f"Bearer {token}"

        namespace = self._resolve_namespace()
        if namespace:
            headers["X-Tenant"] = namespace

        return headers

    def _create_http_client(self) -> Any:
        """Create httpx client with TLS configuration.

        Auth headers are added per-request via _request_headers() so that
        rotated ServiceAccount tokens are picked up automatically.

        Returns:
            httpx.Client: Configured HTTP client
        """
        # Determine TLS verification settings
        verify: bool | str
        if self._insecure:
            verify = False
            logger.warning("TLS verification disabled (insecure mode)")
        elif self._ca_bundle:
            verify = str(self._ca_bundle)
            logger.debug(f"TLS verification using CA bundle: {self._ca_bundle}")
        else:
            verify = True  # Use system CA certificates
            logger.debug("TLS verification using system CA certificates")

        return self.httpx.Client(
            verify=verify,
            timeout=30.0,
        )

    def report_status(self, update: JobStatusUpdate) -> None:
        """Report status update to evalhub or log it.

        Args:
            update: Status update to report
        """
        # If evalhub available, send status update
        if self.sidecar_url and self._httpx_available and self._http_client:
            try:
                url = f"{self.sidecar_url}{self._events_path_template.format(job_id=self.job_id)}"

                # Transform to eval-hub API format
                status_event = {
                    "id": self.benchmark_id,
                    "benchmark_index": self.benchmark_index,
                    "state": update.status.value,
                    "status": update.status.value,
                    "message": update.message.model_dump(mode="json"),
                }

                # Include error details for failed updates
                if update.error:
                    status_event["error_message"] = update.error.model_dump(mode="json")

                # Include provider_id if available
                if self.provider_id:
                    status_event["provider_id"] = self.provider_id

                data = {"benchmark_status_event": status_event}
                logger.debug("Events report_status body: %s", data)

                response = self._http_client.post(
                    url,
                    json=data,
                    headers=self._request_headers(),
                    timeout=10.0,
                )
                response.raise_for_status()

                logger.debug(f"Status update sent to evalhub: {update.status}")
                return

            except self.httpx.HTTPStatusError as e:
                if e.response.status_code == 401:
                    logger.error(
                        "Authentication failed (401). Ensure the job has a valid "
                        "ServiceAccount token at /var/run/secrets/kubernetes.io/serviceaccount/token"
                    )
                elif e.response.status_code == 403:
                    logger.error(
                        "Authorization failed (403). Ensure the ServiceAccount has RBAC "
                        "permissions for evaluations resource in the trustyai.opendatahub.io API group"
                    )
                else:
                    logger.warning(f"Failed to send status to evalhub: {e}")
                # Fall through to local logging
            except Exception as e:
                logger.warning(f"Failed to send status to evalhub: {e}")
                # Fall through to local logging

        # Local logging
        logger.info(
            f"Status: {update.status.value} | "
            f"Phase: {update.phase.value if update.phase else 'N/A'} | "
            f"Progress: {update.progress or 'N/A'} | "
            f"Message: {update.message or ''}"
        )

    def create_oci_artifact(self, spec: OCIArtifactSpec) -> OCIArtifactResult:
        """Create OCI artifact using the SDK persister.

        Args:
            spec: Artifact specification

        Returns:
            OCIArtifactResult: Result with digest and reference

        Raises:
            RuntimeError: If artifact creation fails
        """
        logger.info(f"Creating OCI artifact for job {self.job_id}")
        result = self.persister.persist(spec)
        logger.info(f"Created OCI artifact for job {self.job_id} as: {result}")
        return result

    def report_results(self, results: JobResults) -> None:
        """Report final evaluation results to evalhub or log them.

        This sends the complete results including metrics to the evalhub service.

        Args:
            results: Final job results to report
        """
        # If evalhub available, send results with completed status event
        if self.sidecar_url and self._httpx_available and self._http_client:
            try:
                url = f"{self.sidecar_url}{self._events_path_template.format(job_id=self.job_id)}"

                # Convert evaluation results to metrics map
                metrics = {}
                for result in results.results:
                    metrics[result.metric_name] = result.metric_value

                # Build status event with results
                status_event = {
                    "id": self.benchmark_id,
                    "benchmark_index": self.benchmark_index,
                    "state": JobStatus.COMPLETED.value,
                    "status": JobStatus.COMPLETED.value,
                    "message": {
                        "message": "Evaluation completed successfully",
                        "message_code": "evaluation_completed",
                    },
                    "metrics": metrics,
                    "completed_at": results.completed_at.isoformat(),
                    "duration_seconds": int(results.duration_seconds),
                }

                # Include provider_id if available
                if self.provider_id:
                    status_event["provider_id"] = self.provider_id

                # Include OCI artifact reference if available
                if results.oci_artifact:
                    status_event["artifacts"] = {
                        "oci_reference": results.oci_artifact.reference,
                        "oci_digest": results.oci_artifact.digest,
                    }

                data = {"benchmark_status_event": status_event}
                logger.debug("Events report_results body: %s", data)

                response = self._http_client.post(
                    url,
                    json=data,
                    headers=self._request_headers(),
                    timeout=10.0,
                )
                response.raise_for_status()

                logger.info(
                    f"Results reported to evalhub | "
                    f"Metrics: {len(metrics)} | "
                    f"Score: {results.overall_score}"
                )

            except self.httpx.HTTPStatusError as e:
                logger.error(
                    f"Failed to send results to evalhub (HTTP {e.response.status_code}): {e}"
                )
                # Fall through to local logging
            except Exception as e:
                logger.error(f"Failed to send results to evalhub: {e}")
                # Fall through to local logging

        # Local logging
        logger.info(
            f"Job {results.id} completed | "
            f"Benchmark: {results.benchmark_id} | "
            f"Model: {results.model_name} | "
            f"Score: {results.overall_score} | "
            f"Examples: {results.num_examples_evaluated} | "
            f"Duration: {results.duration_seconds:.2f}s"
        )

    def report_metrics_to_mlflow(self, results: JobResults, job_spec: JobSpec) -> None:
        """Report evaluation metrics to MLflow if experiment is configured.

        Args:
            results: Final job results containing metrics to log
            job_spec: Job specification that may contain experiment configuration

        Raises:
            RuntimeError: If MLflow logging fails
        """
        # Check if experiment is configured
        if not job_spec.experiment_name:
            logger.debug("No MLflow experiment configured, skipping MLflow logging")
            return

        # Try to import mlflow
        try:
            import mlflow
        except ImportError:
            logger.warning(
                "mlflow-skinny not installed. MLflow logging skipped. "
                "Install with: pip install mlflow-skinny>=3.10.0rc0"
            )
            return

        try:
            # Load auth token from projected volume if configured into env variable.
            # On ROSA/STS clusters the auto-mounted SA token has the wrong audience,
            # so the operator mounts a projected token with the default K8s API audience.
            token_path = os.environ.get("MLFLOW_TRACKING_TOKEN_PATH")
            if token_path:
                try:
                    with open(token_path) as f:
                        token = f.read().strip()
                    if token:
                        os.environ["MLFLOW_TRACKING_TOKEN"] = token
                except OSError as e:
                    logger.warning(
                        f"Could not read MLFlow token from {token_path}: {e}"
                    )

            # Set or create experiment
            mlflow.set_experiment(job_spec.experiment_name)

            # Start a run with the job ID as run name
            with mlflow.start_run(run_name=results.id):
                # Log parameters
                mlflow.log_param("benchmark_id", results.benchmark_id)
                mlflow.log_param("model_name", results.model_name)
                mlflow.log_param(
                    "num_examples_evaluated", results.num_examples_evaluated
                )
                mlflow.log_param("duration_seconds", results.duration_seconds)

                # Log tags from job spec (tags is list[dict] with "key"/"value" entries)
                if job_spec.tags:
                    for tag in job_spec.tags:
                        mlflow.set_tag(tag["key"], tag["value"])

                # Log evaluation metrics (only numeric values)
                for result in results.results:
                    if isinstance(result.metric_value, int | float):
                        mlflow.log_metric(
                            result.metric_name, float(result.metric_value)
                        )

                # Log overall score if available
                if results.overall_score is not None:
                    mlflow.log_metric("overall_score", results.overall_score)

                logger.info(
                    f"Metrics logged to MLflow experiment '{job_spec.experiment_name}' "
                    f"(run: {results.id})"
                )

        except Exception as e:
            logger.error(f"Failed to log metrics to MLflow: {e}")
            raise RuntimeError(f"MLflow logging failed: {e}") from e

    @staticmethod
    def from_adapter(adapter: FrameworkAdapter) -> DefaultCallbacks:
        """convenience method, and do not store adapter instance"""
        return DefaultCallbacks(
            job_id=adapter.job_spec.id,
            provider_id=adapter.job_spec.provider_id,
            benchmark_id=adapter.job_spec.benchmark_id,
            benchmark_index=adapter.job_spec.benchmark_index,
            sidecar_url=adapter.job_spec.callback_url,
            insecure=adapter.settings.evalhub_insecure,
            oci_auth_config_path=adapter.settings.oci_auth_config_path,
            oci_insecure=adapter.settings.oci_insecure,
        )
