//
// Copyright 2024-2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package service

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"time"

	"github.com/cenkalti/backoff/v4"
	cpAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
	"github.com/chainloop-dev/chainloop/app/controlplane/internal/dispatcher"
	"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext"
	"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware"
	"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
	casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas"
	"github.com/chainloop-dev/chainloop/pkg/attestation"
	"github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
	"github.com/chainloop-dev/chainloop/pkg/credentials"
	errors "github.com/go-kratos/kratos/v2/errors"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/uuid"
)

type AttestationService struct {
	cpAPI.UnimplementedAttestationServiceServer
	*service

	wrUseCase               *biz.WorkflowRunUseCase
	workflowUseCase         *biz.WorkflowUseCase
	workflowContractUseCase *biz.WorkflowContractUseCase
	casUC                   *biz.CASBackendUseCase
	credsReader             credentials.Reader
	integrationUseCase      *biz.IntegrationUseCase
	integrationDispatcher   *dispatcher.FanOutDispatcher
	casCredsUseCase         *biz.CASCredentialsUseCase
	attestationUseCase      *biz.AttestationUseCase
	casMappingUseCase       *biz.CASMappingUseCase
	referrerUseCase         *biz.ReferrerUseCase
	orgUseCase              *biz.OrganizationUseCase
	prometheusUseCase       *biz.PrometheusUseCase
	projectVersionUseCase   *biz.ProjectVersionUseCase
	projectUseCase          *biz.ProjectUseCase
	signingUseCase          *biz.SigningUseCase
}

type NewAttestationServiceOpts struct {
	WorkflowRunUC      *biz.WorkflowRunUseCase
	WorkflowUC         *biz.WorkflowUseCase
	WorkflowContractUC *biz.WorkflowContractUseCase
	OCIUC              *biz.CASBackendUseCase
	CredsReader        credentials.Reader
	IntegrationUseCase *biz.IntegrationUseCase
	CasCredsUseCase    *biz.CASCredentialsUseCase
	AttestationUC      *biz.AttestationUseCase
	FanoutDispatcher   *dispatcher.FanOutDispatcher
	CASMappingUseCase  *biz.CASMappingUseCase
	ReferrerUC         *biz.ReferrerUseCase
	OrgUC              *biz.OrganizationUseCase
	PromUC             *biz.PrometheusUseCase
	ProjectUC          *biz.ProjectUseCase
	ProjectVersionUC   *biz.ProjectVersionUseCase
	SigningUseCase     *biz.SigningUseCase
	Opts               []NewOpt
}

func NewAttestationService(opts *NewAttestationServiceOpts) *AttestationService {
	return &AttestationService{
		service:                 newService(opts.Opts...),
		wrUseCase:               opts.WorkflowRunUC,
		workflowUseCase:         opts.WorkflowUC,
		workflowContractUseCase: opts.WorkflowContractUC,
		casUC:                   opts.OCIUC,
		credsReader:             opts.CredsReader,
		integrationUseCase:      opts.IntegrationUseCase,
		casCredsUseCase:         opts.CasCredsUseCase,
		integrationDispatcher:   opts.FanoutDispatcher,
		attestationUseCase:      opts.AttestationUC,
		casMappingUseCase:       opts.CASMappingUseCase,
		referrerUseCase:         opts.ReferrerUC,
		orgUseCase:              opts.OrgUC,
		prometheusUseCase:       opts.PromUC,
		projectUseCase:          opts.ProjectUC,
		projectVersionUseCase:   opts.ProjectVersionUC,
		signingUseCase:          opts.SigningUseCase,
	}
}

func (s *AttestationService) GetContract(ctx context.Context, req *cpAPI.AttestationServiceGetContractRequest) (*cpAPI.AttestationServiceGetContractResponse, error) {
	robotAccount := usercontext.CurrentRobotAccount(ctx)
	if robotAccount == nil {
		return nil, errors.NotFound("not found", "neither robot account nor API token found")
	}

	if err := checkAuthRequirements(robotAccount, req.GetWorkflowName()); err != nil {
		return nil, err
	}

	wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, req.GetProjectName(), req.GetWorkflowName(), "")
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	// Find contract revision
	contractVersion, err := s.workflowContractUseCase.Describe(ctx, wf.OrgID.String(), wf.ContractID.String(), int(req.ContractRevision), biz.WithoutReferences())
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	} else if contractVersion == nil {
		return nil, errors.NotFound("not found", "contract not found")
	}

	// If contract is private, check we have permissions on the project
	scopedEntity := contractVersion.Contract.ScopedEntity
	if rbacEnabled(ctx) && contractVersion.Contract.IsProjectScoped() && scopedEntity.ID != wf.ProjectID {
		return nil, errors.NotFound("not found", "contract not found")
	}

	resp := &cpAPI.AttestationServiceGetContractResponse_Result{
		Workflow: bizWorkflowToPb(wf),
		Contract: bizWorkFlowContractVersionToPb(contractVersion.Version),
	}

	return &cpAPI.AttestationServiceGetContractResponse{Result: resp}, nil
}

func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationServiceInitRequest) (*cpAPI.AttestationServiceInitResponse, error) {
	robotAccount := usercontext.CurrentRobotAccount(ctx)
	if robotAccount == nil {
		return nil, errors.NotFound("not found", "neither robot account nor API token found")
	}

	org, err := s.orgUseCase.FindByID(ctx, robotAccount.OrgID)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	if err := checkAuthRequirements(robotAccount, req.GetWorkflowName()); err != nil {
		return nil, err
	}

	wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, req.GetProjectName(), req.GetWorkflowName(), "")
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	// Apply RBAC on the project
	if _, err = s.userHasPermissionOnProject(ctx, org.ID, &cpAPI.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowRunCreate); err != nil {
		return nil, err
	}

	// Find contract revision
	contractVersion, err := s.workflowContractUseCase.Describe(ctx, wf.OrgID.String(), wf.ContractID.String(), int(req.ContractRevision), biz.WithoutReferences())
	if err != nil || contractVersion == nil {
		return nil, errors.NotFound("not found", "contract not found")
	}

	// find the default CAS backend to associate the workflow
	backend, err := s.casUC.FindDefaultBackend(context.Background(), robotAccount.OrgID)
	if err != nil && !biz.IsNotFound(err) {
		return nil, fmt.Errorf("failed to find default CAS backend: %w", err)
	} else if err != nil {
		return nil, errors.NotFound("not found", "default CAS backend not found")
	}

	// Check the status of the backend
	if backend.ValidationStatus != biz.CASBackendValidationOK {
		return nil, cpAPI.ErrorCasBackendErrorReasonInvalid("your CAS backend can't be reached")
	}

	// Create workflowRun
	opts := &biz.WorkflowRunCreateOpts{
		WorkflowID:       wf.ID.String(),
		ContractRevision: contractVersion,
		RunnerRunURL:     req.GetJobUrl(),
		RunnerType:       req.GetRunner().String(),
		CASBackendID:     backend.ID,
		ProjectVersion:   req.GetProjectVersion(),
	}

	run, err := s.wrUseCase.Create(ctx, opts)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	wRun := bizWorkFlowRunToPb(run)
	wRun.Workflow = bizWorkflowToPb(wf)
	resp := &cpAPI.AttestationServiceInitResponse_Result{
		WorkflowRun:              wRun,
		Organization:             org.Name,
		BlockOnPolicyViolation:   org.BlockOnPolicyViolation,
		PoliciesAllowedHostnames: org.PoliciesAllowedHostnames,
	}

	resp.SigningOptions = &cpAPI.AttestationServiceInitResponse_SigningOptions{}
	tsa := s.signingUseCase.GetCurrentTSA()
	if tsa != nil {
		resp.SigningOptions.TimestampAuthorityUrl = tsa.URL.String()
	}

	ca := s.signingUseCase.GetSigningCA()
	if ca != nil {
		// Return the name of the configured signing CA
		resp.SigningOptions.SigningCa = ca.GetName()
	}

	return &cpAPI.AttestationServiceInitResponse{Result: resp}, nil
}

func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationServiceStoreRequest) (*cpAPI.AttestationServiceStoreResponse, error) {
	robotAccount := usercontext.CurrentRobotAccount(ctx)
	if robotAccount == nil {
		return nil, errors.NotFound("not found", "robot account not found")
	}

	bundle := req.GetAttestationBundle()
	if bundle == nil {
		bundle = req.GetBundle()
	}

	if req.GetAttestation() == nil && bundle == nil {
		return nil, errors.BadRequest("input required", "DSSE envelope or attestation bundle is required")
	}

	// This will make sure the provided workflowRunID belongs to the org encoded in the robot account
	wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, "", "", req.WorkflowRunId)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	// Apply RBAC on the project
	if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, &cpAPI.IdentityReference{Name: &wf.Project}, authz.PolicyWorkflowRunCreate); err != nil {
		return nil, err
	}

	wRun, err := s.wrUseCase.GetByIDInOrgOrPublic(ctx, robotAccount.OrgID, req.WorkflowRunId)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	} else if wRun == nil {
		return nil, errors.NotFound("not found", "workflow run not found")
	}

	if len(wRun.CASBackends) == 0 {
		return nil, errors.NotFound("not found", "workflow run has no CAS backend")
	}

	digest, err := s.storeAttestation(ctx, req.GetAttestation(), bundle, robotAccount, wf, wRun, req.MarkVersionAsReleased)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	return &cpAPI.AttestationServiceStoreResponse{
		Result: &cpAPI.AttestationServiceStoreResponse_Result{Digest: digest.String()},
	}, nil
}

// Stores and process a DSSE Envelope with a Chainloop attestation
func (s *AttestationService) storeAttestation(ctx context.Context, envelope []byte, bundle []byte, robotAccount *usercontext.RobotAccount, wf *biz.Workflow, wfRun *biz.WorkflowRun, markAsReleased *bool) (*v1.Hash, error) {
	workflowRunID := wfRun.ID.String()
	casBackend := wfRun.CASBackends[0]

	// extract structured envelope for integrations
	dsseEnv, err := attestation.DSSEEnvelopeFromRaw(bundle, envelope)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	// Store the attestation
	digest, err := s.wrUseCase.SaveAttestation(ctx, workflowRunID, envelope, bundle)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	// If we have an external CAS backend, we will push there the attestation
	if !casBackend.Inline {
		// Check the validation status of the backend. The backend might be different from the one configured as default
		if casBackend.ValidationStatus != biz.CASBackendValidationOK {
			// Try to re-validate the backend; if it still fails, return an error
			if err = s.casUC.PerformValidation(ctx, casBackend.ID.String()); err != nil {
				return nil, cpAPI.ErrorCasBackendErrorReasonInvalid("your CAS backend can't be reached")
			}
		}
		go func() {
			b := backoff.NewExponentialBackOff()
			b.MaxElapsedTime = 1 * time.Minute
			err := backoff.Retry(
				func() error {
					rawContent := bundle
					if rawContent == nil {
						rawContent = envelope
					}

					// reset context
					ctx := context.Background()
					var err error
					if err = s.attestationUseCase.UploadAttestationToCAS(ctx, rawContent, casBackend, workflowRunID, *digest); err != nil {
						return err
					}

					s.log.Infow("msg", "attestation uploaded to CAS", "digest", digest, "runID", workflowRunID)
					return nil
				}, b)

			if err != nil {
				_ = handleUseCaseErr(err, s.log)
			}
		}()
	}

	// Store the exploded attestation referrer information in the DB
	if err := s.referrerUseCase.ExtractAndPersist(ctx, dsseEnv, *digest, wf.ID.String()); err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	if !casBackend.Inline {
		// Store the mappings in the DB
		references, err := s.casMappingUseCase.LookupDigestsInAttestation(dsseEnv, *digest)
		if err != nil {
			return nil, handleUseCaseErr(err, s.log)
		}

		for _, ref := range references {
			s.log.Infow("msg", "creating CAS mapping", "name", ref.Name, "digest", ref.Digest, "project", wf.ProjectID.String(), "workflowRun", workflowRunID, "casBackend", casBackend.ID.String())
			if _, err := s.casMappingUseCase.Create(ctx, ref.Digest, casBackend.ID.String(), &biz.CASMappingCreateOpts{
				WorkflowRunID: &wfRun.ID,
				ProjectID:     &wf.ProjectID,
			}); err != nil {
				return nil, handleUseCaseErr(err, s.log)
			}
		}
	}

	secretName := casBackend.SecretName

	// Run integrations dispatcher
	go func() {
		if err := s.integrationDispatcher.Run(context.TODO(), &dispatcher.RunOpts{
			Envelope: dsseEnv, OrgID: robotAccount.OrgID, WorkflowID: wf.ID.String(),
			DownloadBackendType: string(casBackend.Provider),
			DownloadSecretName:  secretName,
			WorkflowRunID:       workflowRunID,
		}); err != nil {
			_ = handleUseCaseErr(err, s.log)
		}
	}()

	// promote release if the workflowRun is successful
	if markAsReleased != nil && *markAsReleased {
		// Update the project version to mark it as a release
		if _, err := s.projectVersionUseCase.UpdateReleaseStatus(ctx, wfRun.ProjectVersion.ID.String(), true); err != nil {
			return nil, handleUseCaseErr(err, s.log)
		}
	}

	if err := s.wrUseCase.MarkAsFinished(ctx, workflowRunID, biz.WorkflowRunSuccess, ""); err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	// Record the attestation in the prometheus registry
	_ = s.prometheusUseCase.ObserveAttestationIfNeeded(ctx, wfRun, biz.WorkflowRunSuccess)

	return digest, nil
}

func (s *AttestationService) Cancel(ctx context.Context, req *cpAPI.AttestationServiceCancelRequest) (*cpAPI.AttestationServiceCancelResponse, error) {
	robotAccount := usercontext.CurrentRobotAccount(ctx)
	if robotAccount == nil {
		return nil, errors.NotFound("not found", "robot account not found")
	}

	// This will make sure the provided workflowRunID belongs to the org encoded in the robot account
	wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, "", "", req.WorkflowRunId)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	// Apply RBAC on the project
	if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, &cpAPI.IdentityReference{Name: &wf.Project}, authz.PolicyWorkflowRunUpdate); err != nil {
		return nil, err
	}

	var status biz.WorkflowRunStatus
	switch req.Trigger {
	case cpAPI.AttestationServiceCancelRequest_TRIGGER_TYPE_FAILURE:
		status = biz.WorkflowRunError
	case cpAPI.AttestationServiceCancelRequest_TRIGGER_TYPE_CANCELLATION:
		status = biz.WorkflowRunCancelled
	default:
		return nil, fmt.Errorf("invalid trigger %s", req.Trigger)
	}

	if err := s.wrUseCase.MarkAsFinished(ctx, req.WorkflowRunId, status, req.Reason); err != nil {
		return nil, err
	}

	wRun, err := s.wrUseCase.GetByIDInOrgOrPublic(ctx, robotAccount.OrgID, req.WorkflowRunId)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	} else if wRun == nil {
		return nil, errors.NotFound("not found", "workflow run not found")
	}

	// Record the attestation in the prometheus registry
	_ = s.prometheusUseCase.ObserveAttestationIfNeeded(ctx, wRun, status)

	return &cpAPI.AttestationServiceCancelResponse{}, nil
}

// There is another endpoint to get credentials via casCredentialsService.Get
// This one is kept since it leverages robot-accounts in the context of a workflow
func (s *AttestationService) GetUploadCreds(ctx context.Context, req *cpAPI.AttestationServiceGetUploadCredsRequest) (*cpAPI.AttestationServiceGetUploadCredsResponse, error) {
	robotAccount := usercontext.CurrentRobotAccount(ctx)
	if robotAccount == nil {
		return nil, errors.NotFound("not found", "robot account not found")
	}

	// Find the CAS backend associated with this workflowRun, that's the one that will be used to upload the materials
	// NOTE: currently we only support one backend per workflowRun but this will change in the future
	// This is the new mode, where the CAS backend ref is stored in the workflow run since initialization
	wRun, err := s.wrUseCase.GetByIDInOrgOrPublic(ctx, robotAccount.OrgID, req.WorkflowRunId)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	} else if wRun == nil {
		return nil, errors.NotFound("not found", "workflow run not found")
	}

	// Apply RBAC on the project
	if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, &cpAPI.IdentityReference{Name: &wRun.Workflow.Project}, authz.PolicyWorkflowRunCreate); err != nil {
		return nil, err
	}

	if len(wRun.CASBackends) == 0 {
		return nil, errors.NotFound("not found", "workflow run has no CAS backend")
	}

	backend := wRun.CASBackends[0]

	// Check the validation status of the backend. The backend might be different from the one configured as default
	if backend.ValidationStatus != biz.CASBackendValidationOK {
		// Try to re-validate the backend; if it still fails, return an error
		// Ccontinue if validation succeeded
		if err = s.casUC.PerformValidation(ctx, backend.ID.String()); err != nil {
			return nil, cpAPI.ErrorCasBackendErrorReasonInvalid("your CAS backend can't be reached")
		}
	}

	s.log.Infow("msg", "generating upload credentials for CAS backend", "ID", wRun.CASBackends[0].ID, "name", wRun.CASBackends[0].Location, "workflowRun", req.WorkflowRunId)

	// Return the backend information and associated credentials (if applicable)
	resp := &cpAPI.AttestationServiceGetUploadCredsResponse_Result{Backend: bizCASBackendToPb(backend)}
	if backend.SecretName != "" {
		ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Uploader, MaxBytes: backend.Limits.MaxBytes}
		t, err := s.casCredsUseCase.GenerateTemporaryCredentials(ref)
		if err != nil {
			return nil, handleUseCaseErr(err, s.log)
		}

		resp.Token = t
	}

	return &cpAPI.AttestationServiceGetUploadCredsResponse{Result: resp}, nil
}

func (s *AttestationService) GetPolicy(ctx context.Context, req *cpAPI.AttestationServiceGetPolicyRequest) (*cpAPI.AttestationServiceGetPolicyResponse, error) {
	token, ok := attjwtmiddleware.FromJWTAuthContext(ctx)
	if !ok {
		return nil, errors.Forbidden("forbidden", "token not found")
	}

	org, err := requireCurrentOrg(ctx)
	if err != nil {
		return nil, errors.Forbidden("forbidden", "organization not found")
	}

	remotePolicy, err := s.workflowContractUseCase.GetPolicy(req.GetProvider(), req.GetPolicyName(), req.GetOrgName(), org.Name, token.Token)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	return &cpAPI.AttestationServiceGetPolicyResponse{Policy: remotePolicy.Policy, Reference: &cpAPI.RemotePolicyReference{
		Url:    remotePolicy.ProviderRef.URL,
		Digest: remotePolicy.ProviderRef.Digest,
	}}, nil
}

func (s *AttestationService) GetPolicyGroup(ctx context.Context, req *cpAPI.AttestationServiceGetPolicyGroupRequest) (*cpAPI.AttestationServiceGetPolicyGroupResponse, error) {
	token, ok := attjwtmiddleware.FromJWTAuthContext(ctx)
	if !ok {
		return nil, errors.Forbidden("forbidden", "token not found")
	}

	org, err := requireCurrentOrg(ctx)
	if err != nil {
		return nil, errors.Forbidden("forbidden", "organization not found")
	}

	remoteGroup, err := s.workflowContractUseCase.GetPolicyGroup(req.GetProvider(), req.GetGroupName(), req.GetOrgName(), org.Name, token.Token)
	if err != nil {
		return nil, handleUseCaseErr(err, s.log)
	}

	return &cpAPI.AttestationServiceGetPolicyGroupResponse{Group: remoteGroup.PolicyGroup, Reference: &cpAPI.RemotePolicyReference{
		Url:    remoteGroup.ProviderRef.URL,
		Digest: remoteGroup.ProviderRef.Digest,
	}}, nil
}

func bizAttestationToPb(att *biz.Attestation) (*cpAPI.AttestationItem, error) {
	if att == nil || att.Envelope == nil {
		return nil, nil
	}

	encodedAttestation, err := json.Marshal(att.Envelope)
	if err != nil {
		return nil, err
	}

	predicate, err := chainloop.ExtractPredicate(att.Envelope)
	if err != nil {
		return nil, fmt.Errorf("error extracting predicate from attestation: %w", err)
	}

	materials, err := extractMaterials(predicate.GetMaterials())
	if err != nil {
		return nil, fmt.Errorf("error extracting materials from attestation: %w", err)
	}

	policyEvaluationStatus := predicate.GetPolicyEvaluationStatus()

	return &cpAPI.AttestationItem{
		Envelope:           encodedAttestation,
		EnvVars:            extractEnvVariables(predicate.GetEnvVars()),
		DigestInCasBackend: att.Digest,
		Materials:          materials,
		Annotations:        predicate.GetAnnotations(),
		PolicyEvaluations:  extractPolicyEvaluations(predicate.GetPolicyEvaluations()),
		PolicyEvaluationStatus: &cpAPI.AttestationItem_PolicyEvaluationStatus{
			Strategy:      string(policyEvaluationStatus.Strategy),
			Bypassed:      policyEvaluationStatus.Bypassed,
			Blocked:       policyEvaluationStatus.Blocked,
			HasViolations: policyEvaluationStatus.HasViolations,
		},
		Bundle: att.Bundle,
	}, nil
}

// extract policy evaluations in form of a Go map of arrays, into a map of protobuf messages
// (needed to be added to the response message)
func extractPolicyEvaluations(in map[string][]*chainloop.PolicyEvaluation) map[string]*cpAPI.PolicyEvaluations {
	res := make(map[string]*cpAPI.PolicyEvaluations)
	for k, v := range in {
		evaluations := make([]*cpAPI.PolicyEvaluation, 0, len(v))
		for _, ev := range v {
			violations := make([]*cpAPI.PolicyViolation, 0, len(ev.Violations))
			for _, vi := range ev.Violations {
				violations = append(violations, &cpAPI.PolicyViolation{
					Subject: vi.Subject,
					Message: vi.Message,
				})
			}

			eval := &cpAPI.PolicyEvaluation{
				Name:         ev.Name,
				MaterialName: ev.MaterialName,
				Body:         ev.Body,
				Sources:      ev.Sources,
				Annotations:  ev.Annotations,
				Description:  ev.Description,
				With:         ev.With,
				Type:         ev.Type,
				Violations:   violations,
				Skipped:      ev.Skipped,
				SkipReasons:  ev.SkipReasons,
				Requirements: ev.Requirements,
			}

			if ev.PolicyReference != nil {
				r := ev.PolicyReference
				orgName, _ := r.GetAnnotations().AsMap()["organization"].(string)
				eval.PolicyReference = &cpAPI.PolicyReference{
					Name:         r.Name,
					Digest:       r.Digest,
					Organization: orgName,
					Uri:          r.Uri,
				}
			}
			if ev.GroupReference != nil {
				orgName, _ := ev.GroupReference.GetAnnotations().AsMap()["organization"].(string)
				eval.GroupReference = &cpAPI.PolicyReference{
					Name:         ev.GroupReference.Name,
					Digest:       ev.GroupReference.Digest,
					Organization: orgName,
					Uri:          ev.GroupReference.Uri,
				}
			}

			evaluations = append(evaluations, eval)
		}

		res[k] = &cpAPI.PolicyEvaluations{
			Evaluations: evaluations,
		}
	}

	return res
}

func extractEnvVariables(in map[string]string) []*cpAPI.AttestationItem_EnvVariable {
	res := make([]*cpAPI.AttestationItem_EnvVariable, 0, len(in))
	for k, v := range in {
		res = append(res, &cpAPI.AttestationItem_EnvVariable{Name: k, Value: v})
	}

	// Sort result
	sort.Slice(res, func(i, j int) bool {
		return res[i].Name < res[j].Name
	})

	return res
}

func extractMaterials(in []*chainloop.NormalizedMaterial) ([]*cpAPI.AttestationItem_Material, error) {
	res := make([]*cpAPI.AttestationItem_Material, 0, len(in))
	for _, m := range in {
		materialItem := &cpAPI.AttestationItem_Material{
			Name:           m.Name,
			Type:           m.Type,
			Filename:       m.Filename,
			Annotations:    m.Annotations,
			Value:          m.Value,
			UploadedToCas:  m.UploadedToCAS,
			EmbeddedInline: m.EmbeddedInline,
			Tag:            m.Tag,
		}

		if m.Hash != nil {
			materialItem.Hash = m.Hash.String()
		}

		res = append(res, materialItem)
	}

	return res, nil
}

func (s *AttestationService) findWorkflowFromTokenOrNameOrRunID(ctx context.Context, orgID string, projectName, workflowName, runID string) (*biz.Workflow, error) {
	if orgID == "" {
		return nil, biz.NewErrValidationStr("orgID must be provided")
	}

	// This is the case when the workflow if found by name
	if workflowName != "" {
		return s.workflowUseCase.FindByNameInOrg(ctx, orgID, projectName, workflowName)
	}

	// This is the case when the workflow is found by its reference to the run
	if runID != "" {
		run, err := s.wrUseCase.GetByIDInOrg(ctx, orgID, runID)
		if err != nil {
			return nil, fmt.Errorf("error retrieving the workflow run: %w", err)
		}

		return run.Workflow, nil
	}

	return nil, biz.NewErrValidationStr("workflowName or workflowRunId must be provided")
}

func checkAuthRequirements(attToken *usercontext.RobotAccount, workflowName string) error {
	if attToken == nil {
		return errors.Forbidden("forbidden", "authentication not found")
	}

	// For API tokens we do not support explicit workflowName. It is inside the token
	if attToken.ProviderKey == attjwtmiddleware.APITokenProviderKey && workflowName == "" {
		return errors.BadRequest("bad request", "when using an API Token, workflow name is required as parameter")
	}

	return nil
}

func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAPI.FindOrCreateWorkflowRequest) (*cpAPI.FindOrCreateWorkflowResponse, error) {
	apiToken := usercontext.CurrentRobotAccount(ctx)
	if apiToken == nil {
		return nil, errors.NotFound("not found", "neither robot account nor API token found")
	}

	// try to load project and apply RBAC if needed
	if _, err := s.userHasPermissionOnProject(ctx, apiToken.OrgID, &cpAPI.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowCreate); err != nil {
		// if the project is not found, check if user can create projects
		if !errors.IsNotFound(err) {
			return nil, err
		}

		if err = s.userCanCreateProject(ctx); err != nil {
			return nil, err
		}
	}

	// we need this token to forward it to the provider service next
	token, err := entities.GetRawToken(ctx)
	if err != nil {
		return nil, err
	}

	// contract validation
	if req.GetContractBytes() != nil {
		if err = s.workflowContractUseCase.ValidateContractPolicies(req.GetContractBytes(), token); err != nil {
			return nil, handleUseCaseErr(err, s.log)
		}
	}

	// Check if the workflow already exists, if it does we might just need to update the contract
	if wf, err := s.workflowUseCase.FindByNameInOrg(ctx, apiToken.OrgID, req.GetProjectName(), req.GetWorkflowName()); err != nil {
		if !biz.IsNotFound(err) {
			return nil, handleUseCaseErr(err, s.log)
		}
	} else if wf != nil {
		// We might need to update the contract
		if req.GetContractBytes() != nil {
			if _, err := s.workflowContractUseCase.Update(ctx, apiToken.OrgID, wf.ContractName, &biz.WorkflowContractUpdateOpts{
				RawSchema: req.GetContractBytes(),
			}); err != nil {
				return nil, handleUseCaseErr(err, s.log)
			}
			// Check if the contract name the user wants to use is the same as the one in the workflow
		} else if req.GetContractName() != "" && req.GetContractName() != wf.ContractName {
			return nil, errors.BadRequest("bad request", "you can't change to another contract during the attestation process, please update the workflow")
		}

		return &cpAPI.FindOrCreateWorkflowResponse{Result: bizWorkflowToPb(wf)}, nil
	}

	// the workflow does not exist, let's create it alongside its project and contract
	createOpts := &biz.WorkflowCreateOpts{
		OrgID:         apiToken.OrgID,
		Name:          req.GetWorkflowName(),
		Project:       req.GetProjectName(),
		ContractName:  req.GetContractName(),
		ContractBytes: req.GetContractBytes(),
	}

	// set project owner if RBAC is enabled
	user := entities.CurrentUser(ctx)
	if user != nil {
		userID, err := uuid.Parse(user.ID)
		if err != nil {
			return nil, handleUseCaseErr(err, s.log)
		}
		createOpts.Owner = &userID
	}

	wf, err := s.workflowUseCase.Create(ctx, createOpts)
	if err != nil {
		return nil, handleUseCaseErr(fmt.Errorf("failed to initialize the attestation: %w", err), s.log)
	}

	// reset RBAC cache, since we might have created a new project
	usercontext.ResetMembershipsCache()

	return &cpAPI.FindOrCreateWorkflowResponse{Result: bizWorkflowToPb(wf)}, nil
}
