# Copyright 2024 The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.
"""
Base Workflow infrastructure.

See :ref:`explanation-workflows` for a high level explanation of concepts used
here.
"""
from abc import ABCMeta, abstractmethod
from typing import Any, TYPE_CHECKING, TypeVar, cast

from debusine.server.tasks.base import BaseServerTask
from debusine.server.workflows.models import BaseWorkflowData
from debusine.tasks.models import TaskTypes

if TYPE_CHECKING:
    from debusine.db.models import WorkRequest


Model = TypeVar("Model", bound=BaseWorkflowData)


class WorkflowValidationError(Exception):
    """Raised if a workflow fails to validate its inputs."""


class Workflow(BaseServerTask[Model], metaclass=ABCMeta):
    """
    Base class for workflow orchestrators.

    This is the base API for running :class:`WorkflowInstance` logic
    """

    TASK_TYPE = TaskTypes.WORKFLOW

    def __init__(self, work_request: "WorkRequest"):
        """Instantiate a Workflow with its database instance."""
        super().__init__(task_data=work_request.task_data)
        self.set_work_request(work_request)

    @classmethod
    def from_name(cls, name: str) -> type['Workflow[Any]']:
        """Instantiate a workflow by name."""
        res = super().class_from_name(TaskTypes.WORKFLOW, name)
        return cast(type[Workflow[Any]], res)

    @classmethod
    def validate_template_data(cls, data: dict[str, Any]) -> None:
        """Validate WorkflowTemplate data."""
        # By default nothing is enforced

    @classmethod
    def build_workflow_data(
        cls, template_data: dict[str, Any], user_data: dict[str, Any]
    ) -> dict[str, Any]:
        """Merge template data and user-provided data."""
        # Data from the template has priority over user-provided.
        #
        # By default, merging of lists and dictionaries is not supported: only
        # toplevel keys are considered, and if a list or a dict exists both in
        # the user-provided data and in the template, the user-provided version
        # is ignored.
        #
        # Orchestrator subclasses may implement different merging strategies.
        data = user_data.copy()
        data.update(template_data)
        return data

    def validate_input(self) -> None:
        """
        Thorough validation of input data.

        This is run only once at workflow instantiation time, and can do
        slower things like database lookups to validate artifact or collection
        types.
        """
        # Do nothing by default

    @abstractmethod
    def populate(self) -> None:
        """
        Create the initial WorkRequest structure.

        This is called once, when the workflow first becomes runnable.
        validate_input will already have been called.

        This method is required to be idempotent: calling it multiple times
        with the same argument MUST result in the same WorkRequest structure as
        calling it once
        """

    def callback(self, work_request: "WorkRequest") -> None:
        """
        Perform an orchestration step.

        Called with self.work_request when the workflow node becomes ready to
        execute.

        Called with a WorkRequest of type internal/workflow to perform an
        orchestration step triggered by a workflow callback.

        This method is required to be idempotent: calling it multiple times
        with the same argument MUST result in the same WorkRequest structure as
        calling it once
        """
        # Do nothing by default

    def _execute(self) -> bool:
        """Unused abstract method from BaseTask: populate() is used instead."""
        raise NotImplementedError()
