# -*- coding: utf-8 -*-
import logging

from six import iteritems


log = logging.getLogger(__name__)


def operation_docstring_wrapper(operation):
    """
    Workaround for docstrings to work for operations in a sane way.

    Docstrings can only be specified for modules, classes, and functions. Hence,
    the docstring for an Operation is static as defined by the Operation class.
    To make the docstring for an Operation invocation work correctly, it has
    to be masquaraded as a function. The docstring will then be attached to the
    function which works quite nicely. Example in REPL:

    >> petstore = SwaggerClient.from_url('http://url_to_petstore/')
    >> pet = petstore.pet
    >> help(pet.getPetById)

    Help on function getPetById in module bravado_core.resource:

    getPetById(**kwargs)
        [GET] Find pet by ID

        Returns a pet when ID < 10.  ID > 10 or nonintegers will simulate API error conditions  # noqa

        :param petId: ID of pet that needs to be fetched
        :type petId: integer
        :returns: 200: successful operation
        :rtype: #/definitions/Pet
        :returns: 400: Invalid ID supplied
        :returns: 404: Pet not found

    :param operation: :class:`Operation`
    :return: callable that executes the operation
    """
    def wrapper(**kwargs):
        return operation(**kwargs)

    # TODO: make docstring lazy by using 'docstring_property'
    wrapper.__doc__ = create_operation_docstring(operation)
    wrapper.__name__ = str(operation.operation_id)
    return wrapper


def create_operation_docstring(op):
    """Builds a comprehensive docstring for an Operation.

    :param op: :class:`bravado_core.operation.Operation`
    :rtype: str or unicode

    Example: ::

        client.pet.findPetsByStatus?

    Outputs: ::

        [GET] Finds Pets by status

        Multiple status values can be provided with comma seperated strings

        :param status: Statuses to be considered for filter
        :type status: str
        :param from_date: Start date filter
        :type from_date: str
        :rtype: list
    """
    # TODO: remove once lazy docstrings implemented
    log.debug('creating op docstring for %s' % op.operation_id)
    s = ''
    op_spec = op.op_spec
    is_deprecated = op_spec.get('deprecated', False)
    if is_deprecated:
        s += '** DEPRECATED **\n'

    summary = op_spec.get('summary')
    if summary:
        s += u'[{0}] {1}\n\n'.format(op.http_method.upper(), summary)

    desc = op_spec.get('description')
    if desc:
        s += u'{0}\n\n'.format(desc)

    # TODO: add shared parameters
    for param_spec in op_spec.get('parameters', []):
        s += create_param_docstring(param_spec)

    # TODO: add 'examples' if available
    # TODO: good idea to identify the default response?
    responses = op_spec.get('responses')
    for http_status_code, response_spec in iter(sorted(iteritems(responses))):
        response_desc = response_spec.get('description')
        s += u':returns: {0}: {1}\n'.format(http_status_code, response_desc)
        schema_spec = response_spec.get('schema')
        if schema_spec:
            s += u':rtype: {0}\n'.format(formatted_type(schema_spec))
    return s


def create_param_docstring(param_spec):
    """Builds the docstring for a parameter from its specification.

    :param param_spec: parameter spec in json-line dict form
    :rtype: str or unicode

    Example: ::
        :param status: Status to be considered for filter
        :type status: string
    """
    name = param_spec.get('name')
    desc = param_spec.get('description', 'Document your spec, yo!')
    desc = desc if desc else 'Document your spec, yo!'
    default_value = param_spec.get('default')
    location = param_spec.get('in')
    required = param_spec.get('required', False)

    s = u':param {0}: {1}'.format(name, desc)
    if default_value is not None:
        s += u' (Default: {0})'.format(default_value)
    if not required:
        s += ' (optional)'
    s += '\n'

    if location == 'body':
        param_type = formatted_type(param_spec.get('schema'))
    else:
        param_type = param_spec.get('type')
    s += u':type {0}: {1}\n'.format(name, param_type)

    # TODO: Lot more stuff can go in here - see "Parameter Object" in 2.0 Spec.
    return s


def formatted_type(spec):
    """Returns the swagger type from a spec in a colon separated format.

    Example:

    .. code-block:: python

        {
            ...
            "type": "array",
            "items": {
                 "type": "integer",
                 "format": "int64"
                 }
            ...
        }

    Returns:

    .. code-block:: python

        "array:integer:int64"


    :param spec: object spec in dict form
    :rtype: str
    """
    obj_type = spec.get('type')
    obj_format = spec.get('format')
    ref = spec.get('$ref')
    if obj_format and obj_type:
        return "{0}:{1}".format(obj_type, obj_format)
    elif obj_type == 'array':
        return "{0}:{1}".format(obj_type, formatted_type(spec["items"]))
    elif ref:
        return ref
    elif obj_type:
        return obj_type
    else:  # not obj_type
        # if 'default_type_to_object' config is True, then this is defaulted to object type
        return 'notype'
