# -*- coding: utf-8 -*-
# Copyright (c) 2015 Spotify AB

from __future__ import absolute_import, division, print_function
import re

import attr
from six import iteritems, iterkeys, itervalues


from .config import MEDIA_TYPES
from .errors import InvalidRAMLError
from .parameters import (
    Documentation, Header, Body, Response, URIParameter, QueryParameter,
    FormParameter, SecurityScheme
)
from .parser_utils import (
    security_schemes
)
from .raml import RootNode, ResourceNode, ResourceTypeNode, TraitNode
from .utils import (
    load_schema, _resource_type_lookup,
    _get_resource_type, _get_trait, _get_attribute,
    _get_inherited_attribute, _remove_duplicates, _create_uri_params,
    _get, _create_base_param_obj, _get_res_type_attribute,
    _get_inherited_type_params, _get_inherited_item, _get_attribute_dict,
    get_inherited, set_param_object, set_params, _get_data_union,
    _preserve_uri_order
)


__all__ = ["parse_raml"]


def parse_raml(loaded_raml, config):
    """
    Parse loaded RAML file into RAML/Python objects.

    :param RAMLDict loaded_raml: OrderedDict of loaded RAML file
    :returns: :py:class:`.raml.RootNode` object.
    :raises: :py:class:`.errors.InvalidRAMLError` when RAML file is invalid
    """

    validate = str(_get(config, "validate")).lower() == 'true'

    # Postpone validating the root node until the end; otherwise,
    # we end up with duplicate validation exceptions.
    attr.set_run_validators(False)
    root = create_root(loaded_raml, config)
    attr.set_run_validators(validate)

    root.security_schemes = create_sec_schemes(root.raml_obj, root)
    root.traits = create_traits(root.raml_obj, root)
    root.resource_types = create_resource_types(root.raml_obj, root)
    root.resources = create_resources(root.raml_obj, [], root,
                                      parent=None)

    if validate:
        attr.validate(root)  # need to validate again for root node

        if root.errors:
            raise InvalidRAMLError(root.errors)

    return root


def create_root(raml, config):
    """
    Creates a Root Node based off of the RAML's root section.

    :param RAMLDict raml: loaded RAML file
    :returns: :py:class:`.raml.RootNode` object with API root attributes set
    """

    errors = []

    def protocols():
        explicit_protos = _get(raml, "protocols")
        implicit_protos = re.findall(r"(https|http)", base_uri())
        implicit_protos = [p.upper() for p in implicit_protos]

        return explicit_protos or implicit_protos or None

    def base_uri():
        base_uri = _get(raml, "baseUri", "")
        if "{version}" in base_uri:
            base_uri = base_uri.replace("{version}",
                                        str(_get(raml, "version")))
        return base_uri

    def base_uri_params():
        data = _get(raml, "baseUriParameters", {})
        params = _create_base_param_obj(data, URIParameter, config, errors)
        uri = _get(raml, "baseUri", "")
        declared = _get(raml, "uriParameters", {})
        declared = list(iterkeys(declared))
        return _preserve_uri_order(uri, params, config, errors, declared)

    def uri_params():
        data = _get(raml, "uriParameters", {})
        params = _create_base_param_obj(data, URIParameter, config, errors)
        uri = _get(raml, "baseUri", "")

        declared = []
        base = base_uri_params()
        if base:
            declared = [p.name for p in base]
        return _preserve_uri_order(uri, params, config, errors, declared)

    def docs():
        d = _get(raml, "documentation", [])
        assert isinstance(d, list), "Error parsing documentation"
        docs = [Documentation(_get(i, "title"), _get(i, "content")) for i in d]
        return docs or None

    def schemas():
        _schemas = _get(raml, "schemas")
        if not _schemas:
            return None
        schemas = []
        for schema in _schemas:
            value = load_schema(list(itervalues(schema))[0])
            schemas.append({list(iterkeys(schema))[0]: value})
        return schemas or None

    return RootNode(
        raml_obj=raml,
        raw=raml,
        title=_get(raml, "title"),
        version=_get(raml, "version"),
        protocols=protocols(),
        base_uri=base_uri(),
        base_uri_params=base_uri_params(),
        uri_params=uri_params(),
        media_type=_get(raml, "mediaType"),
        documentation=docs(),
        schemas=schemas(),
        config=config,
        secured_by=_get(raml, "securedBy"),
        errors=errors
    )


def create_sec_schemes(raml_data, root):
    """
    Parse security schemes into ``SecurityScheme`` objects

    :param dict raml_data: Raw RAML data
    :param RootNode root: Root Node
    :returns: list of :py:class:`.parameters.SecurityScheme` objects
    """
    def _map_object_types(item):
        return {
            "headers": headers,
            "body": body,
            "responses": responses,
            "queryParameters": query_params,
            "uriParameters": uri_params,
            "formParameters": form_params,
            "usage": usage,
            "mediaType": media_type,
            "protocols": protocols,
            "documentation": documentation,
        }[item]

    def headers(header_data):
        _headers = []
        header_data = _get(header_data, "headers", {})
        for k, v in list(iteritems(header_data)):
            h = _create_base_param_obj({k: v},
                                       Header,
                                       root.config,
                                       root.errors)
            _headers.extend(h)
        return _headers

    def body(body_data):
        body_data = _get(body_data, "body", {})
        _body = []
        for k, v in list(iteritems(body_data)):
            body = Body(
                mime_type=k,
                raw=v,
                schema=load_schema(_get(v, "schema")),
                example=load_schema(_get(v, "example")),
                form_params=_get(v, "formParameters"),
                config=root.config,
                errors=root.errors
            )
            _body.append(body)
        return _body

    def responses(resp_data):
        _resps = []
        resp_data = _get(resp_data, "responses", {})
        for k, v in list(iteritems(resp_data)):
            response = Response(
                code=k,
                raw=v,
                desc=_get(v, "description"),
                headers=headers(_get(v, "headers", {})),
                body=body(_get(v, "body", {})),
                config=root.config,
                errors=root.errors
            )
            _resps.append(response)
        return sorted(_resps, key=lambda x: x.code)

    def query_params(param_data):
        param_data = _get(param_data, "queryParameters", {})
        _params = []
        for k, v in list(iteritems(param_data)):
            p = _create_base_param_obj({k: v},
                                       QueryParameter,
                                       root.config,
                                       root.errors)
            _params.extend(p)
        return _params

    def uri_params(param_data):
        param_data = _get(param_data, "uriParameters")
        _params = []
        for k, v in list(iteritems(param_data)):
            p = _create_base_param_obj({k: v},
                                       URIParameter,
                                       root.config,
                                       root.errors)
            _params.extend(p)
        return _params

    def form_params(param_data):
        param_data = _get(param_data, "formParameters", {})
        _params = []
        for k, v in list(iteritems(param_data)):
            p = _create_base_param_obj({k: v},
                                       FormParameter,
                                       root.config,
                                       root.errors)
            _params.extend(p)
        return _params

    def usage(desc_by_data):
        return _get(desc_by_data, "usage")

    def media_type(desc_by_data):
        return _get(desc_by_data, "mediaType")

    def protocols(desc_by_data):
        return _get(desc_by_data, "protocols")

    def documentation(desc_by_data):
        d = _get(desc_by_data, "documentation", [])
        assert isinstance(d, list), "Error parsing documentation"
        docs = [Documentation(_get(i, "title"), _get(i, "content")) for i in d]
        return docs or None

    def set_property(node, obj, node_data):
        func = _map_object_types(obj)
        item_objs = func({obj: node_data})
        setattr(node, func.__name__, item_objs)

    def initial_wrap(key, data):
        return SecurityScheme(
            name=key,
            raw=data,
            type=_get(data, "type"),
            described_by=_get(data, "describedBy", {}),
            desc=_get(data, "description"),
            settings=_get(data, "settings"),
            config=root.config,
            errors=root.errors