#
# Copyright (c) 2022 Airbyte, Inc., all rights reserved.
#

import inspect

from airbyte_cdk.sources.declarative.interpolation.interpolated_mapping import InterpolatedMapping
from airbyte_cdk.sources.declarative.interpolation.jinja import JinjaInterpolation

"""
    Create a partial on steroids.
    Returns a partial object which when called will behave like func called with the arguments supplied.
    Parameters will be interpolated before the creation of the object
    The interpolation will take in kwargs, and config as parameters that can be accessed through interpolating.
    If any of the parameters are also create functions, they will also be created.
    kwargs are propagated to the recursive method calls
    :param func: Function
    :param args:
    :param keywords:
    :return: partially created object
    """


def create(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        interpolation = JinjaInterpolation()
        all_keywords = {**keywords}
        all_keywords.update(fkeywords)

        # config is a special keyword used for interpolation
        config = all_keywords.pop("config", None)

        # options is a special keyword used for interpolation and propagation
        if "options" in all_keywords:
            options = all_keywords.pop("options")
        else:
            options = dict()

        # create object's partial parameters
        fully_created = _create_inner_objects(all_keywords, options)

        # interpolate the parameters
        interpolated_keywords = InterpolatedMapping(fully_created, interpolation).eval(config, **{"options": options})
        interpolated_keywords = {k: v for k, v in interpolated_keywords.items() if v is not None}

        all_keywords.update(interpolated_keywords)

        # if config is not none, add it back to the keywords mapping
        if config is not None:
            all_keywords["config"] = config

        kwargs_to_pass_down = _get_kwargs_to_pass_to_func(func, options)
        all_keywords_to_pass_down = _get_kwargs_to_pass_to_func(func, all_keywords)
        try:
            ret = func(*args, *fargs, **{**all_keywords_to_pass_down, **kwargs_to_pass_down})
        except TypeError as e:
            raise Exception(f"failed to create object of type {func} because {e}")
        return ret

    newfunc.func = func
    newfunc.args = args
    newfunc.kwargs = keywords

    return newfunc


def _get_kwargs_to_pass_to_func(func, kwargs):
    argspec = inspect.getfullargspec(func)
    kwargs_to_pass_down = set(argspec.kwonlyargs)
    args_to_pass_down = set(argspec.args)
    all_args = args_to_pass_down.union(kwargs_to_pass_down)
    kwargs_to_pass_down = {k: v for k, v in kwargs.items() if k in all_args}
    return kwargs_to_pass_down


def _create_inner_objects(keywords, kwargs):
    fully_created = dict()
    for k, v in keywords.items():
        if type(v) == type(create):
            fully_created[k] = v(kwargs=kwargs)
        else:
            fully_created[k] = v
    return fully_created
