"""Took for traversing a Pyramid Traversal node graph and generating an OpenAPI spec to describe it."""

import inspect

from pathlib import Path

from pyramid.path import DottedNameResolver

from .helpers import (
    set_request_body,
    set_query_params,
    set_response_body,
    get_operation_id,
    split_docstring,
    set_tag,
    schema_name_resolver,
)

from apispec import APISpec, utils

import logging

log = logging.getLogger(__name__)


def ops_from_view(
    spec, introspectable, url_params, response_types, gen_external_docs_callback=None
):
    """Generates a dictionary of operations for a given introspectable(view).

    `url_params` are url params that apply to the URL we are currently at.
    `traverse_resource` deduces any url params for the current URL,
    and also passes them recursively to child resources.
    """
    # Do openapi magic here
    # Taken from pyramid-marshmallow
    operations = {}
    methods = introspectable["request_methods"] or ["GET"]
    if isinstance(methods, str):
        methods = [methods]
    for method in methods:
        method = method.lower()
        operations[method] = introspectable

    final_ops = dict()
    for method, view in operations.items():
        summary, descr, user_op = split_docstring(view["callable"].__doc__)
        op = {
            "responses": dict(),
            "parameters": url_params.copy(),
            "operationId": get_operation_id(view)
            if len(operations) == 1
            else f"{method}_{get_operation_id(view)}",
        }
        if gen_external_docs_callback is not None:
            op["externalDocs"] = gen_external_docs_callback(view["callable"])
        if summary:
            op["summary"] = summary
        if descr:
            op["description"] = descr

        if "validate" in view:
            if method == "get":
                set_query_params(spec, op, view)
            else:
                set_request_body(spec, op, view)
        if "marshal_responses" in view:
            set_response_body(spec, op, view)
        set_tag(spec, op, view)
        final_op = utils.deepupdate(op, user_op)
        final_op = utils.deepupdate(final_op, view.get("api_spec", dict()))

        # We are required to have some response, so make one up.
        if "200" not in final_op["responses"]:
            final_op["responses"]["200"] = {
                "description": "",
            }

        # Naive auto-detection of resources that represent an SQL table
        if "404" not in final_op["responses"] and len(url_params) > 0:
            final_op["responses"]["404"] = {
                "description": "An object was not found for "
                + " or ".join(map(lambda param: f"`{param['name']}`", url_params))
            }
        if "400" not in final_op["responses"] and "validate" in view:
            final_op["responses"]["400"] = {"description": "Request validation failed"}

        # Any response types generated by framework are also added
        for key, value in response_types.items():
            log.debug(f"Checking if {key} in responses")
            if key not in final_op["responses"]:
                log.debug(f"Adding response {key}")
                final_op["responses"][key] = value

        log.debug(f"FINAL OP: {final_op}")
        final_ops[method] = final_op

    return final_ops


def populate_url_params(param):
    """Takes a param definition as defined on a resource,
    and adds necessary fields to make it an OpenAPI URL parameter
    """
    return {**param, "in": "path", "required": True}


def traverse_resource(
    spec,
    introspector,
    context,
    path="",
    url_params=[],
    response_types={},
    gen_external_docs_callback=None,
):
    log.info(
        f"Traversing {context} at path {path} inherited response types {response_types}"
    )
    # Iterate all views of this resource
    for item in introspector.get_category("views", context):
        introspectable = item["introspectable"]
        if introspectable["context"] == context:
            view_path = f"{path}/{introspectable['name']}"

            ops = ops_from_view(
                spec,
                introspectable,
                url_params,
                response_types,
                gen_external_docs_callback,
            )
            spec.path(view_path, operations=ops)
        else:
            # log.info(item["introspectable"]["context"])
            pass

    # Then, check child resources
    new_children = getattr(context, "static_children", None)

    dynamic_child = context.get_dynamic_openapi_info()
    if dynamic_child:
        log.info("Path has dynamic children")
        new_url_params = url_params + list(
            map(populate_url_params, dynamic_child["params"])
        )
        # We need to determine some sane name for the url param
        # So we pick table name + primary key name
        traverse_resource(
            spec,
            introspector,
            dynamic_child["resource"],
            f"{path}/"
            + "/".join(
                map(lambda param: "{" + param["name"] + "}", dynamic_child["params"])
            ),
            new_url_params,
            response_types,
            gen_external_docs_callback,
        )

    if new_children:
        log.info("Path has children")

        # Supports both dynamic and static children at the same time
        for k, v in new_children.items():
            traverse_resource(
                spec,
                introspector,
                v,
                f"{path}/{k}",
                url_params,
                response_types,
                gen_external_docs_callback,
            )

    log.info("Path has no children")


def generate_apispec(
    name,
    version,
    registry,
    resource,
    path="",
    global_response_types={},
    gen_external_docs_callback=None,
) -> dict:
    """Returns an APISpec object from traversing the tree with a given resource as root.
    `path` specifies an optional URL prefix for every endpoint

    :param name:
        The name of the API that is documented
    :param version:
        The version of the API that is documented
    :param registry:
        An instance of a pyramid registry
    :param resource:
        The traversal context to use as root
    :param path:
        An optional path prefix
    :param global_response_types:
        Any HTTP response codes that may be returned due to how the server is configured (f.ex extra validation or security)
    :param gen_external_docs_callback:
        An optional callback that takes a view and returns a dict containing an external documentation object. See https://swagger.io/specification/#external-documentation-object
    """

    name_resolver = DottedNameResolver()
    MarshmallowPlugin = name_resolver.maybe_resolve(
        registry.settings.get(
            "openapi.plugin", "apispec.ext.marshmallow.MarshmallowPlugin"
        )
    )

    marshmallow_plugin = MarshmallowPlugin(schema_name_resolver=schema_name_resolver)

    spec = APISpec(
        name,
        version,
        openapi_version="3.0.0",
        plugins=[marshmallow_plugin],
    )

    traverse_resource(
        spec,
        registry.introspector,
        resource,
        path,
        response_types=global_response_types,
        gen_external_docs_callback=gen_external_docs_callback,
    )
    return spec.to_dict()
