from aiohttp.web import Request, Response, middleware
from marshmallow import ValidationError

from smb.common import aiotvm
from maps_adv.geosmb.doorman.proto.errors_pb2 import Error
from maps_adv.geosmb.doorman.server.lib.exceptions import (
    AttemptToUpdateClearedClient,
    BadClientData,
    ClientAlreadyExists,
    NoTvmTicket,
    TvmForbidden,
    UnknownClient,
    UnsupportedEventType,
)


def _dict_to_str(data: dict, separator=": ") -> str:
    return ", ".join(f"{field}{separator}{data[field]}" for field in sorted(data))


@middleware
async def handle_authentication_errors(req: Request, handler) -> Response:
    try:
        return await handler(req)
    except NoTvmTicket:
        error = Error(
            code=Error.NO_TVM_TICKET,
            description="X-Ya-Service-Ticket header is expected",
        )
        return Response(status=401, body=error.SerializeToString())
    except aiotvm.CheckTicketFails as exc:
        error = Error(
            code=Error.INVALID_TVM_TICKET, description=f"Token validation fails: {exc}"
        )
        return Response(status=401, body=error.SerializeToString())
    except TvmForbidden:
        error = Error(code=Error.TVM_FORBIDDEN, description="Access denied")
        return Response(status=403, body=error.SerializeToString())


@middleware
async def handle_validation_errors(req: Request, handler) -> Response:
    def _validation_response(exc):
        error = Error(code=Error.VALIDATION_ERROR, description=str(exc))
        return Response(status=400, body=error.SerializeToString())

    try:
        return await handler(req)
    except ValidationError as exc:
        if isinstance(exc.messages, dict):
            description = _dict_to_str(exc.messages)
        else:
            description = ", ".join(sorted(exc.messages))
        return _validation_response(description)
    except BadClientData as exc:
        return _validation_response(exc)


@middleware
async def handle_domain_errors(req: Request, handler) -> Response:
    def _conflict_response(error_code_pb, exc):
        error = Error(code=error_code_pb, description=str(exc))
        return Response(status=409, body=error.SerializeToString())

    try:
        return await handler(req)
    except ClientAlreadyExists as e:
        return _conflict_response(Error.CLIENT_ALREADY_EXISTS, e)
    except UnknownClient as e:
        search_fields_str = _dict_to_str(e.search_fields, separator="=")
        error = Error(
            code=Error.UNKNOWN_CLIENT,
            description=f"Unknown client with {search_fields_str}",
        )
        return Response(status=404, body=error.SerializeToString())
    except UnsupportedEventType as e:
        error = Error(
            code=Error.UNSUPPORTED_EVENT_TYPE,
            description=str(e),
        )
        return Response(status=400, body=error.SerializeToString())
    except AttemptToUpdateClearedClient:
        error = Error(
            code=Error.ATTEMPT_TO_UPDATE_CLEARED_CLIENT,
            description="Client has been cleared. It can't be updated.",
        )
        return Response(status=400, body=error.SerializeToString())
