from typing import Dict, List, Optional, Union

from clipped.compact.pydantic import Field, StrictStr, validator
from clipped.types.ref_or_obj import BoolOrRef, RefField

from polyaxon._flow.params import V1Param
from polyaxon._schemas.base import BaseSchemaModel
from polyaxon._schemas.lifecycle import V1Statuses


class V1Hook(BaseSchemaModel):
    """You can configure Polyaxon to send notifications and webhooks to users and systems
    when operations reaches a final state,
    or trigger any logic that is tightly coupled with a class of operations.

    Hooks allow you to build or set up integrations and dependencies
    based on final events generated by Polyaxon.
    External systems can subscribe or provide handling for certain events.
    When one of those events is triggered, Polyaxon will trigger the components defined
    in the hooks sections to perform a post-done logic, like sending an HTTP request
    payload to the webhook's configured URL.

    In addition to the main use-case of hooks, i.e. notifications,
    you can also use hooks to trigger a post-success logic that does
    not have to run on the same container, does not require specific accelerators,
    or is not specific to a single operation.
    For instance, you can build a custom hook to:
      * run evaluation after a training operation succeeds.
      * perform data or model checks.

    Polyaxon can trigger hooks when a run reaches a final status:

     * succeeded
     * failed
     * stopped
     * done (any final state)

    You can additionally provide a set of `conditions`
    to perform additional checks before triggering the logic, for instance,
    in addition to the success status, you can restrict the hook to only trigger
    if a metric has reached a certain value: `conditions: {{ loss > 0.9 }}`.

    You can resolve any context information from the main operation inside hooks,
    like params, globals, ...

    Args:
        trigger: str
        connection: str
        hub_ref: str
        conditions: str, optional
        queue: str, optional
        namespace: str, optional
        presets: List[str], optional
        disableDefaults: bool, optional
        params: Dict[str, [V1Param](/docs/core/specification/params/)], optional

    ## YAML usage

    ```yaml
    >>> hook:
    >>>   trigger: failed
    >>>   connection: slack-connection
    >>>   hubRef: slack
    ```

    ## Python usage

    ```python
    >>> from polyaxon.schemas import V1Statuses, V1Hook
    >>> hook = V1Hook(
    >>>     trigger=V1Statuses.FAILED,
    >>>     hub_ref="slack",
    >>>     connection="slack-connection",
    >>> )
    ```

    ## Fields

    ### connection

    The connection to notify, this [connection](/docs/setup/connections/)
    must be configured at deployment time to be used here by referencing the name.

    ```yaml
    >>> hook:
    >>>   connection: some-connection
    ```

    ### trigger

    The trigger represents the status condition to check before sending the notification.

    ```yaml
    >>> hook:
    >>>   trigger: succeeded
    ```

    In this example, the notification will be sent if the run succeeds.

    ### hubRef

    Polyaxon provides a [Component Hub](/docs/management/component-hub/)
    for hosting versioned components with an access control system to improve
    the productivity of your team.

    To trigger a hook based on a component hosted on Polyaxon Component Hub, you can use `hubRef`

    ```yaml
    >>> hook:
    >>>   hubRef: slack
    ...
    ```

    Or custom hook component

    ```yaml
    >>> hook:
    >>>   hubRef: my-component:dev
    ...
    ```

    ### conditions

    After the main operation is done, conditions take advantage of all context values
    resolved from the main operation, including outputs, to decide if the hook can be started.


    ```yaml
    >>>   conditions: '{{ some-io-param == "some-value" }}'
    ```

    In the example above, the hook will only run if a param is passed, or an output is logged and
    is equal to "some-value".

    ### presets

    The [presets](/docs/management/organizations/presets/) to use for the hook operation,
    if provided, it will override the component's presets otherwise
    the presets of the component will be used if available.

    ```yaml
    >>> hook:
    >>>   presets: [test]
    ```

    ### queue

    The [queue](/docs/core/scheduling-strategies/queues/) to use.
    If not provided, the default queue will be used.

    ```yaml
    >>> hook:
    >>>   queue: agent-name/queue-name
    ```

    If the agent name is not specified, Polyaxon will resolve the name of the queue
    based on the default agent.

    ```yaml
    >>> hook:
    >>>   queue: queue-name
    ```

    ### namespace

    > **Note**: Please note that this field is only available in some commercial editions.

    The namespace to use, if not provided, it will default to the agent's namespace.

    ```yaml
    >>> hook:
    >>>   namespace: polyaxon
    ```

    ### disableDefaults

    One major difference between hooks and normal operations,
    is that hooks will be initialized automatically with:
    `uuid`, `kind`, `name`, `inputs`, `outputs`, `status`, and `condition` of the main operation as
    context only params, to reduce the boilerplate and the need to request usual information
    required for most notification operations.

    If you do not need these context values or if you decide to request params manually,
    you can set this field to `false`.

    ```yaml
    >>> hook:
    >>>   disableDefaults: true
    >>>   ...
    ```

    ### params

    The [params](/docs/core/specification/params/) to pass if the handler requires extra params,
    they will be validated against the inputs/outputs.
    If a parameter is passed and the component does not define a corresponding inputs/outputs,
    a validation error will be raised unless the param has the `contextOnly` flag enabled.

    ```yaml
    >>> hook:
    >>>   params:
    >>>     param1: {value: 1.1}
    >>>     param2: {value: test}
    >>>   ...
    ```
    """

    _IDENTIFIER = "hook"

    hub_ref: StrictStr = Field(alias="hubRef")
    connection: Optional[StrictStr]
    trigger: Optional[V1Statuses]
    conditions: Optional[StrictStr]
    presets: Optional[List[StrictStr]]
    queue: Optional[StrictStr]
    namespace: Optional[StrictStr]
    params: Optional[Union[Dict[str, V1Param], RefField]]
    disable_defaults: Optional[BoolOrRef] = Field(alias="disableDefaults")

    @validator("trigger")
    def validate_trigger(cls, v):
        if v and v not in V1Statuses.get_allowable_hook_values():
            raise ValueError(
                "Trigger must be one of {}".format(
                    V1Statuses.get_allowable_hook_values()
                )
            )
        return v

    def check_trigger_for_status(self, status: V1Statuses) -> bool:
        if not self.trigger or self.trigger == V1Statuses.DONE:
            return True

        return status == self.trigger
