#!/usr/bin/env python3
# Copyright 2010-2025 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Translate C++'s absl::Status errors to Python standard errors.

Here we try to use the standard Python errors we would use if the C++ code was
instead implemented in Python. This will give Python users a more familiar API.
"""

import enum
from typing import Optional, Type
from ortools.math_opt import rpc_pb2


class _StatusCode(enum.Enum):
    """The C++ absl::Status::code() values."""

    OK = 0
    CANCELLED = 1
    UNKNOWN = 2
    INVALID_ARGUMENT = 3
    DEADLINE_EXCEEDED = 4
    NOT_FOUND = 5
    ALREADY_EXISTS = 6
    PERMISSION_DENIED = 7
    UNAUTHENTICATED = 16
    RESOURCE_EXHAUSTED = 8
    FAILED_PRECONDITION = 9
    ABORTED = 10
    OUT_OF_RANGE = 11
    UNIMPLEMENTED = 12
    INTERNAL = 13
    UNAVAILABLE = 14
    DATA_LOSS = 15


class InternalMathOptError(RuntimeError):
    """Some MathOpt internal error.

    This error is usually raised because of a bug in MathOpt or one of the solver
    library it wraps.
    """


def status_proto_to_exception(
    status_proto: rpc_pb2.StatusProto,
) -> Optional[Exception]:
    """Returns the Python exception that best match the input absl::Status.

    There are some Status that we expect the MathOpt code to return, for those the
    matching exceptions are:
    - InvalidArgument: ValueError
    - FailedPrecondition: AssertionError
    - Unimplemented: NotImplementedError
    - Internal: InternalMathOptError

    Other Status's are not used by MathOpt, if they are seen a
    InternalMathOptError is raised (as if the Status was Internal) and the error
    message contains the unexpected code.

    Args:
      status_proto: The input proto to convert to an exception.

    Returns:
      The corresponding exception. None if the input status is OK.
    """
    try:
        code = _StatusCode(status_proto.code)
    except ValueError:
        return InternalMathOptError(
            f"unknown C++ error (code = {status_proto.code}):"
            f" {status_proto.message}"
        )

    if code == _StatusCode.OK:
        return None

    # For expected errors we compute the corresponding class.
    error_type: Optional[Type[Exception]] = None
    if code == _StatusCode.INVALID_ARGUMENT:
        error_type = ValueError
    if code == _StatusCode.FAILED_PRECONDITION:
        error_type = AssertionError
    if code == _StatusCode.UNIMPLEMENTED:
        error_type = NotImplementedError
    if code == _StatusCode.INTERNAL:
        error_type = InternalMathOptError

    if error_type is not None:
        return error_type(f"{status_proto.message} (was C++ {code.name})")

    return InternalMathOptError(
        f"unexpected C++ error {code.name}: {status_proto.message}"
    )
