package service

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

	"git.itzana.me/strafesnet/go-grpc/maps"
	"git.itzana.me/strafesnet/maps-service/pkg/api"
	"git.itzana.me/strafesnet/maps-service/pkg/datastore"
	"git.itzana.me/strafesnet/maps-service/pkg/model"
)

var(
	CreationPhaseMapfixesLimit = 20
	CreationPhaseMapfixStatuses = []model.MapfixStatus{
		model.MapfixStatusChangesRequested,
		model.MapfixStatusSubmitted,
		model.MapfixStatusUnderConstruction,
	}
	// limit mapfixes in the pipeline to one per target map
	ActiveAcceptedMapfixStatuses = []model.MapfixStatus{
		model.MapfixStatusUploading,
		model.MapfixStatusValidated,
		model.MapfixStatusValidating,
		model.MapfixStatusAcceptedUnvalidated,
	}
	// Allow 5 mapfixes every 10 minutes
	CreateMapfixRateLimit int64 = 5
	CreateMapfixRecencyWindow = time.Second*600
)

var (
	ErrCreationPhaseMapfixesLimit = errors.New("Active mapfixes limited to 20")
	ErrActiveMapfixSameTargetAssetID = errors.New("There is an active mapfix with the same TargetAssetID")
	ErrAcceptOwnMapfix = fmt.Errorf("%w: You cannot accept your own mapfix as the submitter", ErrPermissionDenied)
	ErrCreateMapfixRateLimit = errors.New("You must not create more than 5 mapfixes every 10 minutes")
)

// POST /mapfixes
func (svc *Service) CreateMapfix(ctx context.Context, request *api.MapfixTriggerCreate) (*api.OperationID, error) {
	// sanitization
	if request.AssetID<0 || request.TargetAssetID<0{
		return nil, ErrNegativeID
	}
	var ModelID=uint64(request.AssetID);
	var TargetAssetID=uint64(request.TargetAssetID);

	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return nil, ErrUserInfo
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return nil, err
	}

	// Check if user's mapfixes in the creation phase exceeds the limit
	{
		filter := datastore.Optional()
		filter.Add("submitter", int64(userId))
		filter.Add("status_id", CreationPhaseMapfixStatuses)
		creation_mapfixes, err := svc.DB.Mapfixes().List(ctx, filter, model.Page{
			Number: 1,
			Size:   int32(CreationPhaseMapfixesLimit),
		},datastore.ListSortDisabled)
		if err != nil {
			return nil, err
		}

		if CreationPhaseMapfixesLimit <= len(creation_mapfixes) {
			return nil, ErrCreationPhaseMapfixesLimit
		}
	}

	// Check if TargetAssetID actually exists
	{
		_, err := svc.Maps.Get(ctx, &maps.IdMessage{
			ID: request.TargetAssetID,
		})
		if err != nil {
			// TODO: match specific does not exist grpc error
			return nil, err
		}
	}

	// Check if too many operations have been created recently
	{
		count, err := svc.DB.Operations().CountSince(ctx,
			int64(userId),
			time.Now().Add(-CreateMapfixRecencyWindow),
		)
		if err != nil {
			return nil, err
		}

		if CreateMapfixRateLimit < count {
			return nil, ErrCreateMapfixRateLimit
		}
	}

	operation, err := svc.DB.Operations().Create(ctx, model.Operation{
		Owner:         userId,
		StatusID:      model.OperationStatusCreated,
	})
	if err != nil {
		return nil, err
	}

	create_request := model.CreateMapfixRequest{
		OperationID:   operation.ID,
		ModelID:       ModelID,
		TargetAssetID: TargetAssetID,
		Description:   request.Description,
	}

	j, err := json.Marshal(create_request)
	if err != nil {
		return nil, err
	}

	_, err = svc.Nats.Publish("maptest.mapfixes.create", []byte(j))
	if err != nil {
		return nil, err
	}

	return &api.OperationID{
		OperationID: operation.ID,
	}, nil
}

// GetMapfix implements getMapfix operation.
//
// Retrieve map with ID.
//
// GET /mapfixes/{MapfixID}
func (svc *Service) GetMapfix(ctx context.Context, params api.GetMapfixParams) (*api.Mapfix, error) {
	mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
	if err != nil {
		return nil, err
	}
	return &api.Mapfix{
		ID:            mapfix.ID,
		DisplayName:   mapfix.DisplayName,
		Creator:       mapfix.Creator,
		GameID:        int32(mapfix.GameID),
		CreatedAt:     mapfix.CreatedAt.Unix(),
		UpdatedAt:     mapfix.UpdatedAt.Unix(),
		Submitter:     int64(mapfix.Submitter),
		AssetID:       int64(mapfix.AssetID),
		AssetVersion:  int64(mapfix.AssetVersion),
		Completed:     mapfix.Completed,
		TargetAssetID: int64(mapfix.TargetAssetID),
		StatusID:      int32(mapfix.StatusID),
		Description:   mapfix.Description,
	}, nil
}

// ListMapfixes implements listMapfixes operation.
//
// Get list of mapfixes.
//
// GET /mapfixes
func (svc *Service) ListMapfixes(ctx context.Context, params api.ListMapfixesParams) (*api.Mapfixes, error) {
	filter := datastore.Optional()

	if params.DisplayName.IsSet(){
		filter.Add("display_name", params.DisplayName.Value)
	}
	if params.Creator.IsSet(){
		filter.Add("creator", params.Creator.Value)
	}
	if params.GameID.IsSet(){
		filter.Add("game_id", params.GameID.Value)
	}
	if params.Submitter.IsSet(){
		filter.Add("submitter", params.Submitter.Value)
	}
	if params.AssetID.IsSet(){
		filter.Add("asset_id", params.AssetID.Value)
	}
	if params.TargetAssetID.IsSet(){
		filter.Add("target_asset_id", params.TargetAssetID.Value)
	}
	if params.StatusID.IsSet(){
		filter.Add("status_id", params.StatusID.Value)
	}

	sort := datastore.ListSort(params.Sort.Or(int32(datastore.ListSortDisabled)))

	total, items, err := svc.DB.Mapfixes().ListWithTotal(ctx, filter, model.Page{
		Number: params.Page,
		Size:   params.Limit,
	},sort)
	if err != nil {
		return nil, err
	}

	var resp api.Mapfixes
	resp.Total=total
	for _, item := range items {
		resp.Mapfixes = append(resp.Mapfixes, api.Mapfix{
			ID:            item.ID,
			DisplayName:   item.DisplayName,
			Creator:       item.Creator,
			GameID:        int32(item.GameID),
			CreatedAt:     item.CreatedAt.Unix(),
			UpdatedAt:     item.UpdatedAt.Unix(),
			Submitter:     int64(item.Submitter),
			AssetID:       int64(item.AssetID),
			AssetVersion:  int64(item.AssetVersion),
			Completed:     item.Completed,
			TargetAssetID: int64(item.TargetAssetID),
			StatusID:      int32(item.StatusID),
			Description:   item.Description,
		})
	}

	return &resp, nil
}

// PatchMapfixCompleted implements patchMapfixCompleted operation.
//
// Retrieve map with ID.
//
// POST /mapfixes/{MapfixID}/completed
func (svc *Service) SetMapfixCompleted(ctx context.Context, params api.SetMapfixCompletedParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	has_role, err := userInfo.HasRoleMaptest()
	if err != nil {
		return err
	}
	// check if caller has MaptestGame role (request must originate from a maptest roblox game)
	if !has_role {
		return ErrPermissionDeniedNeedRoleMaptest
	}

	pmap := datastore.Optional()
	pmap.Add("completed", true)
	return svc.DB.Mapfixes().Update(ctx, params.MapfixID, pmap)
}

// UpdateMapfixModel implements patchMapfixModel operation.
//
// Update model following role restrictions.
//
// POST /mapfixes/{MapfixID}/model
func (svc *Service) UpdateMapfixModel(ctx context.Context, params api.UpdateMapfixModelParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	// read mapfix (this could be done with a transaction WHERE clause)
	mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
	if err != nil {
		return err
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return err
	}

	// check if caller is the submitter
	has_role := userId == mapfix.Submitter
	if !has_role {
		return ErrPermissionDeniedNotSubmitter
	}

	OldModelID := mapfix.AssetID
	OldModelVersion := mapfix.AssetVersion
	NewModelID := uint64(params.ModelID)
	NewModelVersion := uint64(params.ModelVersion)

	// check if Status is ChangesRequested|Submitted|UnderConstruction
	pmap := datastore.Optional()
	pmap.Add("asset_id", NewModelID)
	pmap.Add("asset_version", NewModelVersion)
	//always reset completed when model changes
	pmap.Add("completed", false)
	err = svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusChangesRequested, model.MapfixStatusSubmitted, model.MapfixStatusUnderConstruction}, pmap)
	if err != nil {
		return err
	}

	event_data := model.AuditEventDataChangeModel{
		OldModelID: OldModelID,
		OldModelVersion: OldModelVersion,
		NewModelID: NewModelID,
		NewModelVersion: NewModelVersion,
	}

	EventData, err := json.Marshal(event_data)
	if err != nil {
		return err
	}

	_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
		ID:           0,
		User:         userId,
		ResourceType: model.ResourceMapfix,
		ResourceID:   params.MapfixID,
		EventType:    model.AuditEventTypeChangeModel,
		EventData:    EventData,
	})
	if err != nil {
		return err
	}

	return nil
}

// ActionMapfixReject invokes actionMapfixReject operation.
//
// Role Reviewer changes status from Submitted -> Rejected.
//
// POST /mapfixes/{MapfixID}/status/reject
func (svc *Service) ActionMapfixReject(ctx context.Context, params api.ActionMapfixRejectParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	has_role, err := userInfo.HasRoleMapfixReview()
	if err != nil {
		return err
	}
	// check if caller has required role
	if !has_role {
		return ErrPermissionDeniedNeedRoleMapfixReview
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return err
	}

	// transaction
	target_status := model.MapfixStatusRejected
	smap := datastore.Optional()
	smap.Add("status_id", target_status)
	err = svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusSubmitted}, smap)
	if err != nil {
		return err
	}

	event_data := model.AuditEventDataAction{
		TargetStatus: uint32(target_status),
	}

	EventData, err := json.Marshal(event_data)
	if err != nil {
		return err
	}

	_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
		ID:           0,
		User:         userId,
		ResourceType: model.ResourceMapfix,
		ResourceID:   params.MapfixID,
		EventType:    model.AuditEventTypeAction,
		EventData:    EventData,
	})
	if err != nil {
		return err
	}

	return nil
}

// ActionMapfixRequestChanges invokes actionMapfixRequestChanges operation.
//
// Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested.
//
// POST /mapfixes/{MapfixID}/status/request-changes
func (svc *Service) ActionMapfixRequestChanges(ctx context.Context, params api.ActionMapfixRequestChangesParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	has_role, err := userInfo.HasRoleMapfixReview()
	if err != nil {
		return err
	}
	// check if caller has required role
	if !has_role {
		return ErrPermissionDeniedNeedRoleMapfixReview
	}

	// transaction
	smap := datastore.Optional()
	smap.Add("status_id", model.MapfixStatusChangesRequested)
	return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusValidated, model.MapfixStatusAcceptedUnvalidated, model.MapfixStatusSubmitted}, smap)
}

// ActionMapfixRevoke invokes actionMapfixRevoke operation.
//
// Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction.
//
// POST /mapfixes/{MapfixID}/status/revoke
func (svc *Service) ActionMapfixRevoke(ctx context.Context, params api.ActionMapfixRevokeParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	// read mapfix (this could be done with a transaction WHERE clause)
	mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
	if err != nil {
		return err
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return err
	}

	// check if caller is the submitter
	has_role := userId == mapfix.Submitter
	if !has_role {
		return ErrPermissionDeniedNotSubmitter
	}

	// transaction
	target_status := model.MapfixStatusUnderConstruction
	smap := datastore.Optional()
	smap.Add("status_id", target_status)
	err = svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusSubmitted, model.MapfixStatusChangesRequested}, smap)
	if err != nil {
		return err
	}

	event_data := model.AuditEventDataAction{
		TargetStatus: uint32(target_status),
	}

	EventData, err := json.Marshal(event_data)
	if err != nil {
		return err
	}

	_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
		ID:           0,
		User:         userId,
		ResourceType: model.ResourceMapfix,
		ResourceID:   params.MapfixID,
		EventType:    model.AuditEventTypeAction,
		EventData:    EventData,
	})
	if err != nil {
		return err
	}

	return nil
}

// ActionMapfixTriggerSubmit invokes actionMapfixTriggerSubmit operation.
//
// Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting.
//
// POST /mapfixes/{MapfixID}/status/trigger-submit
func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.ActionMapfixTriggerSubmitParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	// read mapfix (this could be done with a transaction WHERE clause)
	mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
	if err != nil {
		return err
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return err
	}

	// check if caller is the submitter
	has_role := userId == mapfix.Submitter
	if !has_role {
		return ErrPermissionDeniedNotSubmitter
	}

	// transaction
	target_status := model.MapfixStatusSubmitting
	smap := datastore.Optional()
	smap.Add("status_id", target_status)
	err = svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusUnderConstruction, model.MapfixStatusChangesRequested}, smap)
	if err != nil {
		return err
	}

	validate_request := model.CheckMapfixRequest{
		MapfixID: mapfix.ID,
		ModelID:  mapfix.AssetID,
	}

	j, err := json.Marshal(validate_request)
	if err != nil {
		return err
	}

	_, err = svc.Nats.Publish("maptest.mapfixes.check", []byte(j))
	if err != nil {
		return err
	}

	event_data := model.AuditEventDataAction{
		TargetStatus: uint32(target_status),
	}

	EventData, err := json.Marshal(event_data)
	if err != nil {
		return err
	}

	_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
		ID:           0,
		User:         userId,
		ResourceType: model.ResourceMapfix,
		ResourceID:   params.MapfixID,
		EventType:    model.AuditEventTypeAction,
		EventData:    EventData,
	})
	if err != nil {
		return err
	}

	return nil
}

// ActionMapfixResetSubmitting implements actionMapfixResetSubmitting operation.
//
// Role MapfixReview changes status from Submitting -> UnderConstruction.
//
// POST /mapfixes/{MapfixID}/status/reset-submitting
func (svc *Service) ActionMapfixResetSubmitting(ctx context.Context, params api.ActionMapfixResetSubmittingParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return err
	}

	// check when mapfix was updated
	mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
	if err != nil {
		return err
	}
	if time.Now().Before(mapfix.UpdatedAt.Add(time.Second*10)) {
		// the last time the mapfix was updated must be longer than 10 seconds ago
		return ErrDelayReset
	}

	// check if caller has required role
	has_role := userId == mapfix.Submitter
	if !has_role {
		return ErrPermissionDeniedNotSubmitter
	}

	// transaction
	target_status := model.MapfixStatusUnderConstruction
	smap := datastore.Optional()
	smap.Add("status_id", target_status)
	err = svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusSubmitting}, smap)
	if err != nil {
		return err
	}

	event_data := model.AuditEventDataAction{
		TargetStatus: uint32(target_status),
	}

	EventData, err := json.Marshal(event_data)
	if err != nil {
		return err
	}

	_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
		ID:           0,
		User:         userId,
		ResourceType: model.ResourceMapfix,
		ResourceID:   params.MapfixID,
		EventType:    model.AuditEventTypeAction,
		EventData:    EventData,
	})
	if err != nil {
		return err
	}

	return nil
}

// ActionMapfixTriggerUpload invokes actionMapfixTriggerUpload operation.
//
// Role Admin changes status from Validated -> Uploading.
//
// POST /mapfixes/{MapfixID}/status/trigger-upload
func (svc *Service) ActionMapfixTriggerUpload(ctx context.Context, params api.ActionMapfixTriggerUploadParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	has_role, err := userInfo.HasRoleMapfixUpload()
	if err != nil {
		return err
	}
	// check if caller has required role
	if !has_role {
		return ErrPermissionDeniedNeedRoleMapfixUpload
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return err
	}

	// transaction
	target_status := model.MapfixStatusUploading
	smap := datastore.Optional()
	smap.Add("status_id", target_status)
	mapfix, err := svc.DB.Mapfixes().IfStatusThenUpdateAndGet(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusValidated}, smap)
	if err != nil {
		return err
	}

	// this is a map fix
	upload_fix_request := model.UploadMapfixRequest{
		MapfixID:      mapfix.ID,
		ModelID:       mapfix.ValidatedAssetID,
		ModelVersion:  mapfix.ValidatedAssetVersion,
		TargetAssetID: mapfix.TargetAssetID,
	}

	j, err := json.Marshal(upload_fix_request)
	if err != nil {
		return err
	}

	_, err = svc.Nats.Publish("maptest.mapfixes.upload", []byte(j))
	if err != nil {
		return err
	}

	event_data := model.AuditEventDataAction{
		TargetStatus: uint32(target_status),
	}

	EventData, err := json.Marshal(event_data)
	if err != nil {
		return err
	}

	_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
		ID:           0,
		User:         userId,
		ResourceType: model.ResourceMapfix,
		ResourceID:   params.MapfixID,
		EventType:    model.AuditEventTypeAction,
		EventData:    EventData,
	})
	if err != nil {
		return err
	}

	return nil
}

// ActionMapfixValidate invokes actionMapfixValidate operation.
//
// Role MapfixRelease changes status from Uploading -> Validated.
//
// POST /mapfixes/{MapfixID}/status/reset-uploading
func (svc *Service) ActionMapfixValidated(ctx context.Context, params api.ActionMapfixValidatedParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	has_role, err := userInfo.HasRoleMapfixUpload()
	if err != nil {
		return err
	}
	// check if caller has required role
	if !has_role {
		return ErrPermissionDeniedNeedRoleMapfixUpload
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return err
	}

	// check when mapfix was updated
	mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
	if err != nil {
		return err
	}
	if time.Now().Before(mapfix.UpdatedAt.Add(time.Second*10)) {
		// the last time the mapfix was updated must be longer than 10 seconds ago
		return ErrDelayReset
	}

	// transaction
	target_status := model.MapfixStatusValidated
	smap := datastore.Optional()
	smap.Add("status_id", target_status)
	err = svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusUploading}, smap)
	if err != nil {
		return err
	}

	event_data := model.AuditEventDataAction{
		TargetStatus: uint32(target_status),
	}

	EventData, err := json.Marshal(event_data)
	if err != nil {
		return err
	}

	_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
		ID:           0,
		User:         userId,
		ResourceType: model.ResourceMapfix,
		ResourceID:   params.MapfixID,
		EventType:    model.AuditEventTypeAction,
		EventData:    EventData,
	})
	if err != nil {
		return err
	}

	return nil
}

// ActionMapfixTriggerValidate invokes actionMapfixTriggerValidate operation.
//
// Role Reviewer triggers validation and changes status from Submitted -> Validating.
//
// POST /mapfixes/{MapfixID}/status/trigger-validate
func (svc *Service) ActionMapfixTriggerValidate(ctx context.Context, params api.ActionMapfixTriggerValidateParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	has_role, err := userInfo.HasRoleMapfixReview()
	if err != nil {
		return err
	}
	// check if caller has required role
	if !has_role {
		return ErrPermissionDeniedNeedRoleMapfixReview
	}

	// read mapfix (this could be done with a transaction WHERE clause)
	mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
	if err != nil {
		return err
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return err
	}

	// check if caller is NOT the submitter
	has_role = userId == mapfix.Submitter
	if has_role {
		return ErrAcceptOwnMapfix
	}

	// Check if an active mapfix with the same target asset id exists
	if mapfix.TargetAssetID != 0 {
		filter := datastore.Optional()
		filter.Add("target_asset_id", mapfix.TargetAssetID)
		filter.Add("status_id", ActiveAcceptedMapfixStatuses)
		active_mapfixes, err := svc.DB.Mapfixes().List(ctx, filter, model.Page{
			Number: 1,
			Size:   1,
		},datastore.ListSortDisabled)
		if err != nil {
			return err
		}
		if len(active_mapfixes) != 0{
			return ErrActiveMapfixSameTargetAssetID
		}
	}

	// transaction
	target_status := model.MapfixStatusValidating
	smap := datastore.Optional()
	smap.Add("status_id", target_status)
	mapfix, err = svc.DB.Mapfixes().IfStatusThenUpdateAndGet(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusSubmitted}, smap)
	if err != nil {
		return err
	}

	validate_request := model.ValidateMapfixRequest{
		MapfixID:         mapfix.ID,
		ModelID:          mapfix.AssetID,
		ModelVersion:     mapfix.AssetVersion,
		ValidatedModelID: nil,
	}

	// sentinel values because we're not using rust
	if mapfix.ValidatedAssetID != 0 {
		validate_request.ValidatedModelID = &mapfix.ValidatedAssetID
	}

	j, err := json.Marshal(validate_request)
	if err != nil {
		return err
	}

	_, err = svc.Nats.Publish("maptest.mapfixes.validate", []byte(j))
	if err != nil {
		return err
	}

	event_data := model.AuditEventDataAction{
		TargetStatus: uint32(target_status),
	}

	EventData, err := json.Marshal(event_data)
	if err != nil {
		return err
	}

	_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
		ID:           0,
		User:         userId,
		ResourceType: model.ResourceMapfix,
		ResourceID:   params.MapfixID,
		EventType:    model.AuditEventTypeAction,
		EventData:    EventData,
	})
	if err != nil {
		return err
	}

	return nil
}

// ActionMapfixRetryValidate invokes actionMapfixRetryValidate operation.
//
// Role Reviewer re-runs validation and changes status from Accepted -> Validating.
//
// POST /mapfixes/{MapfixID}/status/retry-validate
func (svc *Service) ActionMapfixRetryValidate(ctx context.Context, params api.ActionMapfixRetryValidateParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	has_role, err := userInfo.HasRoleMapfixReview()
	if err != nil {
		return err
	}
	// check if caller has required role
	if !has_role {
		return ErrPermissionDeniedNeedRoleMapfixReview
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return err
	}

	// transaction
	target_status := model.MapfixStatusValidating
	smap := datastore.Optional()
	smap.Add("status_id", target_status)
	mapfix, err := svc.DB.Mapfixes().IfStatusThenUpdateAndGet(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusAcceptedUnvalidated}, smap)
	if err != nil {
		return err
	}

	validate_request := model.ValidateMapfixRequest{
		MapfixID:     mapfix.ID,
		ModelID:          mapfix.AssetID,
		ModelVersion:     mapfix.AssetVersion,
		ValidatedModelID: nil,
	}

	// sentinel values because we're not using rust
	if mapfix.ValidatedAssetID != 0 {
		validate_request.ValidatedModelID = &mapfix.ValidatedAssetID
	}

	j, err := json.Marshal(validate_request)
	if err != nil {
		return err
	}

	_, err = svc.Nats.Publish("maptest.mapfixes.validate", []byte(j))
	if err != nil {
		return err
	}

	event_data := model.AuditEventDataAction{
		TargetStatus: uint32(target_status),
	}

	EventData, err := json.Marshal(event_data)
	if err != nil {
		return err
	}

	_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
		ID:           0,
		User:         userId,
		ResourceType: model.ResourceMapfix,
		ResourceID:   params.MapfixID,
		EventType:    model.AuditEventTypeAction,
		EventData:    EventData,
	})
	if err != nil {
		return err
	}

	return nil
}

// ActionMapfixAccepted implements actionMapfixAccepted operation.
//
// Role MapfixReview changes status from Validating -> Accepted.
//
// POST /mapfixes/{MapfixID}/status/reset-validating
func (svc *Service) ActionMapfixAccepted(ctx context.Context, params api.ActionMapfixAcceptedParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
	if !ok {
		return ErrUserInfo
	}

	has_role, err := userInfo.HasRoleMapfixReview()
	if err != nil {
		return err
	}
	// check if caller has required role
	if !has_role {
		return ErrPermissionDeniedNeedRoleMapfixReview
	}

	userId, err := userInfo.GetUserID()
	if err != nil {
		return err
	}

	// check when mapfix was updated
	mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
	if err != nil {
		return err
	}
	if time.Now().Before(mapfix.UpdatedAt.Add(time.Second*10)) {
		// the last time the mapfix was updated must be longer than 10 seconds ago
		return ErrDelayReset
	}

	// transaction
	target_status := model.MapfixStatusAcceptedUnvalidated
	smap := datastore.Optional()
	smap.Add("status_id", target_status)
	err = svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusValidating}, smap)
	if err != nil {
		return err
	}

	event_data := model.AuditEventDataAction{
		TargetStatus: uint32(target_status),
	}

	EventData, err := json.Marshal(event_data)
	if err != nil {
		return err
	}

	_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
		ID:           0,
		User:         userId,
		ResourceType: model.ResourceMapfix,
		ResourceID:   params.MapfixID,
		EventType:    model.AuditEventTypeAction,
		EventData:    EventData,
	})
	if err != nil {
		return err
	}

	return nil
}