"""
Error handlers for the API.

:copyright: Copyright 2013-2017, Dorian Pula <dorian.pula@amber-penguin-software.ca>
:license: AGPL v3+
"""

import http
import logging
import traceback

import flask
import jsonschema
from werkzeug import exceptions

logger = logging.getLogger(__name__)


HTTP_ERRORS_WITH_PATHS = [
    http.HTTPStatus.NOT_FOUND,
    http.HTTPStatus.UNAUTHORIZED,
]
HTTP_ERRORS_WITHOUT_PATHS = [
    http_code
    for http_code in exceptions.default_exceptions
    if http_code not in HTTP_ERRORS_WITH_PATHS
]

MESSAGE_FOR_STATUS = {
    http.HTTPStatus.NOT_FOUND.value: 'Resource not found.',
    http.HTTPStatus.UNAUTHORIZED.value: 'Not authorized to access this resource.',
}


class InvalidOperationError(Exception):
    def __init__(self, message=http.HTTPStatus.FORBIDDEN.description):
        self.message = message


class ConflictingResourceError(Exception):
    def __init__(self, message=http.HTTPStatus.CONFLICT.description):
        self.message = message


class InvalidCredentialsError(Exception):
    def __init__(self, username):
        self.message = 'Invalid credentials provided.'
        self.username = username
        self.code = http.HTTPStatus.UNAUTHORIZED


def get_message_for_status(http_code):
    try:
        http_status = http.HTTPStatus(http_code)
    except ValueError:
        logger.warning(f'Unexpected HTTP status code: {http_code}')
        http_status = http.HTTPStatus.INTERNAL_SERVER_ERROR
    return MESSAGE_FOR_STATUS.get(http_status, http_status.description)


def error_message_from_http_code_with_resource(error):
    error_message = {
        'error': {
            'status_code': error.code,
            'message': get_message_for_status(error.code),
            'resource': flask.request.path,
        },
    }
    return flask.jsonify(error_message), error.code


def error_message_from_http_code(error):
    logger.error(f'HTTP {error.code} occurred while processing "{flask.request.url}": "{error}"')
    error_message = {
        'error': {
            'status_code': error.code,
            'message': get_message_for_status(error.code),
        },
    }
    return flask.jsonify(error_message), error.code


def error_message_from_invalid_credentials(error):
    logger.error(f'Invalid credentials provided for "{error.username}"')
    error_message = {
        'error': {
            'status_code': error.code,
            'message': error.message,
        },
    }
    return flask.jsonify(error_message), error.code


def error_message_from_invalid_op_error(error: InvalidOperationError):
    logger.error(f'Invalid operation for "{flask.request.url}" because "{error.message}"')
    error_message = {
        'error': {
            'status_code': http.HTTPStatus.FORBIDDEN,
            'message': f'{error.message}',
            'resource': flask.request.url,
        },
    }
    return flask.jsonify(error_message), http.HTTPStatus.FORBIDDEN


def error_message_from_conflicting_resource_error(error: ConflictingResourceError):
    logger.error(f'Conflicting resource for "{flask.request.url}" because "{error.message}"')
    error_message = {
        'error': {
            'status_code': http.HTTPStatus.CONFLICT,
            'message': f'{error.message}',
            'resource': flask.request.url,
        }
    }

    return flask.jsonify(error_message), http.HTTPStatus.CONFLICT


def error_message_from_exception(error: Exception):
    logger.exception(f'Uncaught error while processing "{flask.request.url}":')
    error_message = {
        'error': {
            'status_code': http.HTTPStatus.INTERNAL_SERVER_ERROR,
            'message': f'{type(error)} with {error.args}: {error}',
            'stack_trace': traceback.format_exc(),
        },
    }
    return flask.jsonify(error_message), http.HTTPStatus.INTERNAL_SERVER_ERROR


def error_message_from_json_schema_validation_error(error: jsonschema.ValidationError):
    logger.exception(f'Invalid json {flask.request.json} for "{flask.request.url}":')
    error_message = {
        'error': {
            'status_code': http.HTTPStatus.BAD_REQUEST,
            'message': f'{error}',
        },
    }
    return flask.jsonify(error_message), http.HTTPStatus.BAD_REQUEST


def attach_error_handlers_to_api_app(app):
    for http_code in HTTP_ERRORS_WITH_PATHS:
        app.register_error_handler(
            http_code, error_message_from_http_code_with_resource)

    for http_code in HTTP_ERRORS_WITHOUT_PATHS:
        app.register_error_handler(http_code, error_message_from_http_code)

    app.register_error_handler(
        InvalidOperationError,
        error_message_from_invalid_op_error
    )
    app.register_error_handler(
        ConflictingResourceError,
        error_message_from_conflicting_resource_error
    )
    app.register_error_handler(
        jsonschema.ValidationError,
        error_message_from_json_schema_validation_error
    )
    app.register_error_handler(
        InvalidCredentialsError,
        error_message_from_invalid_credentials
    )
    app.register_error_handler(Exception, error_message_from_exception)
