# Import python libs
import inspect

# Import pop libs
import pop.exc
import pop.hub
import pop.loader
from typing import Any, Callable, Dict, Iterable, List


def contract(
    hub: "pop.hub.Hub",  # pylint: disable=unused-argument
    raws: Iterable["pop.loader.LoadedMod"],
    mod: "pop.loader.LoadedMod",
):
    """
    Verify module level contract - functions only
    :param hub: The redistributed pop central hub
    :param raws: A list of loaded modules with contracts
    :param mod: A loader module
    """
    sig_errs = []
    sig_miss = []
    mname = mod.__name__
    for raw in raws:
        if isinstance(raw, pop.loader.LoadError):
            sig_errs.append(str(raw))
            continue
        else:
            for fun in raw._funcs:
                if fun.startswith("sig_"):
                    tfun = fun[4:]
                    if tfun not in mod._funcs:
                        sig_miss.append(tfun)
                        continue
                    sig_errs.extend(sig(mod._funcs[tfun].func, raw._funcs[fun].func))
    if sig_errs or sig_miss:
        msg = ""
        if sig_errs:
            msg += f"Signature Errors in {mname}:\n"
            for err in sig_errs:
                msg += f"{err}\n"
        if sig_miss:
            msg += f"Signature Functions Missing in {mname}:\n"
            for err in sig_miss:
                msg += f"{err}\n"
        msg = msg.strip()
        raise pop.exc.ContractSigException(msg)


def sig_map(ver: Callable) -> Dict[str, Any]:
    """
    Generates the map dict for the signature verification
    """
    vsig = inspect.signature(ver)
    vparams = list(vsig.parameters.values())
    vdat = {"args": [], "v_pos": -1, "kw": [], "kwargs": False, "ann": {}}
    for ind in range(len(vparams)):
        param = vparams[ind]
        val = param.kind.value
        name = param.name
        if val == 0 or val == 1:
            vdat["args"].append(name)
            if param.default != inspect._empty:  # Is a KW, can be inside of **kwargs
                vdat["kw"].append(name)
        elif val == 2:
            vdat["v_pos"] = ind
        elif val == 3:
            vdat["kw"].append(name)
        elif val == 4:
            vdat["kwargs"] = ind
        if param.annotation != inspect._empty:
            vdat["ann"][name] = param.annotation
    return vdat


def sig(func: Callable, ver: Callable) -> List[str]:
    """
    Takes 2 functions, the first function is verified to have a parameter signature
    compatible with the second function
    """
    errors = []
    fsig = inspect.signature(func)
    fparams = list(fsig.parameters.values())
    vdat = sig_map(ver)
    arg_len = len(vdat["args"])
    v_pos = False
    for ind in range(len(fparams)):
        param = fparams[ind]
        val = param.kind.value
        name = param.name
        has_default = param.default != inspect._empty
        ann = param.annotation
        vann = vdat["ann"].get(name, inspect._empty)
        if vann != ann:
            errors.append(f'Parameter, "{name}" is type "{str(ann)}" not "{str(vann)}"')
        if val == 2:
            v_pos = True
        if val == 0 or val == 1:
            if ind >= arg_len:  # Past available positional args
                if not vdat["v_pos"] == -1:  # Has a *args
                    if ind >= vdat["v_pos"] and v_pos:
                        # Invalid unless it is a kw
                        if not name in vdat["kw"]:
                            # Is a kw
                            errors.append(f'Parameter "{name}" is invalid')
                        if vdat["kwargs"] is False:
                            errors.append(f'Parameter "{name}" not defined as kw only')
                        continue
                elif vdat["kwargs"] is not False and not has_default:
                    errors.append(
                        f'Parameter "{name}" is past available positional params'
                    )
                elif vdat["kwargs"] is False:
                    errors.append(
                        f'Parameter "{name}" is past available positional params'
                    )
            else:
                v_param = vdat["args"][ind]
                if v_param != name:
                    errors.append(
                        f'Parameter "{name}" does not have the correct name: {v_param}'
                    )
        if val == 2:
            if ind < vdat["v_pos"]:
                errors.append(
                    f'Parameter "{name}" is not in the correct position for *args'
                )
        if val == 3:
            if name not in vdat["kw"] and not vdat["kwargs"]:
                errors.append(f'Parameter "{name}" is not available as a kwarg')
        if val == 4:
            if vdat["kwargs"] is False:
                errors.append(f"Kwargs are not permitted as a parameter")
    return errors
