from __future__ import annotations

import logging
import operator
from collections.abc import Sequence
from typing import TYPE_CHECKING, Generic, TypeVar

from fastapi.exceptions import RequestValidationError

from .models import DiagnosticError, DiagnosticResponse

if TYPE_CHECKING:
    from gcm_diagnostics.collector import DiagnosticCollector

T = TypeVar("T", bound=DiagnosticError)


class DiagnosticException(RequestValidationError, Generic[T]):
    """
    Exception thrown when DiagnosticCollector collects any error. Contains all the collected errors, which
    then can be presented to user.
    """

    def __init__(
        self, detail: Sequence[T] | None = None, logger: logging.Logger | None = None, *, raised_from: DiagnosticCollector
    ) -> None:
        super().__init__(detail or [])
        self.logger = logger or logging.root
        self.raised_from = raised_from

    def errors(self) -> Sequence[T]:
        """
        Return all collected errors.
        """
        return super().errors()

    @property
    def status_code(self) -> int:
        """
        Return combined HTTP status code of the errors. If there are only errors of one type, return that
        error type's status code. Otherwise, returns generic 422 (Unprocessable entity) error.
        :return:
        """
        status_codes: set[int] = {error.status_code for error in self.errors()}

        # If there is only one status code in all errors, return it.
        if len(status_codes) == 1:
            return status_codes.pop()

        # Decide which status code to return based on the highest 100th of the status codes.
        max_status_code_family = max(status_codes) // 100 * 100

        # Otherwise, return generic 422 status code.
        self.logger.debug(
            "Multiple error types detected (%r), returning aggregated %d status code", status_codes, max_status_code_family
        )

        return max_status_code_family

    @property
    def response(self) -> DiagnosticResponse[T]:
        """
        Create response model that represents the error. It has the same format as built-in FastAPIs
        RequestValidationError to provide uniform error reporting both from OpenAPI validations and
        also from business logic validations.
        """
        return DiagnosticResponse(
            detail=sorted(self.errors(), key=operator.attrgetter("loc")),
        )

    def __str__(self) -> str:
        return "Diagnostics:\n" + "\n".join(f"  - {error!s}" for error in self.errors())
