package workflowruns

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/labstack/echo/v4"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/hatchet-dev/hatchet/api/v1/server/oas/apierrors"
	"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
	contracts "github.com/hatchet-dev/hatchet/internal/services/shared/proto/v1"
	"github.com/hatchet-dev/hatchet/pkg/constants"
	v1 "github.com/hatchet-dev/hatchet/pkg/repository"
	"github.com/hatchet-dev/hatchet/pkg/repository/sqlcv1"
)

func (t *V1WorkflowRunsService) V1WorkflowRunCreate(ctx echo.Context, request gen.V1WorkflowRunCreateRequestObject) (gen.V1WorkflowRunCreateResponseObject, error) {
	tenant := ctx.Get("tenant").(*sqlcv1.Tenant)
	tenantId := tenant.ID

	// make sure input can be marshalled and unmarshalled to input type
	inputBytes, err := json.Marshal(request.Body.Input)

	if err != nil {
		return gen.V1WorkflowRunCreate400JSONResponse(
			apierrors.NewAPIErrors("Invalid input"),
		), nil
	}

	var additionalMetadataBytes []byte

	if request.Body.AdditionalMetadata != nil {

		additionalMetadataBytes, err = json.Marshal(request.Body.AdditionalMetadata)

		if err != nil {
			return gen.V1WorkflowRunCreate400JSONResponse(
				apierrors.NewAPIErrors("Invalid additional metadata"),
			), nil
		}
	}

	var priority *int32
	if request.Body.Priority != nil {
		if *request.Body.Priority < 1 || *request.Body.Priority > 3 {
			return gen.V1WorkflowRunCreate400JSONResponse(
				apierrors.NewAPIErrors(fmt.Sprintf("priority must be between 1 and 3, got %d", *request.Body.Priority)),
			), nil
		}

		newPrio := int32(*request.Body.Priority)
		priority = &newPrio
	}

	grpcReq := &contracts.TriggerWorkflowRunRequest{
		WorkflowName:       request.Body.WorkflowName,
		Input:              inputBytes,
		AdditionalMetadata: additionalMetadataBytes,
		Priority:           priority,
	}

	resp, err := t.proxyTrigger.Do(
		ctx.Request().Context(),
		tenant,
		grpcReq,
	)

	if err != nil {
		if e, ok := status.FromError(err); ok {
			switch e.Code() { // nolint: gocritic
			case codes.InvalidArgument:
				return gen.V1WorkflowRunCreate400JSONResponse(
					apierrors.NewAPIErrors(e.Message()),
				), nil
			case codes.ResourceExhausted:
				return gen.V1WorkflowRunCreate400JSONResponse(
					apierrors.NewAPIErrors(e.Message()),
				), nil
			}
		}

		return nil, err
	}

	// loop for workflow to be created in the OLAP database
	var rawWorkflowRun *v1.V1WorkflowRunPopulator
	retries := 0

	externalId, err := uuid.Parse(resp.ExternalId)

	if err != nil {
		return nil, fmt.Errorf("invalid external id returned from trigger workflow run: %w", err)
	}

	for retries < 10 {
		rawWorkflowRun, err = t.config.V1.OLAP().ReadWorkflowRun(
			ctx.Request().Context(),
			externalId,
		)

		if err != nil && !errors.Is(err, pgx.ErrNoRows) {
			return nil, err
		}

		if err != nil && errors.Is(err, pgx.ErrNoRows) {
			retries++
			time.Sleep(1 * time.Second)
			continue
		}

		break
	}

	if rawWorkflowRun == nil || rawWorkflowRun.WorkflowRun == nil {
		return nil, fmt.Errorf("rawWorkflowRun not populated, we are likely seeing high latency in creating tasks")
	}

	if rawWorkflowRun.WorkflowRun.TenantID != tenantId {
		return nil, fmt.Errorf("tenantId mismatch in the triggered workflow run")
	}

	details, err := t.getWorkflowRunDetails(
		ctx.Request().Context(),
		tenantId,
		rawWorkflowRun,
	)

	if err != nil {
		return nil, err
	}

	if request.Body.AdditionalMetadata != nil {
		correlationIdInterface, ok := (*request.Body.AdditionalMetadata)[string(constants.CorrelationIdKey)]
		if ok {
			correlationId, ok := correlationIdInterface.(string)
			if ok {
				ctx.Set(constants.CorrelationIdKey.String(), correlationId)
			}
		}
	}

	ctx.Set(constants.ResourceIdKey.String(), rawWorkflowRun.WorkflowRun.ExternalID.String())
	ctx.Set(constants.ResourceTypeKey.String(), constants.ResourceTypeWorkflowRun.String())

	// Search for api errors to see how we handle errors in other cases
	return gen.V1WorkflowRunCreate200JSONResponse(
		*details,
	), nil
}
