from collections import namedtuple
from typing import Any, Literal, TypedDict

Position = namedtuple("Position", ["line_no", "column_no", "index"])


class ErrorMessage:
    def __init__(self, text, code, index=None, position=None):
        self.text = text
        self.code = code
        self.index = index
        self.position = position

    def __eq__(self, other) -> bool:
        return (
            self.text == other.text
            and self.code == other.code
            and self.index == other.index
            and self.position == other.position
        )

    def __repr__(self):
        return f"{self.__class__.__name__}({self.text!r}, code={self.code!r}, index={self.index!r}, position={self.position!r})"


class ErrorResponse(Exception):
    """
    Raised when a client request results in an error response being returned.
    """

    def __init__(self, title: str, status_code: int, content: Any) -> None:
        self.title = title
        self.status_code = status_code
        self.content = content

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.title!r}, status_code={self.status_code!r}, content={self.content!r})"

    def __str__(self) -> str:
        return repr(self.content)


class ClientError(Exception):
    """
    Raised when a client is unable to fulfil an API request.
    """

    def __init__(self, messages):
        self.messages = messages
        super().__init__(messages)

    def __repr__(self):
        return f"{self.__class__.__name__}(messages={self.messages!r})"

    def __str__(self):
        if len(self.messages) == 1 and not self.messages[0].index:
            return self.messages[0].text
        return str(self.messages)


class SchemaError(Exception):
    """
    Any error occurring during the acquisition and processing of the OpenAPI schema.
    """


class ArkindexErrorMessage(TypedDict):
    attr: list[str | int] | None
    """
    Optional path to an attribute that this error is related to.
    For example, `["elements", 1, "id"]` is equivalent to `body["elements"][1]["id"]`.

    This typically matches an attribute of the body of the API request, but it can also refer to the query parameters,
    HTTP headers, or to other attributes in the model that may be affected in any way by the API request.

    This attribute may be `None` for errors that do not apply only to one specific field.

    Due to limitations of the API framework, `["non_field_errors"]` should be treated the same as `None`.
    """

    code: str
    """
    A machine-readable error code. This may be used to identify specific errors automatically.
    """

    detail: str
    """
    A human-readable error message.

    This should only be used for display purposes and should not be parsed automatically.
    To detect specific errors, use `code` instead.
    """


class ArkindexErrorContent(TypedDict):
    """
    Content of an error response that follows the standard Arkindex error format.
    """

    type: Literal["server_error", "client_error", "validation_error"]
    errors: list[ArkindexErrorMessage]


class ArkindexConflictErrorContent(ArkindexErrorContent):
    existing_id: str | None


class ArkindexError(ErrorResponse):
    """
    An error response returned by the API that matches the standard Arkindex error format.
    """

    # Redefine __init__ explicitly so that the types are taken into account by type checkers
    # This all can be replaced with an ErrorResponse[T] generic class when we drop Python 3.10 support
    def __init__(self, title: str, status_code: int, content: ArkindexErrorContent):
        self.title = title
        self.status_code = status_code
        self.content = content


class ArkindexServerError(ArkindexError):
    """
    A server error response (HTTP 5xx) returned by the API that matches the standard Arkindex error format.

    This does not match every HTTP 5xx response, only those with this specific format,
    so this should not be used to handle transient errors and retry a request.

    To handle all HTTP 5xx responses, use `except ErrorResponse` and test the `status_code`.
    """


class ArkindexClientError(ArkindexError):
    """
    A client error response (HTTP 4xx) returned by the API that matches the standard Arkindex error format.
    """


class ArkindexValidationError(ArkindexClientError):
    """
    A validation error response returned by the API that matches the standard Arkindex error format.

    This is a subtype of client errors that only uses HTTP 400,
    where the `attr` of each error message is usually defined,
    and that usually refers to issues with the request's parameters.
    """

    def __init__(self, title: str, status_code: Literal[400], content: ArkindexErrorContent):
        self.title = title
        self.status_code = status_code
        self.content = content


class ArkindexConflictError(ArkindexClientError):
    """
    A conflict error response returned by the API that matches the standard Arkindex error format.

    This is a subtype of client errors that only uses HTTP 409 and may contain an `existing_id` attribute
    that refers to the ID of an existing resource that caused the conflict.
    """

    def __init__(
        self,
        title: str,
        status_code: Literal[409],
        content: ArkindexConflictErrorContent,
    ):
        self.title = title
        self.status_code = status_code
        self.content = content
