# 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.

"""Pydantic models used by debusine workflows."""

from datetime import datetime
from enum import StrEnum
from typing import Any

try:
    import pydantic.v1 as pydantic
except ImportError:
    import pydantic as pydantic  # type: ignore

from debusine.server.tasks.models import PackageUploadTarget
from debusine.tasks.models import (
    ActionRetryWithDelays,
    AutopkgtestFailOn,
    AutopkgtestNeedsInternet,
    AutopkgtestTimeout,
    BackendType,
    BaseDynamicTaskData,
    BaseTaskData,
    LookupMultiple,
    LookupSingle,
    SbuildBinNMU,
    SbuildInput,
    empty_lookup_multiple,
)


class BaseWorkflowData(BaseTaskData):
    """
    Base class for workflow data.

    Workflow data is encoded as JSON in the database, and it is modeled as a
    pydantic data structure in memory for both ease of access and validation.
    """


class WorkRequestManualUnblockAction(StrEnum):
    """An action taken on a review of a manual unblock request."""

    ACCEPT = "accept"
    REJECT = "reject"


class WorkRequestManualUnblockLog(pydantic.BaseModel):
    """A log entry for a review of a manual unblock request."""

    user_id: int
    timestamp: datetime
    notes: str | None = None
    action: WorkRequestManualUnblockAction | None = None


class WorkRequestManualUnblockData(pydantic.BaseModel):
    """Data for a manual unblock request."""

    log: list[WorkRequestManualUnblockLog] = pydantic.Field(
        default_factory=list
    )


class WorkRequestWorkflowData(pydantic.BaseModel):
    """Data structure for WorkRequest.workflow_data."""

    class Config:
        """Set up stricter pydantic Config."""

        validate_assignment = True
        extra = pydantic.Extra.forbid

    #: If the work request fails, if True the workflow can continue, if false
    #: it is interrupted
    allow_failure: bool = pydantic.Field(default=False, allow_mutation=False)

    #: name of the step in the visual representation of the workflow
    display_name: str | None = pydantic.Field(
        default=None, allow_mutation=False
    )

    #: internal identifier used to differentiate multiple workflow callbacks
    #: inside a single workflow. It allows the orchestrator to encode the plan
    #: about what it is supposed to do at this point in the workflow.
    step: str | None = pydantic.Field(default=None, allow_mutation=False)

    #: Name of the group within this workflow containing this work request.
    group: str | None = pydantic.Field(default=None, allow_mutation=False)

    manual_unblock: WorkRequestManualUnblockData | None = None

    retry_count: int = pydantic.Field(default=0)


class SbuildWorkflowData(BaseWorkflowData):
    """Sbuild workflow data."""

    input: SbuildInput
    target_distribution: str
    # If AUTO is used, default to BackendType.UNSHARE
    backend: BackendType = BackendType.AUTO
    architectures: list[str] = pydantic.Field(
        min_items=1,
        unique_items=True,
    )
    environment_variant: str | None = None
    binnmu: SbuildBinNMU | None = None
    build_profiles: list[str] | None = None
    build_logs_collection: LookupSingle | None = None
    retry_delays: list[str] | None = pydantic.Field(default=None, min_items=1)

    @pydantic.validator("retry_delays")
    @classmethod
    def validate_retry_delays(
        cls, values: list[str] | None
    ) -> list[str] | None:
        """Check items in `retry_delays` field."""
        for v in values or []:
            if ActionRetryWithDelays._delay_re.match(v) is None:
                raise ValueError(
                    f"Item in retry_delays must be an integer followed by "
                    f"m, h, d, or w; got {v!r}"
                )
        return values


class SbuildWorkflowDynamicData(BaseDynamicTaskData):
    """Dynamic data for the Sbuild workflow."""

    input_source_artifact_id: int
    build_logs_collection_id: int | None = None


class UpdateEnvironmentsWorkflowTarget(BaseWorkflowData):
    """A target for an update_environments workflow."""

    codenames: str | list[str]
    codename_aliases: dict[str, list[str]] = pydantic.Field(
        default_factory=dict
    )
    variants: str | list[str] = pydantic.Field(default_factory=list)
    backends: str | list[str] = pydantic.Field(default_factory=list)
    architectures: list[str] = pydantic.Field(min_items=1, unique_items=True)
    mmdebstrap_template: dict[str, Any] | None = None
    simplesystemimagebuild_template: dict[str, Any] | None = None


class UpdateEnvironmentsWorkflowData(BaseWorkflowData):
    """update_environments workflow data."""

    vendor: str
    targets: list[UpdateEnvironmentsWorkflowTarget] = pydantic.Field(
        min_items=1
    )


class PackageUploadWorkflowData(BaseWorkflowData):
    """`package_upload` workflow data."""

    prefix: str = ""

    source_artifact: LookupSingle | None
    binary_artifacts: LookupMultiple = pydantic.Field(
        default_factory=empty_lookup_multiple
    )
    merge_uploads: bool = False
    since_version: str | None = None
    target_distribution: str | None = None
    key: LookupSingle | None = None
    require_signature: bool = True
    target: PackageUploadTarget
    vendor: str | None = None
    codename: str | None = None


class PackageUploadWorkflowDynamicData(BaseDynamicTaskData):
    """Dynamic data for the `package_upload` workflow."""

    source_artifact_id: int | None
    binary_artifacts_ids: list[int] = pydantic.Field(default_factory=list)


class AutopkgtestWorkflowData(BaseWorkflowData):
    """`autopkgtest` workflow data."""

    prefix: str = ""

    source_artifact: LookupSingle
    binary_artifacts: LookupMultiple
    context_artifacts: LookupMultiple = pydantic.Field(
        default_factory=empty_lookup_multiple
    )

    vendor: str
    codename: str
    backend: BackendType = BackendType.AUTO
    architectures: list[str] = pydantic.Field(default_factory=list)

    include_tests: list[str] = pydantic.Field(default_factory=list)
    exclude_tests: list[str] = pydantic.Field(default_factory=list)
    debug_level: int = pydantic.Field(default=0, ge=0, le=3)
    extra_environment: dict[str, str] = pydantic.Field(default_factory=dict)
    needs_internet: AutopkgtestNeedsInternet = AutopkgtestNeedsInternet.RUN
    fail_on: AutopkgtestFailOn = pydantic.Field(
        default_factory=AutopkgtestFailOn
    )
    timeout: AutopkgtestTimeout | None


class AutopkgtestWorkflowDynamicData(BaseDynamicTaskData):
    """Dynamic data for the `autopkgtest` workflow."""

    source_artifact_id: int
    binary_artifacts_ids: list[int]
    context_artifacts_ids: list[int] = pydantic.Field(default_factory=list)
