// Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
// Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
// Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors
// may be used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package workflow

import (
	"database/sql"
	"errors"
	"fmt"
	"github.com/google/uuid"
	"go.temporal.io/sdk/temporal"
	"go.temporal.io/sdk/workflow"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/wrapperspb"
	"path/filepath"
	"peridot.resf.org/apollo/rpmutils"
	"peridot.resf.org/peridot/db/models"
	peridotpb "peridot.resf.org/peridot/pb"
	yumrepofspb "peridot.resf.org/peridot/yumrepofs/pb"
	"peridot.resf.org/utils"
	"strings"
	"time"
)

type FutureContext struct {
	Future    workflow.Future
	Ctx       workflow.Context
	TaskQueue string
	Task      *models.Task
}

type sideEffectBuild struct {
	Task  *models.Task
	Build *models.Build
}

type channelArchBuild struct {
	err          error
	newArtifacts []*peridotpb.TaskArtifact
}

func getSrpmBuildArches(project *models.Project, metadata *anypb.Any) ([]string, error) {
	// add all project arches first
	arches := project.Archs

	// let's check srpm for incompatible arches / noarch
	rpmArtifactMetadata := &peridotpb.RpmArtifactMetadata{}
	err := metadata.UnmarshalTo(rpmArtifactMetadata)
	if err != nil {
		return nil, err
	}
	if len(rpmArtifactMetadata.BuildArch) > 0 {
		arches = rpmArtifactMetadata.BuildArch
	}
	// if exclusive to specific arches, then only build for those
	if len(rpmArtifactMetadata.ExclusiveArch) > 0 {
		var newArches []string
		for _, arch := range arches {
			if utils.StrContains(arch, rpmArtifactMetadata.ExclusiveArch) {
				newArches = append(newArches, arch)
			}
		}
		arches = newArches
	}
	if len(rpmArtifactMetadata.ExcludeArch) > 0 {
		var newArches []string
		for _, arch := range arches {
			if !utils.StrContains(arch, rpmArtifactMetadata.ExcludeArch) {
				newArches = append(newArches, arch)
			}
		}
		arches = newArches
	}
	// if noarch isn't excluded and is in build arches or exclusive arches, then add it
	// the kernel package for example has a separate build process for noarch
	if !utils.StrContains("noarch", rpmArtifactMetadata.ExcludeArch) && (utils.StrContains("noarch", rpmArtifactMetadata.BuildArch) || utils.StrContains("noarch", rpmArtifactMetadata.ExclusiveArch)) {
		arches = append(arches, "noarch")
	}

	// verify that we're not adding any arches that are not in the project
	var finalArches []string
	for _, arch := range arches {
		if arch == "noarch" || utils.StrContains(arch, project.Archs) {
			// double check that we're not adding duplicate arches
			if !utils.StrContains(arch, finalArches) {
				finalArches = append(finalArches, arch)
			}
		}
	}

	if len(finalArches) == 0 {
		return nil, errors.New("no arches found for project")
	}
	arches = finalArches

	return arches, nil
}

// BuildBatchWorkflow is a workflow that builds a batch of packages (executes BuildWorkflow as a child workflow)
func (c *Controller) BuildBatchWorkflow(ctx workflow.Context, req *peridotpb.SubmitBuildBatchRequest, newBuildReqs []string, newModuleBuildReqs []string, buildBatchId string, user *utils.ContextUser) error {
	var futures []FutureContext

	for _, buildPkg := range newBuildReqs {
		buildReq := &peridotpb.SubmitBuildRequest{
			ProjectId: req.ProjectId,
			Package: &peridotpb.SubmitBuildRequest_PackageName{
				PackageName: wrapperspb.String(buildPkg),
			},
		}
		triggerCtx := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
			TaskQueue:           c.mainQueue,
			WorkflowTaskTimeout: 3 * time.Hour,
		})
		futures = append(futures, FutureContext{
			Ctx:       triggerCtx,
			Future:    workflow.ExecuteChildWorkflow(triggerCtx, c.TriggerBuildFromBatchWorkflow, buildReq, buildBatchId, false, user),
			TaskQueue: c.mainQueue,
		})
	}
	for _, buildPkg := range newModuleBuildReqs {
		buildReq := &peridotpb.SubmitBuildRequest{
			ProjectId: req.ProjectId,
			Package: &peridotpb.SubmitBuildRequest_PackageName{
				PackageName: wrapperspb.String(buildPkg),
			},
		}
		triggerCtx := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
			TaskQueue:           c.mainQueue,
			WorkflowTaskTimeout: 3 * time.Hour,
		})
		futures = append(futures, FutureContext{
			Ctx:       triggerCtx,
			Future:    workflow.ExecuteChildWorkflow(triggerCtx, c.TriggerBuildFromBatchWorkflow, buildReq, buildBatchId, true, user),
			TaskQueue: c.mainQueue,
		})
	}

	// Build failures doesn't mean a batch trigger has failed
	// A batch can contain failed builds
	for _, future := range futures {
		_ = future.Future.Get(future.Ctx, nil)
	}

	return nil
}

// TriggerBuildFromBatchWorkflow is a sub-workflow to create a task and trigger a build
func (c *Controller) TriggerBuildFromBatchWorkflow(ctx workflow.Context, req *peridotpb.SubmitBuildRequest, buildBatchId string, module bool, user *utils.ContextUser) error {
	var sideEffect sideEffectBuild
	sideEffectCall := workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} {
		projects, err := c.db.ListProjects(&peridotpb.ProjectFilters{
			Id: wrapperspb.String(req.ProjectId),
		})
		if err != nil {
			return nil
		}
		if len(projects) != 1 {
			return nil
		}
		project := projects[0]

		filters := &peridotpb.PackageFilters{}
		switch p := req.Package.(type) {
		case *peridotpb.SubmitBuildRequest_PackageId:
			filters.Id = p.PackageId
		case *peridotpb.SubmitBuildRequest_PackageName:
			filters.NameExact = p.PackageName
		}

		pkgs, err := c.db.GetPackagesInProject(filters, req.ProjectId, 0, 1)
		if err != nil {
			return nil
		}
		if len(pkgs) != 1 {
			return nil
		}
		pkg := pkgs[0]

		var importRevision *models.ImportRevision

		if req.ScmHash == nil {
			// Each import can have multiple revisions
			importRevisions, err := c.db.GetLatestImportRevisionsForPackageInProject(pkg.Name, req.ProjectId)
			if err != nil {
				return nil
			}

			// Construct the branch this project is targeting
			// Using the constructed branch, we will check if there is a revision for the branch
			// In fork mode, if the project is configured correctly the upstream will have a revision for the branch
			upstreamBranch := fmt.Sprintf("%s%d%s", project.TargetBranchPrefix, project.MajorVersion, project.BranchSuffix.String)
			for _, revision := range importRevisions {
				if revision.ScmBranchName == upstreamBranch {
					importRevision = &*&revision
					break
				}
			}
		} else {
			importRevision, err = c.db.GetImportRevisionByScmHash(req.ScmHash.Value)
			if err != nil {
				if err == sql.ErrNoRows {
					activityErr := fmt.Errorf("import revision not found for scm hash %s", req.ScmHash.Value)
					c.log.Errorf("%s", activityErr.Error())
					return nil
				}
			}
		}
		if importRevision == nil {
			return nil
		}

		beginTx, err := c.db.Begin()
		if err != nil {
			c.log.Errorf("could not start transaction: %v", err)
			return nil
		}
		tx := c.db.UseTransaction(beginTx)

		task, err := tx.CreateTask(user, "noarch", peridotpb.TaskType_TASK_TYPE_BUILD, &req.ProjectId, nil)
		if err != nil {
			c.log.Errorf("could not create build task in TriggerBuildFromBatchWorkflow: %v", err)
			return nil
		}

		metadataAnyPb, err := anypb.New(&peridotpb.PackageOperationMetadata{
			PackageName: pkg.Name,
		})
		if err != nil {
			return nil
		}
		err = tx.SetTaskMetadata(task.ID.String(), metadataAnyPb)
		if err != nil {
			c.log.Errorf("could not set task metadata in TriggerBuildFromBatchWorkflow: %v", err)
			return nil
		}

		var build *models.Build
		if module {
			err = beginTx.Commit()
			if err != nil {
				c.log.Errorf("could not commit transaction: %v", err)
				return nil
			}
		} else {
			build, err = tx.CreateBuild(pkg.ID.String(), importRevision.PackageVersionId, task.ID.String(), req.ProjectId)
			if err != nil {
				c.log.Errorf("could not create build in TriggerBuildFromBatchWorkflow: %v", err)
				return nil
			}

			err = tx.AttachBuildToBatch(build.ID.String(), buildBatchId)
			if err != nil {
				c.log.Errorf("could not attach build to batch: %v", err)
				return nil
			}

			err = beginTx.Commit()
			if err != nil {
				c.log.Errorf("could not commit transaction: %v", err)
				return nil
			}
		}

		return &sideEffectBuild{
			Task:  task,
			Build: build,
		}
	})
	err := sideEffectCall.Get(&sideEffect)
	if err != nil {
		return err
	}
	if sideEffect.Task == nil {
		return fmt.Errorf("could not trigger build")
	}

	buildCtx := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
		WorkflowID:          sideEffect.Task.ID.String(),
		TaskQueue:           c.mainQueue,
		WorkflowTaskTimeout: 3 * time.Hour,
		RetryPolicy: &temporal.RetryPolicy{
			MaximumAttempts: 1,
		},
	})

	if module {
		return workflow.ExecuteChildWorkflow(buildCtx, c.BuildModuleWorkflow, req, sideEffect.Task, &peridotpb.ExtraBuildOptions{
			BuildBatchId: buildBatchId,
		}).Get(buildCtx, nil)
	}

	return workflow.ExecuteChildWorkflow(buildCtx, c.BuildWorkflow, req, sideEffect.Task, &peridotpb.ExtraBuildOptions{
		BuildBatchId:    buildBatchId,
		ReusableBuildId: sideEffect.Build.ID.String(),
	}).Get(buildCtx, nil)
}

// BuildWorkflow initiates a new artifact build, which can currently be a standard package and a module.
// Modules trigger build activities for each component
// todo(mustafa): More info
func (c *Controller) BuildWorkflow(ctx workflow.Context, req *peridotpb.SubmitBuildRequest, task *models.Task, extraOptions *peridotpb.ExtraBuildOptions) (*peridotpb.SubmitBuildTask, error) {
	submitBuildTask := peridotpb.SubmitBuildTask{
		ChecksDisabled: req.DisableChecks,
		ParentTaskId:   utils.NullStringValueP(task.ParentTaskId),
	}

	deferTask, errorDetails, err := c.commonCreateTask(task, nil)
	defer deferTask()
	if err != nil {
		return nil, err
	}

	filters := &peridotpb.PackageFilters{}
	switch p := req.Package.(type) {
	case *peridotpb.SubmitBuildRequest_PackageId:
		filters.Id = p.PackageId
	case *peridotpb.SubmitBuildRequest_PackageName:
		filters.NameExact = p.PackageName
	}

	pkgs, err := c.db.GetPackagesInProject(filters, req.ProjectId, 0, 1)
	if err != nil {
		setInternalError(errorDetails, err)
		return nil, err
	}
	if len(pkgs) != 1 {
		setPackageNotFoundError(errorDetails, req.ProjectId, ErrorDomainBuildsPeridot)
		return nil, utils.CouldNotRetrieveObjects
	}
	pkg := pkgs[0]

	projects, err := c.db.ListProjects(&peridotpb.ProjectFilters{
		Id: wrapperspb.String(req.ProjectId),
	})
	if err != nil {
		setInternalError(errorDetails, err)
		return nil, err
	}
	if len(projects) != 1 {
		setInternalError(errorDetails, errors.New("project could not be found"))
		return nil, utils.CouldNotRetrieveObjects
	}
	project := projects[0]

	var importRevision *models.ImportRevision

	if req.ScmHash == nil {
		// Each import can have multiple revisions
		importRevisions, err := c.db.GetLatestImportRevisionsForPackageInProject(pkg.Name, req.ProjectId)
		if err != nil {
			setInternalError(errorDetails, err)
			return nil, err
		}

		// Construct the branch this project is targeting
		// Using the constructed branch, we will check if there is a revision for the branch
		// In fork mode, if the project is configured correctly the upstream will have a revision for the branch
		upstreamBranch := fmt.Sprintf("%s%d%s", project.TargetBranchPrefix, project.MajorVersion, project.BranchSuffix.String)
		for _, revision := range importRevisions {
			if revision.ScmBranchName == upstreamBranch {
				importRevision = &*&revision
				break
			}
		}
	} else {
		importRevision, err = c.db.GetImportRevisionByScmHash(req.ScmHash.Value)
		if err != nil {
			if err == sql.ErrNoRows {
				activityErr := fmt.Errorf("import revision not found for scm hash %s", req.ScmHash.Value)
				setActivityError(errorDetails, activityErr)
				return nil, activityErr
			}
		}
	}
	if importRevision == nil {
		setActivityError(errorDetails, errors.New("could not find upstream branch"))
		return nil, errors.New("could not find upstream branch")
	}
	packageVersion, err := c.db.GetPackageVersion(importRevision.PackageVersionId)
	if err != nil {
		err = fmt.Errorf("could not get package version for import revision %s: %v", importRevision.ID, err)
		setInternalError(errorDetails, err)
		return nil, err
	}

	// Create a side repo if the build request specifies side NVRs
	// Packages specified here will be excluded from the main repo
	if len(req.SideNvrs) > 0 {
		var buildNvrs []*models.Build
		var excludes []string
		for _, sideNvr := range req.SideNvrs {
			if !rpmutils.NVRNoArch().MatchString(sideNvr) {
				err = fmt.Errorf("invalid side NVR: %s", sideNvr)
				setActivityError(errorDetails, err)
				return nil, err
			}
			nvrMatch := rpmutils.NVRNoArch().FindStringSubmatch(sideNvr)
			nvrName := nvrMatch[1]
			nvrVersion := nvrMatch[2]
			nvrRelease := nvrMatch[3]

			buildNvr, err := c.db.GetBuildByPackageNameAndVersionAndRelease(nvrName, nvrVersion, nvrRelease, req.ProjectId)
			if err != nil {
				if err == sql.ErrNoRows {
					err = fmt.Errorf("side NVR %s not found in project %s", sideNvr, req.ProjectId)
					setActivityError(errorDetails, err)
					return nil, err
				}
				err = fmt.Errorf("could not get build for side NVR %s: %v", sideNvr, err)
				setInternalError(errorDetails, err)
				return nil, err
			}

			artifacts, err := c.db.GetArtifactsForBuild(buildNvr.ID.String())
			if err != nil {
				err = fmt.Errorf("could not get artifacts for build %s: %v", buildNvr.ID, err)
				setInternalError(errorDetails, err)
				return nil, err
			}

			for _, artifact := range artifacts {
				artifactName := strings.TrimPrefix(artifact.Name, fmt.Sprintf("%s/", buildNvr.TaskId))
				if rpmutils.NVR().MatchString(artifactName) {
					excludes = append(excludes, rpmutils.NVR().FindStringSubmatch(artifactName)[1])
				}
			}

			buildNvrs = append(buildNvrs, buildNvr)
		}

		repo, err := c.db.CreateRepositoryWithPackages(uuid.New().String(), req.ProjectId, true, []string{})
		if err != nil {
			c.log.Errorf("failed to create repository: %v", err)
			return nil, status.Error(codes.Internal, "failed to create repository for side nvrs")
		}

		if extraOptions == nil {
			extraOptions = &peridotpb.ExtraBuildOptions{}
		}
		if extraOptions.ExtraYumrepofsRepos == nil {
			extraOptions.ExtraYumrepofsRepos = []*peridotpb.ExtraYumrepofsRepo{}
		}
		extraOptions.ExtraYumrepofsRepos = append(extraOptions.ExtraYumrepofsRepos, &peridotpb.ExtraYumrepofsRepo{
			Name:           repo.Name,
			ModuleHotfixes: true,
			IgnoreExclude:  true,
		})
		extraOptions.ExcludePackages = excludes

		for _, buildNvr := range buildNvrs {
			yumrepoCtx := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
				TaskQueue: "yumrepofs",
			})
			updateRepoRequest := &UpdateRepoRequest{
				ProjectID:        req.ProjectId,
				TaskID:           &buildNvr.TaskId,
				BuildIDs:         []string{buildNvr.ID.String()},
				Delete:           false,
				ForceRepoId:      repo.ID.String(),
				ForceNonModular:  true,
				DisableSigning:   true,
				DisableSetActive: true,
			}
			updateRepoTask := &yumrepofspb.UpdateRepoTask{}
			err = workflow.ExecuteChildWorkflow(yumrepoCtx, c.RepoUpdaterWorkflow, updateRepoRequest).Get(ctx, updateRepoTask)
			if err != nil {
				return nil, err
			}
		}
	}

	// If there is a parent task, we need to use that ID as the parent task ID
	taskID := task.ID.String()
	if task.ParentTaskId.Valid {
		taskID = task.ParentTaskId.String
	}
	buildID := extraOptions.ReusableBuildId
	if buildID == "" {
		err = errors.New("reusable build id not found")
		setActivityError(errorDetails, err)
		return nil, err
	}

	projectId := project.ID.String()
	var srpmTask models.Task
	srpmTaskEffect := workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} {
		newTask, err := c.db.CreateTask(nil, "noarch", peridotpb.TaskType_TASK_TYPE_BUILD_SRPM, &projectId, &taskID)
		if err != nil {
			return &models.Task{}
		}
		return newTask
	})
	err = srpmTaskEffect.Get(&srpmTask)
	if err != nil {
		setInternalError(errorDetails, fmt.Errorf("could not get srpm task effect: %v", err))
		return nil, err
	}
	if !srpmTask.ProjectId.Valid {
		setInternalError(errorDetails, fmt.Errorf("could not create srpm task"))
		return nil, err
	}

	// Provision a new worker specifically to build the SRPM
	// SRPMs can be built with any architecture
	srpmTaskQueue, cleanupSrpm, err := c.provisionWorker(ctx, &ProvisionWorkerRequest{
		TaskId:       srpmTask.ID.String(),
		ParentTaskId: sql.NullString{String: taskID, Valid: true},
		Purpose:      "srpm",
		Arch:         "noarch",
		ProjectId:    req.ProjectId,
		Privileged:   true,
	})
	if err != nil {
		setInternalError(errorDetails, err)
		return nil, err
	}
	defer cleanupSrpm()

	srpmCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
		ScheduleToStartTimeout: 30 * time.Minute,
		StartToCloseTimeout:    time.Hour,
		HeartbeatTimeout:       2 * time.Minute,
		TaskQueue:              srpmTaskQueue,
		RetryPolicy: &temporal.RetryPolicy{
			MaximumAttempts: 1,
		},
	})
	upstreamPrefix := fmt.Sprintf("%s/%s", project.TargetGitlabHost, project.TargetPrefix)
	err = workflow.ExecuteActivity(srpmCtx, c.BuildSRPMActivity, upstreamPrefix, importRevision.ScmHash, project.ID.String(), pkg.Name, packageVersion, srpmTask, extraOptions).Get(srpmCtx, nil)
	if err != nil {
		setActivityError(errorDetails, err)
		return nil, err
	}

	uploadSrpmCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
		ScheduleToStartTimeout: 30 * time.Minute,
		StartToCloseTimeout:    time.Hour,
		HeartbeatTimeout:       time.Minute,
		TaskQueue:              srpmTaskQueue,
		RetryPolicy: &temporal.RetryPolicy{
			MaximumAttempts: 3,
		},
	})
	var uploadSRPMResult UploadActivityResult
	err = workflow.ExecuteActivity(uploadSrpmCtx, c.UploadSRPMActivity, project.ID.String(), taskID).Get(uploadSrpmCtx, &uploadSRPMResult)
	if err != nil {
		setActivityError(errorDetails, err)
		return nil, err
	}
	cleanupSrpm()

	// todo(mustafa): Currently we do not support partial arch builds
	// Partial arch builds are when you introduce new architectures to a project
	// and be able to build them without affecting the existing architectures.
	// That is an important feature for Peridot, but not strictly necessary as of now.
	nvr := strings.TrimSuffix(filepath.Base(uploadSRPMResult.ObjectName), ".rpm")

	firstNvrCheckSideEffectCall := workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} {
		exists, err := c.db.NVRAExists(nvr)
		if err != nil {
			err = fmt.Errorf("could not check if NVR exists: %v", err)
			return err
		}

		return exists
	})
	var res interface{}
	err = firstNvrCheckSideEffectCall.Get(&res)
	if err != nil {
		setInternalError(errorDetails, err)
		return nil, err
	}
	if err, ok := res.(error); ok {
		setActivityError(errorDetails, err)
		return nil, err
	}
	if exists, ok := res.(bool); ok && exists {
		err = fmt.Errorf("NVR %s already locked", nvr)
		task.Status = peridotpb.TaskStatus_TASK_STATUS_CANCELED
		_ = c.logToMon([]string{err.Error()}, task.ID.String(), taskID)
		setActivityError(errorDetails, err)
		return nil, err
	}

	subtask, err := c.db.GetTask(uploadSRPMResult.Subtask.ID.String(), utils.Pointer(project.ID.String()))
	if err != nil {
		setInternalError(errorDetails, err)
		return nil, err
	}
	metadata := &anypb.Any{}
	err = protojson.Unmarshal(subtask[0].Metadata.JSONText, metadata)
	if err != nil {
		setInternalError(errorDetails, err)
		return nil, err
	}
	arches, err := getSrpmBuildArches(&project, metadata)
	if err != nil {
		setActivityError(errorDetails, err)
		return nil, err
	}

	// Invoke build for all archs
	var artifacts []*peridotpb.TaskArtifact
	archChannel := workflow.NewChannel(ctx)
	for _, archTop := range arches {
		archGoCtx := workflow.WithValue(ctx, "arch", archTop)
		workflow.Go(archGoCtx, func(ctx workflow.Context) {
			arch := ctx.Value("arch").(string)
			ret := &channelArchBuild{}
			defer archChannel.Send(ctx, ret)

			var archTask models.Task
			archTaskEffect := workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} {
				newTask, err := c.db.CreateTask(nil, arch, peridotpb.TaskType_TASK_TYPE_BUILD_ARCH, &projectId, &taskID)
				if err != nil {
					return &models.Task{}
				}
				return newTask
			})
			err := archTaskEffect.Get(&archTask)
			if err != nil || !archTask.ProjectId.Valid {
				ret.err = fmt.Errorf("failed to create arch task: %s", err)
				return
			}

			workerReq := &ProvisionWorkerRequest{
				TaskId:       archTask.ID.String(),
				ParentTaskId: sql.NullString{String: taskID, Valid: true},
				Purpose:      "b-" + arch,
				Arch:         arch,
				ProjectId:    req.ProjectId,
				HighResource: true,
				Privileged:   true,
			}
			archTaskQueue, cleanupArch, err := c.provisionWorker(ctx, workerReq)
			if err != nil {
				ret.err = fmt.Errorf("failed to provision worker: %s", err)
				return
			}
			defer cleanupArch()

			archCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
				ScheduleToStartTimeout: 12 * time.Hour,
				StartToCloseTimeout:    48 * time.Hour,
				HeartbeatTimeout:       2 * time.Hour,
				TaskQueue:              archTaskQueue,
				RetryPolicy: &temporal.RetryPolicy{
					MaximumAttempts: 1,
				},
			})
			err = workflow.ExecuteActivity(archCtx, c.BuildArchActivity, project.ID.String(), pkg.Name, req.DisableChecks, packageVersion, uploadSRPMResult, archTask, arch, extraOptions).Get(archCtx, nil)
			if err != nil {
				ret.err = fmt.Errorf("failed to build arch %s: %s", arch, err)
				return
			}

			uploadArchCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
				ScheduleToStartTimeout: 12 * time.Hour,
				StartToCloseTimeout:    24 * time.Hour,
				HeartbeatTimeout:       2 * time.Minute,
				TaskQueue:              archTaskQueue,
			})

			var res []*UploadActivityResult
			err = workflow.ExecuteActivity(uploadArchCtx, c.UploadArchActivity, project.ID.String(), taskID).Get(ctx, &res)
			if err != nil {
				ret.err = fmt.Errorf("failed to upload arch %s: %s", arch, err)
				return
			}

			exists, err := c.db.NVRAExists(strings.TrimSuffix(filepath.Base(uploadSRPMResult.ObjectName), ".rpm"))
			if err != nil {
				ret.err = err
				return
			}

			// Only refresh SRPM if we don't already have it in the repo tree
			// This is to enable us to rebuild archs we don't have while keeping
			// the integrity of the already published repository
			if !exists {
				res = append(res, &uploadSRPMResult)
			}

			var newArtifacts []*peridotpb.TaskArtifact
			for _, result := range res {
				err = c.db.AttachTaskToBuild(buildID, result.Subtask.ID.String())
				if err != nil {
					ret.err = fmt.Errorf("failed to attach task to build: %s", err)
					return
				}
				if result.Skip {
					continue
				}
				subtask, err := c.db.GetTask(result.Subtask.ID.String(), utils.Pointer(project.ID.String()))
				if err != nil {
					ret.err = fmt.Errorf("failed to get task: %s", err)
					return
				}
				metadata := &anypb.Any{}
				err = protojson.Unmarshal(subtask[0].Metadata.JSONText, metadata)
				if err != nil {
					ret.err = fmt.Errorf("failed to unmarshal metadata: %s", err)
					return
				}
				ret.newArtifacts = append(newArtifacts, &peridotpb.TaskArtifact{
					TaskId:     result.Subtask.ID.String(),
					Name:       result.ObjectName,
					HashSha256: result.HashSha256,
					Arch:       result.Arch,
					Metadata:   metadata,
				})
			}
		})
	}

	var artifactNames []string
	archWaitChannel := workflow.NewChannel(ctx)
	for i := 0; i < len(arches); i++ {
		workflow.Go(ctx, func(ctx workflow.Context) {
			var res channelArchBuild
			archChannel.Receive(ctx, &res)

			if res.err != nil {
				archWaitChannel.Send(ctx, res.err)
				return
			}

			for _, artifact := range res.newArtifacts {
				if utils.StrContains(artifact.Name, artifactNames) {
					continue
				}
				artifactNames = append(artifactNames, artifact.Name)
			}

			archWaitChannel.Send(ctx, nil)
		})
	}

	var archErrs []error
	for i := 0; i < len(arches); i++ {
		var res interface{}
		archWaitChannel.Receive(ctx, &res)
		if err, ok := res.(error); ok {
			if err != nil {
				archErrs = append(archErrs, err)
			}
		}
	}
	isError := false
	isCanceled := false
	var retErr error
	for _, archErr := range archErrs {
		if archErr != nil {
			if !strings.Contains(archErr.Error(), "canceled") {
				isError = true
				retErr = archErr
			}
			if !isError && strings.Contains(archErr.Error(), "canceled") {
				isCanceled = true
				retErr = archErr
			}
		}
	}
	if retErr != nil {
		setActivityError(errorDetails, retErr)
		if isCanceled {
			task.Status = peridotpb.TaskStatus_TASK_STATUS_CANCELED
		} else {
			task.Status = peridotpb.TaskStatus_TASK_STATUS_FAILED
		}
		_ = c.db.SetTaskStatus(task.ID.String(), task.Status)

		return nil, retErr
	}

	submitBuildTask = peridotpb.SubmitBuildTask{
		BuildId:        buildID,
		BuildTaskId:    taskID,
		PackageName:    pkg.Name,
		ImportRevision: importRevision.ToProto(),
		Artifacts:      artifacts,
		ChecksDisabled: req.DisableChecks,
		Modular:        req.ModuleVariant,
		ParentTaskId:   utils.NullStringValueP(task.ParentTaskId),
		RepoChanges:    &yumrepofspb.UpdateRepoTask{Changes: []*yumrepofspb.RepositoryChange{}},
	}
	sbtAny, err := anypb.New(&submitBuildTask)
	if err != nil {
		setInternalError(errorDetails, err)
		return nil, err
	}
	err = c.db.SetTaskResponse(task.ID.String(), sbtAny)
	if err != nil {
		setInternalError(errorDetails, err)
		return nil, err
	}
	submitBuildTask.Artifacts = nil

	task.Status = peridotpb.TaskStatus_TASK_STATUS_RUNNING

	// Save once here so RepoUpdaterWorkflow can use it
	deferTask()

	task.Status = peridotpb.TaskStatus_TASK_STATUS_FAILED

	if !extraOptions.DisableYumrepofsUpdates && !req.SetInactive {
		yumrepoCtx := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
			TaskQueue: "yumrepofs",
		})
		updateRepoRequest := &UpdateRepoRequest{
			ProjectID: req.ProjectId,
			BuildIDs:  []string{buildID},
			Delete:    false,
			TaskID:    &taskID,
		}
		updateRepoTask := &yumrepofspb.UpdateRepoTask{}
		err = workflow.ExecuteChildWorkflow(yumrepoCtx, c.RepoUpdaterWorkflow, updateRepoRequest).Get(yumrepoCtx, updateRepoTask)
		if err != nil {
			setActivityError(errorDetails, err)
			return nil, err
		}

		submitBuildTask.RepoChanges = updateRepoTask
	}

	// Lock NVR only once
	effectCallNVRA := workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} {
		err = c.db.LockNVRA(nvr)
		if err != nil {
			// retry once again, stupid error
			err = c.db.LockNVRA(nvr)
			if err != nil {
				err = fmt.Errorf("failed to lock nvr: %s", err)
				setInternalError(errorDetails, err)
				return err
			}
		}

		return nil
	})
	var resNVRA interface{}
	err = effectCallNVRA.Get(&resNVRA)
	if err != nil {
		setInternalError(errorDetails, err)
		return nil, err
	}
	if err, ok := resNVRA.(error); ok {
		setActivityError(errorDetails, err)
		return nil, err
	}

	task.Status = peridotpb.TaskStatus_TASK_STATUS_SUCCEEDED

	return &submitBuildTask, nil
}
