package stackbuilders

import (
	"context"
	"time"

	portainer "github.com/portainer/portainer/api"
	"github.com/portainer/portainer/api/dataservices"
	"github.com/portainer/portainer/api/stacks/stackutils"
	httperror "github.com/portainer/portainer/pkg/libhttp/error"

	"github.com/rs/zerolog/log"
)

// stackBuildProcess is the common interface shared by all stack build methods.
type stackBuildProcess interface {
	setGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint)
	// prepare handles all pre-save steps: sets type-specific metadata, stores
	// files on disk, or clones the git repository.
	prepare(ctx context.Context, payload *StackPayload, userID portainer.UserID) error
	saveStack() (*portainer.Stack, error)
	deploy(ctx context.Context, endpoint *portainer.Endpoint) error
	// postDeploy runs after a successful deployment: for git builders it enables
	// auto-update; for other builders it is a no-op.
	postDeploy(ctx context.Context, stack *portainer.Stack) error
}

// Build executes the stack build process. It returns the created stack and any
// error encountered during the process. The returned error is of type
// *httperror.HandlerError, which could be an InternalServerError depending on
// the error encountered during the stack build process.
//
// The stack is saved to DB with Status=Deploying and returned immediately.
// Deployment runs in a background goroutine. The caller must poll
// GET /stacks/{id} to track completion.
func Build(ctx context.Context, dataStore dataservices.DataStore, builder stackBuildProcess, payload *StackPayload, endpoint *portainer.Endpoint, userID portainer.UserID) (*portainer.Stack, *httperror.HandlerError) {
	builder.setGeneralInfo(payload, endpoint)

	if err := builder.prepare(ctx, payload, userID); err != nil {
		return nil, httperror.InternalServerError("Failed to prepare stack", err)
	}

	stack, err := builder.saveStack()
	if err != nil {
		return nil, httperror.InternalServerError("Failed to save stack", err)
	}

	go deploy(dataStore, builder, stack.ID, endpoint)

	return stack, nil
}

func deploy(dataStore dataservices.DataStore, builder stackBuildProcess, stackID portainer.StackID, endpoint *portainer.Endpoint) {
	backgroundCtx := context.Background()
	ctx, cancel := context.WithTimeout(backgroundCtx, 15*time.Minute)
	defer cancel()

	deployErr := builder.deploy(ctx, endpoint)

	var stack *portainer.Stack

	if err := dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
		var err error

		stack, err = tx.Stack().Read(stackID)
		if err != nil {
			return err
		}

		stackutils.UpdateStackStatusFromDeploymentResult(stack, deployErr)

		return tx.Stack().Update(stack.ID, stack)
	}); err != nil {
		log.Error().Err(err).
			AnErr("deploy_error", deployErr).
			Int("stack_id", int(stackID)).
			Str("context", "deploy").
			Msg("Failed to update stack status after async deployment")

		return
	}

	if deployErr != nil {
		return
	}

	if err := builder.postDeploy(backgroundCtx, stack); err != nil {
		log.Error().Err(err).
			Int("stack_id", int(stackID)).
			Str("context", "deploy").
			Msg("Failed to run post-deployment hook")
	}
}
