from aiohttp.web import Request, Response, json_response, middleware
from google.protobuf import timestamp_pb2
from marshmallow import ValidationError

from maps_adv.warden.proto.errors_pb2 import Error
from maps_adv.warden.server.lib.domains.tasks import (
    ConflictOperation,
    ExecutorIdAlreadyUsed,
    StatusSequenceViolation,
    TaskInProgressByAnotherExecutor,
    TaskNotFound,
    TaskTypeAlreadyAssigned,
    TooEarlyForNewTask,
    UnknownTaskType,
    UpdateToInitialStatus,
)

__all__ = [
    "all_middlewares",
    "handle_conflict_operation",
    "handle_status_sequence_violation",
    "handle_update_to_initial_status",
    "handle_task_in_progress_by_another_executor",
    "handle_task_type_already_assigned",
    "handle_unknown_task_and_type",
    "handle_validation_error",
    "handle_too_early_to_create_new_task",
    "handle_executor_id_already_used",
]


@middleware
async def handle_unknown_task_and_type(req: Request, handler) -> Response:
    try:
        return await handler(req)
    except (UnknownTaskType, TaskNotFound):
        if req.path == "/tasks/":
            error = Error(code=Error.UNKNOWN_TASK_OR_TYPE)
            return Response(status=404, body=error.SerializeToString())
        else:
            return Response(status=404)


@middleware
async def handle_validation_error(req: Request, handler) -> Response:
    try:
        return await handler(req)
    except ValidationError as exc:
        if req.path == "/tasks/":
            error = Error(
                code=Error.VALIDATION_ERROR,
                description=", ".join(
                    f"{field}: {message}" for field, message in exc.messages.items()
                ),
            )
            return Response(status=400, body=error.SerializeToString())
        else:
            return json_response(data=exc.messages, status=400)


@middleware
async def handle_task_type_already_assigned(req: Request, handler) -> Response:
    try:
        return await handler(req)
    except TaskTypeAlreadyAssigned:
        if req.path == "/tasks/":
            error = Error(code=Error.TASK_TYPE_ALREADY_ASSIGNED)
            return Response(status=409, body=error.SerializeToString())
        else:
            return json_response(
                data={"error": ["Task of requested type is already assigned."]},
                status=409,
            )


@middleware
async def handle_too_early_to_create_new_task(req: Request, handler) -> Response:
    try:
        return await handler(req)
    except TooEarlyForNewTask as exc:
        if req.path == "/tasks/":
            error = Error(
                code=Error.TOO_EARLY_FOR_NEW_TASK_OF_REQUESTED_TYPE,
                scheduled_time=_dt_to_proto(exc.scheduled_time),
            )
            return Response(status=409, body=error.SerializeToString())
        else:
            return json_response(
                data={
                    "error": ["Too early to create new task of requested type."],
                    "scheduled_time": f"{exc.scheduled_time}",
                },
                status=409,
            )


@middleware
async def handle_task_in_progress_by_another_executor(
    req: Request, handler
) -> Response:
    try:
        return await handler(req)
    except TaskInProgressByAnotherExecutor:
        if req.path == "/tasks/":
            error = Error(code=Error.TASK_IN_PROGRESS_BY_ANOTHER_EXECUTOR)
            return Response(status=403, body=error.SerializeToString())
        else:
            return json_response(
                data={"error": ["Task is already in progress by another executor."]},
                status=403,
            )


@middleware
async def handle_status_sequence_violation(req: Request, handler) -> Response:
    try:
        return await handler(req)
    except StatusSequenceViolation as exc:
        if req.path == "/tasks/":
            error = Error(
                code=Error.STATUS_SEQUENCE_VIOLATION,
                description=f"Update from {exc.from_} to {exc.to} is not allowed.",
            )
            return Response(status=400, body=error.SerializeToString())
        else:
            return json_response(
                data={
                    "error": [f"Update from {exc.from_} to {exc.to} is not allowed."]
                },
                status=400,
            )


@middleware
async def handle_update_to_initial_status(req: Request, handler) -> Response:
    try:
        return await handler(req)
    except UpdateToInitialStatus:
        if req.path == "/tasks/":
            error = Error(code=Error.UPDATE_STATUS_TO_INITIAL)
            return Response(status=400, body=error.SerializeToString())
        else:
            return json_response(
                data={"error": ["Task status can't be updated to initial value."]},
                status=400,
            )


@middleware
async def handle_conflict_operation(req: Request, handler) -> Response:
    try:
        return await handler(req)
    except ConflictOperation:
        if req.path == "/tasks/":
            error = Error(code=Error.CONFLICT)
            return Response(status=409, body=error.SerializeToString())
        else:
            return json_response(
                data={"error": ["Request execution may result in a conflict."]},
                status=409,
            )


@middleware
async def handle_executor_id_already_used(req: Request, handler) -> Response:
    try:
        return await handler(req)
    except ExecutorIdAlreadyUsed:
        if req.path == "/tasks/":
            error = Error(code=Error.EXECUTOR_ID_ALREADY_USED)
            return Response(status=400, body=error.SerializeToString())
        else:
            return json_response(
                data={"error": ["Requested executor_id already has been used."]},
                status=400,
            )


_locals = locals()
all_middlewares = [_locals[name] for name in __all__ if name != "all_middlewares"]


def _dt_to_proto(dt):
    seconds, micros = map(int, "{:.6f}".format(dt.timestamp()).split("."))
    return timestamp_pb2.Timestamp(seconds=seconds, nanos=micros * 1000)
