#!/usr/bin/env python3

"""![status](../../../badges/status-application-errors.svg) Application-layer exceptions"""

from __future__ import annotations

from typing import Any

from pydantic import JsonValue


class EvaluationError(Exception):
    """Base exception for application-layer evaluation failures."""


class RequestValidationError(EvaluationError):
    """Base exception for contextual validation of evaluation requests."""


##########################
# Metric-related errors  #
##########################
class UnknownMetricError(RequestValidationError):
    """Raised when a requested metric name is not registered."""

    def __init__(self, metric_name: str, *, available: list[str] | None = None) -> None:
        self.metric_name = metric_name
        self.available = available or []
        message = f"Unknown metric: {metric_name!r}."
        if self.available:  # pragma: no cover
            message += f" Available metrics: {', '.join(sorted(self.available))}."
        super().__init__(message)


class MetricConfigurationError(RequestValidationError):
    """Raised when a metric cannot be instantiated from the provided config."""

    def __init__(
        self,
        metric_name: str,
        *,
        config: dict[str, JsonValue] | None = None,
        reason: str | None = None,
    ) -> None:
        self.metric_name = metric_name
        self.config = config
        self.reason = reason
        message = f"Invalid configuration for metric {metric_name!r}."
        if reason:  # pragma: no cover
            message += f" {reason}"
        super().__init__(message)


class DuplicateMetricResultKeyError(RequestValidationError):
    """Raised when multiple metrics are specified with the same result key in a
    request."""

    def __init__(
        self, duplicate_keys: str | list[str], metric_names: list[str]
    ) -> None:
        self.duplicate_keys: list[str] = (
            duplicate_keys if isinstance(duplicate_keys, list) else [duplicate_keys]
        )  # pragma: no cover
        self.metric_names: list[str] = metric_names
        duplicates: str = (
            f"'{duplicate_keys}'"
            if isinstance(duplicate_keys, str)
            else f"'{', '.join(self.duplicate_keys)}'"
        )  # pragma: no cover
        names: str = f"'{', '.join(self.metric_names)}'"
        super().__init__(f"Duplicate result keys {duplicates} for metrics: {names}.")


##########################
# Dataset-related errors #
##########################
class UnsupportedDatasetError(RequestValidationError):
    """Raised when no reader can handle a dataset spec."""

    def __init__(self, dataset_type: type[object]) -> None:
        self.dataset_type = dataset_type
        super().__init__(f"Unsupported dataset spec: {dataset_type.__name__}.")


class DatasetAccessError(EvaluationError):
    """Raised when a dataset cannot be accessed or read."""

    def __init__(self, *, path: str, reason: str) -> None:
        self.path = path
        self.reason = reason
        super().__init__(f"Failed to access dataset at {path!r}: {reason}")


class DatasetRecordError(EvaluationError):
    """Base exception for invalid or unusable dataset records."""

    def __init__(self, *, index: int, reason: str) -> None:
        self.index = index
        self.reason = reason
        super().__init__(f"Invalid dataset record at index {index}: {reason}")


class MissingTextFieldError(DatasetRecordError):
    """Raised when a record does not contain the configured text field."""

    def __init__(self, *, index: int, text_key: str, record: dict[str, Any]) -> None:
        self.text_key = text_key
        self.record = record
        super().__init__(
            index=index,
            reason=f"missing text field {text_key!r}",
        )


class MissingRecordIdFieldError(DatasetRecordError):
    """Raised when a record does not contain the configured record-id field."""

    def __init__(
        self,
        *,
        index: int,
        record_id_key: str,
        record: dict[str, Any],
    ) -> None:
        self.record_id_key = record_id_key
        self.record = record
        super().__init__(
            index=index,
            reason=f"missing record id field {record_id_key!r}",
        )
