from coreapi import Document, Link, Field
from coreapi.compat import string_types, urlparse
from coreapi.exceptions import ParseError


def _parse_document(data, base_url=None):
    schema_url = base_url
    base_url = _get_document_base_url(data, base_url)
    info = _get_dict(data, 'info')
    title = _get_string(info, 'title')
    consumes = get_strings(_get_list(data, 'consumes'))
    paths = _get_dict(data, 'paths')
    content = {}
    for path in paths.keys():
        url = base_url + path.lstrip('/')
        spec = _get_dict(paths, path)
        default_parameters = get_dicts(_get_list(spec, 'parameters'))
        for action in spec.keys():
            action = action.lower()
            if action not in ('get', 'put', 'post', 'delete', 'options', 'head', 'patch'):
                continue
            operation = _get_dict(spec, action)

            link_description = _get_string(operation, 'description')
            link_consumes = get_strings(_get_list(operation, 'consumes', consumes))

            # Determine any fields on the link.
            has_body = False
            has_form = False

            fields = []
            parameters = get_dicts(_get_list(operation, 'parameters', default_parameters), dereference_using=data)
            for parameter in parameters:
                name = _get_string(parameter, 'name')
                location = _get_string(parameter, 'in')
                required = _get_bool(parameter, 'required', default=(location == 'path'))
                description = _get_string(parameter, 'description')
                if location == 'body':
                    has_body = True
                    schema = _get_dict(parameter, 'schema', dereference_using=data)
                    expanded = _expand_schema(schema)
                    if expanded is not None:
                        expanded_fields = [
                            Field(name=field_name, location='form', required=is_required, description=field_description)
                            for field_name, is_required, field_description in expanded
                            if not any([field.name == field_name for field in fields])
                        ]
                        fields += expanded_fields
                    else:
                        field = Field(name=name, location='body', required=required, description=description)
                        fields.append(field)
                else:
                    if location == 'formData':
                        has_form = True
                        location = 'form'
                    field = Field(name=name, location=location, required=required, description=description)
                    fields.append(field)

            encoding = ''
            if has_body:
                encoding = _select_encoding(link_consumes)
            elif has_form:
                encoding = _select_encoding(link_consumes, form=True)

            link = Link(url=url, action=action, encoding=encoding, fields=fields, description=link_description)

            # Add the link to the document content.
            tags = get_strings(_get_list(operation, 'tags'))
            operation_id = _get_string(operation, 'operationId')
            if tags:
                tag = tags[0]
                prefix = tag + '_'
                if operation_id.startswith(prefix):
                    operation_id = operation_id[len(prefix):]
                if tag not in content:
                    content[tag] = {}
                content[tag][operation_id] = link
            else:
                content[operation_id] = link

    return Document(url=schema_url, title=title, content=content)


def _get_document_base_url(data, base_url=None):
    """
    Get the base url to use when constructing absolute paths from the
    relative ones provided in the schema defination.
    """
    prefered_schemes = ['https', 'http']
    if base_url:
        url_components = urlparse.urlparse(base_url)
        default_host = url_components.netloc
        default_scheme = url_components.scheme
    else:
        default_host = ''
        default_scheme = None

    host = _get_string(data, 'host', default=default_host)
    path = _get_string(data, 'basePath', default='/')
    path = '/' + path.lstrip('/')
    path = path.rstrip('/') + '/'

    if not host:
        # No host is provided, and we do not have an initial URL.
        return path

    schemes = _get_list(data, 'schemes')

    if not schemes:
        # No schemes provided, use the initial URL, or a fallback.
        scheme = default_scheme or prefered_schemes[0]
    elif default_scheme in schemes:
        # Schemes provided, the initial URL matches one of them.
        scheme = default_scheme
    else:
        # Schemes provided, the initial URL does not match, pick a fallback.
        for scheme in prefered_schemes:
            if scheme in schemes:
                break
        else:
            raise ParseError('Unsupported transport schemes "%s"' % schemes)

    return '%s://%s%s' % (scheme, host, path)


def _select_encoding(consumes, form=False):
    """
    Given an OpenAPI 'consumes' list, return a single 'encoding' for CoreAPI.
    """
    if form:
        preference = [
            'multipart/form-data',
            'application/x-www-form-urlencoded',
            'application/json'
        ]
    else:
        preference = [
            'application/json',
            'multipart/form-data',
            'application/x-www-form-urlencoded',
            'application/octet-stream'
        ]

    if not consumes:
        return preference[0]

    for media_type in preference:
        if media_type in consumes:
            return media_type

    return consumes[0]


def _expand_schema(schema):
    """
    When an OpenAPI parameter uses `in="body"`, and the schema type is "object",
    then we expand out the parameters of the object into individual fields.
    """
    schema_type = schema.get('type')
    schema_properties = _get_dict(schema, 'properties')
    schema_required = _get_list(schema, 'required')
    if ((schema_type == ['object']) or (schema_type == 'object')) and schema_properties:
        return [
            (key, key in schema_required, schema_properties[key].get('description'))
            for key in schema_properties.keys()
        ]
    return None


# Helper functions to get an expected type from a dictionary.

def dereference(lookup_string, struct):
    """
    Dereference a JSON pointer.
    http://tools.ietf.org/html/rfc6901
    """
    keys = lookup_string.strip('#/').split('/')
    node = struct
    for key in keys:
        node = _get_dict(node, key)
    return node


def is_json_pointer(value):
    return isinstance(value, dict) and ('$ref' in value) and (len(value) == 1)


def _get_string(item, key, default=''):
    value = item.get(key)
    return value if isinstance(value, string_types) else default


def _get_dict(item, key, default={}, dereference_using=None):
    value = item.get(key)
    if isinstance(value, dict):
        if dereference_using and is_json_pointer(value):
            return dereference(value['$ref'], dereference_using)
        return value
    return default.copy()


def _get_list(item, key, default=[]):
    value = item.get(key)
    return value if isinstance(value, list) else list(default)


def _get_bool(item, key, default=False):
    value = item.get(key)
    return value if isinstance(value, bool) else default


# Helper functions to get an expected type from a list.

def get_dicts(item, dereference_using=None):
    ret = [value for value in item if isinstance(value, dict)]
    if dereference_using:
        return [
            dereference(value['$ref'], dereference_using) if is_json_pointer(value) else value
            for value in ret
        ]
    return ret


def get_strings(item):
    return [value for value in item if isinstance(value, string_types)]
