package service

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

	"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(
	CreationPhaseSubmissionsLimit = 20
	CreationPhaseSubmissionStatuses = []model.Status{
		model.StatusChangesRequested,
		model.StatusSubmitted,
		model.StatusUnderConstruction,
	}
	ActiveSubmissionStatuses = []model.Status{
		model.StatusUploaded,
		model.StatusUploading,
		model.StatusValidated,
		model.StatusValidating,
		model.StatusAccepted,
		model.StatusChangesRequested,
		model.StatusSubmitted,
		model.StatusUnderConstruction,
	}
)

var (
	ErrCreationPhaseSubmissionsLimit = errors.New("Active submissions limited to 20")
	ErrActiveSubmissionSameAssetID = errors.New("There is an active submission with the same AssetID")
	ErrActiveSubmissionSameTargetAssetID = errors.New("There is an active submission with the same TargetAssetID")
	ErrReleaseInvalidStatus = errors.New("Only submissions with Uploaded status can be released")
	ErrReleaseNoTargetAssetID = errors.New("Only submissions with a TargetAssetID can be released")
)

// POST /submissions
func (svc *Service) CreateSubmission(ctx context.Context, request *api.SubmissionCreate) (*api.ID, error) {
	userInfo, ok := ctx.Value("UserInfo").(UserInfo)
	if !ok {
		return nil, ErrUserInfo
	}

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

	// Check if user's submissions in the creation phase exceeds the limit
	{
		filter := datastore.Optional()
		filter.Add("submitter", int64(userId))
		filter.Add("status_id", CreationPhaseSubmissionStatuses)
		creation_submissions, err := svc.DB.Submissions().List(ctx, filter, model.Page{
			Number: 1,
			Size:   int32(CreationPhaseSubmissionsLimit),
		},model.SubmissionListSortDisabled)
		if err != nil {
			return nil, err
		}

		if CreationPhaseSubmissionsLimit <= len(creation_submissions) {
			return nil, ErrCreationPhaseSubmissionsLimit
		}
	}

	// Check if an active submission with the same asset id exists
	{
		filter := datastore.Optional()
		filter.Add("asset_id", request.AssetID)
		filter.Add("asset_version", request.AssetVersion)
		filter.Add("status_id", ActiveSubmissionStatuses)
		active_submissions, err := svc.DB.Submissions().List(ctx, filter, model.Page{
			Number: 1,
			Size:   1,
		},model.SubmissionListSortDisabled)
		if err != nil {
			return nil, err
		}
		if len(active_submissions) != 0{
			return nil, ErrActiveSubmissionSameAssetID
		}
	}

	// Check if an active submission with the same target asset id exists
	if request.TargetAssetID.IsSet() && request.TargetAssetID.Value != 0{
		filter := datastore.Optional()
		filter.Add("target_asset_id", request.TargetAssetID.Value)
		filter.Add("status_id", ActiveSubmissionStatuses)
		active_submissions, err := svc.DB.Submissions().List(ctx, filter, model.Page{
			Number: 1,
			Size:   1,
		},model.SubmissionListSortDisabled)
		if err != nil {
			return nil, err
		}
		if len(active_submissions) != 0{
			return nil, ErrActiveSubmissionSameTargetAssetID
		}
	}

	submission, err := svc.DB.Submissions().Create(ctx, model.Submission{
		ID:            0,
		DisplayName:   request.DisplayName,
		Creator:       request.Creator,
		GameID:        request.GameID,
		Submitter:     int64(userId),
		AssetID:       request.AssetID,
		AssetVersion:  request.AssetVersion,
		Completed:     false,
		TargetAssetID: request.TargetAssetID.Value,
		StatusID:      model.StatusUnderConstruction,
	})
	if err != nil {
		return nil, err
	}
	return &api.ID{
		ID: submission.ID,
	}, nil
}

// GetSubmission implements getSubmission operation.
//
// Retrieve map with ID.
//
// GET /submissions/{SubmissionID}
func (svc *Service) GetSubmission(ctx context.Context, params api.GetSubmissionParams) (*api.Submission, error) {
	submission, err := svc.DB.Submissions().Get(ctx, params.SubmissionID)
	if err != nil {
		return nil, err
	}
	return &api.Submission{
		ID:            submission.ID,
		DisplayName:   submission.DisplayName,
		Creator:       submission.Creator,
		GameID:        submission.GameID,
		CreatedAt:     submission.CreatedAt.Unix(),
		UpdatedAt:     submission.UpdatedAt.Unix(),
		Submitter:     int64(submission.Submitter),
		AssetID:       int64(submission.AssetID),
		AssetVersion:  int64(submission.AssetVersion),
		Completed:     submission.Completed,
		TargetAssetID: api.NewOptInt64(int64(submission.TargetAssetID)),
		StatusID:      int32(submission.StatusID),
	}, nil
}

// ListSubmissions implements listSubmissions operation.
//
// Get list of submissions.
//
// GET /submissions
func (svc *Service) ListSubmissions(ctx context.Context, params api.ListSubmissionsParams) ([]api.Submission, 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)
	}

	sort := model.SubmissionListSort(params.Sort.Or(int32(model.SubmissionListSortDisabled)))

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

	var resp []api.Submission
	for i := 0; i < len(items); i++ {
		resp = append(resp, api.Submission{
			ID:            items[i].ID,
			DisplayName:   items[i].DisplayName,
			Creator:       items[i].Creator,
			GameID:        items[i].GameID,
			CreatedAt:     items[i].CreatedAt.Unix(),
			UpdatedAt:     items[i].UpdatedAt.Unix(),
			Submitter:     int64(items[i].Submitter),
			AssetID:       int64(items[i].AssetID),
			AssetVersion:  int64(items[i].AssetVersion),
			Completed:     items[i].Completed,
			TargetAssetID: api.NewOptInt64(int64(items[i].TargetAssetID)),
			StatusID:      int32(items[i].StatusID),
		})
	}

	return resp, nil
}

// PatchSubmissionCompleted implements patchSubmissionCompleted operation.
//
// Retrieve map with ID.
//
// POST /submissions/{SubmissionID}/completed
func (svc *Service) SetSubmissionCompleted(ctx context.Context, params api.SetSubmissionCompletedParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfo)
	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 ErrPermissionDenied
	}

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

// UpdateSubmissionModel implements patchSubmissionModel operation.
//
// Update model following role restrictions.
//
// POST /submissions/{SubmissionID}/model
func (svc *Service) UpdateSubmissionModel(ctx context.Context, params api.UpdateSubmissionModelParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfo)
	if !ok {
		return ErrUserInfo
	}

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

	has_role, err := userInfo.IsSubmitter(uint64(submission.Submitter))
	if err != nil {
		return err
	}
	// check if caller is the submitter
	if !has_role {
		return ErrPermissionDenied
	}

	// check if Status is ChangesRequested|Submitted|UnderConstruction
	pmap := datastore.Optional()
	pmap.AddNotNil("asset_id", params.ModelID)
	pmap.AddNotNil("asset_version", params.VersionID)
	//always reset completed when model changes
	pmap.Add("completed", false)
	return svc.DB.Submissions().IfStatusThenUpdate(ctx, params.SubmissionID, []model.Status{model.StatusChangesRequested, model.StatusSubmitted, model.StatusUnderConstruction}, pmap)
}

// ActionSubmissionReject invokes actionSubmissionReject operation.
//
// Role Reviewer changes status from Submitted -> Rejected.
//
// POST /submissions/{SubmissionID}/status/reject
func (svc *Service) ActionSubmissionReject(ctx context.Context, params api.ActionSubmissionRejectParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfo)
	if !ok {
		return ErrUserInfo
	}

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

	// transaction
	smap := datastore.Optional()
	smap.Add("status_id", model.StatusRejected)
	return svc.DB.Submissions().IfStatusThenUpdate(ctx, params.SubmissionID, []model.Status{model.StatusSubmitted}, smap)
}

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

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

	// transaction
	smap := datastore.Optional()
	smap.Add("status_id", model.StatusChangesRequested)
	return svc.DB.Submissions().IfStatusThenUpdate(ctx, params.SubmissionID, []model.Status{model.StatusValidated, model.StatusAccepted, model.StatusSubmitted}, smap)
}

// ActionSubmissionRevoke invokes actionSubmissionRevoke operation.
//
// Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction.
//
// POST /submissions/{SubmissionID}/status/revoke
func (svc *Service) ActionSubmissionRevoke(ctx context.Context, params api.ActionSubmissionRevokeParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfo)
	if !ok {
		return ErrUserInfo
	}

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

	has_role, err := userInfo.IsSubmitter(uint64(submission.Submitter))
	if err != nil {
		return err
	}
	// check if caller is the submitter
	if !has_role {
		return ErrPermissionDenied
	}

	// transaction
	smap := datastore.Optional()
	smap.Add("status_id", model.StatusUnderConstruction)
	return svc.DB.Submissions().IfStatusThenUpdate(ctx, params.SubmissionID, []model.Status{model.StatusSubmitted, model.StatusChangesRequested}, smap)
}

// ActionSubmissionSubmit invokes actionSubmissionSubmit operation.
//
// Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitted.
//
// POST /submissions/{SubmissionID}/status/submit
func (svc *Service) ActionSubmissionSubmit(ctx context.Context, params api.ActionSubmissionSubmitParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfo)
	if !ok {
		return ErrUserInfo
	}

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

	has_role, err := userInfo.IsSubmitter(uint64(submission.Submitter))
	if err != nil {
		return err
	}
	// check if caller is the submitter
	if !has_role {
		return ErrPermissionDenied
	}

	// transaction
	smap := datastore.Optional()
	smap.Add("status_id", model.StatusSubmitted)
	return svc.DB.Submissions().IfStatusThenUpdate(ctx, params.SubmissionID, []model.Status{model.StatusUnderConstruction, model.StatusChangesRequested}, smap)
}

// ActionSubmissionTriggerUpload invokes actionSubmissionTriggerUpload operation.
//
// Role Admin changes status from Validated -> Uploading.
//
// POST /submissions/{SubmissionID}/status/trigger-upload
func (svc *Service) ActionSubmissionTriggerUpload(ctx context.Context, params api.ActionSubmissionTriggerUploadParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfo)
	if !ok {
		return ErrUserInfo
	}

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

	// transaction
	smap := datastore.Optional()
	smap.Add("status_id", model.StatusUploading)
	submission, err := svc.DB.Submissions().IfStatusThenUpdateAndGet(ctx, params.SubmissionID, []model.Status{model.StatusValidated}, smap)
	if err != nil {
		return err
	}

	// sentinel value because we are not using rust
	if submission.TargetAssetID == 0 {
		// this is a new map
		publish_new_request := model.PublishNewRequest{
			SubmissionID: submission.ID,
			ModelID:      submission.AssetID,
			ModelVersion: submission.AssetVersion,
			// publish as displayname, whatever
			ModelName:    submission.DisplayName,
		}

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

		svc.Nats.Publish("maptest.submissions.publishnew", []byte(j))
	} else {
		// this is a map fix
		publish_fix_request := model.PublishFixRequest{
			SubmissionID:  submission.ID,
			ModelID:       submission.AssetID,
			ModelVersion:  submission.AssetVersion,
			TargetAssetID: submission.TargetAssetID,
		}

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

		svc.Nats.Publish("maptest.submissions.publishfix", []byte(j))
	}

	return nil
}

// ActionSubmissionTriggerValidate invokes actionSubmissionTriggerValidate operation.
//
// Role Reviewer triggers validation and changes status from Submitted|Accepted -> Validating.
//
// POST /submissions/{SubmissionID}/status/trigger-validate
func (svc *Service) ActionSubmissionTriggerValidate(ctx context.Context, params api.ActionSubmissionTriggerValidateParams) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfo)
	if !ok {
		return ErrUserInfo
	}

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

	// transaction
	smap := datastore.Optional()
	smap.Add("status_id", model.StatusValidating)
	submission, err := svc.DB.Submissions().IfStatusThenUpdateAndGet(ctx, params.SubmissionID, []model.Status{model.StatusSubmitted, model.StatusAccepted}, smap)
	if err != nil {
		return err
	}

	validate_request := model.ValidateRequest{
		SubmissionID:     submission.ID,
		ModelID:          submission.AssetID,
		ModelVersion:     submission.AssetVersion,
		ValidatedModelID: 0, //TODO: reuse velidation models
	}

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

	svc.Nats.Publish("maptest.submissions.validate", []byte(j))

	return nil
}

// ReleaseSubmissions invokes releaseSubmissions operation.
//
// Release a set of uploaded maps.
//
// POST /release-submissions
func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.ReleaseInfo) error {
	userInfo, ok := ctx.Value("UserInfo").(UserInfo)
	if !ok {
		return ErrUserInfo
	}

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

	idList := make([]int64, len(request))
	for i, releaseInfo := range request {
		idList[i] = releaseInfo.SubmissionID
	}

	// fetch submissions
	submissions, err := svc.DB.Submissions().GetList(ctx, idList)
	if err != nil {
		return err
	}

	// check each submission to make sure it is ready to release
	for _,submission := range submissions{
		if submission.StatusID != model.StatusUploaded{
			return ErrReleaseInvalidStatus
		}
		if submission.TargetAssetID == 0{
			return ErrReleaseNoTargetAssetID
		}
	}

	for i,submission := range submissions{
		date := request[i].Date.Unix()
		// create each map with go-grpc
		_, err := svc.Client.Create(ctx, &maps.MapRequest{
			ID:          submission.TargetAssetID,
			DisplayName: &submission.DisplayName,
			Creator:     &submission.Creator,
			GameID:      &submission.GameID,
			Date:        &date,
		})
		if err != nil {
			return err
		}

		// update each status to Released
		smap := datastore.Optional()
		smap.Add("status_id", model.StatusReleased)
		err = svc.DB.Submissions().IfStatusThenUpdate(ctx, submission.ID, []model.Status{model.StatusUploaded}, smap)
		if err != nil {
			return err
		}
	}

	return nil
}