import inspect
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import List

import pop.exc
import pop.hub
import pop.loader


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
