"""
This module defines the various contracts, i.e. types of plugins supported by
Transformer.
The term "contract" indicates that these types constrain what plugin
implementors can do in Transformer.

Transformer plugins are just functions that accept certain inputs and have
certain outputs.
Different types of plugins have different input and output types.
Not all types of plugins can be applied at the same point in Transformer's
pipeline (e.g. python.Program objects are built much later than Task objects),
hence the multiplicity of contracts.
These input and output types are formalized here using Python's annotation
syntax and the typing module.

In addition to the plugin contracts, this module provides an "isvalid" method
for checking whether arbitrary Python objects conform to a given plugin contract.

# Plugin kinds

## OnTask

Kind of "stateless" plugins that operate independently on each task.
When implementing one, imagine their execution could be parallelized by
Transformer in the future.

Example: a plugin that injects a header in all requests.

## OnScenario

Kind of plugins that operate on scenarios.

Each scenario is the root of a tree composed of smaller scenarios and tasks
(the leaves of this tree). Therefore, in an OnScenario plugin, you have the
possibility of inspecting the subtree and making decisions based on that.
However, OnScenario plugins will be applied to all scenarios by Transformer,
so you don't need to recursively apply the plugin yourself on all subtrees.
If you do that, the plugin will be applied many times more than necessary.

Example: a plugin that keeps track of how long each scenario runs.

## OnPythonProgram

Kind of plugins that operate on the whole syntax tree.

The input and output of this kind of plugins is the complete, final locustfile
generated by Transformer, represented as a syntax tree.
OnPythonProgram plugins therefore have the most freedom compared to other
plugin kinds, because they can change anything.
Their downside is that manipulating the syntax tree is more complex than the
scenario tree or individual tasks.

Example: a plugin that injects some code in the global scope.

# Other types

This module also defines:

## Plugin

Any supported kind of Transformer plugin.
"""
import inspect
from typing import Sequence, Callable, Union, Dict, Any

from transformer import python
from transformer.decision import Decision
from transformer.task import Task, Task2

PluginValidator = Callable[[inspect.Signature], Decision]
_PluginValidatorDecorator = Callable[[PluginValidator], PluginValidator]

_PLUGIN_VALIDATORS: Dict[type, PluginValidator] = {}


def _register_contract(plugin_type: type) -> None:
    if plugin_type in _PLUGIN_VALIDATORS:
        raise ValueError(f"{plugin_type} already registered; cannot register it again")

    *expected_params, expected_return = plugin_type.__args__

    def _validator(sig: inspect.Signature) -> Decision:
        if sig.return_annotation != expected_return:
            return Decision.no(
                f"expected {expected_return}, got {sig.return_annotation}"
            )

        actual_params = [p.annotation for p in sig.parameters.values()]
        if actual_params != expected_params:
            return Decision.no(
                f"expected parameters {expected_params}, got {actual_params}"
            )

        return Decision.yes()

    _PLUGIN_VALIDATORS[plugin_type] = _validator


OnTask = Callable[[Task2], Task2]


OnScenario = Callable[["scenario.Scenario"], "scenario.Scenario"]


OnPythonProgram = Callable[[python.Program], python.Program]


# Historically Transformer has only one kind of plugin, which transformed a
# sequence of Task objects into another such sequence. Operating on a full list
# of tasks (instead of task by task) offered more leeway: a plugin could e.g.
# add a new task, or change only the first task.
# However this OnTaskSequence model is too constraining for some use-cases,
# e.g. when a plugin needs to inject code in the global scope, and having to
# deal with a full, immutable list of tasks in plugins that independently
# operate on each task implies a lot of verbosity and redundancy.
# For these reasons, other plugin kinds were created to offer a more varied
# choice for plugin implementers.
# See https://github.com/zalando-incubator/Transformer/issues/10.
OnTaskSequence = Callable[[Sequence[Task]], Sequence[Task]]


Plugin = Union[OnTask, OnScenario, OnPythonProgram, OnTaskSequence]

for plugin_type in Plugin.__args__:
    _register_contract(plugin_type)


def isvalid(plugin_type: type, obj: Any) -> Decision:
    """
    Checks whether obj is an implementation of the plugin contract plugin_type.
    The return value is basically a boolean, with an additional string
    describing the reason for this decision.

    :param plugin_type: plugin contract to verify obj against
    :param obj: any Python object
    :return: whether obj is conform to the plugin_type contract
    :raise TypeError: if plugin_type is not a plugin contract
    """
    if plugin_type is Plugin:
        return Decision.any(
            (isvalid(t, obj) for t in Plugin.__args__),
            "should be valid for a Plugin subtype",
        )

    if not callable(obj):
        return Decision.no(f"{obj!r} is not a function")

    try:
        validator = _PLUGIN_VALIDATORS[plugin_type]
    except KeyError:
        raise TypeError(f"no Plugin contract registered for {plugin_type}")

    try:
        actual_signature = inspect.signature(obj)
    except (ValueError, TypeError) as err:
        return Decision.no(f"could not extract signature from {obj!r}: {err}")

    return Decision.whether(
        validator(actual_signature), f"{obj.__name__!r} should implement {plugin_type}"
    )
