package service

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

	"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,
	}
	// prevent two mapfixes with same asset id
	ActiveMapfixStatuses = []model.MapfixStatus{
		model.MapfixStatusUploading,
		model.MapfixStatusValidated,
		model.MapfixStatusValidating,
		model.MapfixStatusAccepted,
		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.MapfixStatusAccepted,
	}
)

var (
	ErrCreationPhaseMapfixesLimit = errors.New("Active mapfixes limited to 20")
	ErrActiveMapfixSameAssetID = errors.New("There is an active mapfix with the same AssetID")
	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)
)

// POST /mapfixes
func (svc *Service) CreateMapfix(ctx context.Context, request *api.MapfixCreate) (*api.ID, error) {
	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 an active mapfix 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", ActiveMapfixStatuses)
		active_mapfixes, err := svc.DB.Mapfixes().List(ctx, filter, model.Page{
			Number: 1,
			Size:   1,
		},datastore.ListSortDisabled)
		if err != nil {
			return nil, err
		}
		if len(active_mapfixes) != 0{
			return nil, ErrActiveMapfixSameAssetID
		}
	}

	mapfix, err := svc.DB.Mapfixes().Create(ctx, model.Mapfix{
		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,
		StatusID:      model.MapfixStatusUnderConstruction,
	})
	if err != nil {
		return nil, err
	}
	return &api.ID{
		ID: mapfix.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:        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),
		StatusMessage: mapfix.StatusMessage,
	}, nil
}

// ListMapfixes implements listMapfixes operation.
//
// Get list of mapfixes.
//
// GET /mapfixes
func (svc *Service) ListMapfixes(ctx context.Context, params api.ListMapfixesParams) ([]api.Mapfix, 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 := datastore.ListSort(params.Sort.Or(int32(datastore.ListSortDisabled)))

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

	var resp []api.Mapfix
	for _, item := range items {
		resp = append(resp, api.Mapfix{
			ID:            item.ID,
			DisplayName:   item.DisplayName,
			Creator:       item.Creator,
			GameID:        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),
		})
	}

	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
	}

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

	// 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.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusChangesRequested, model.MapfixStatusSubmitted, model.MapfixStatusUnderConstruction}, pmap)
}

// 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 ErrPermissionDeniedNeedRoleMapReview
	}

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

// 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 ErrPermissionDeniedNeedRoleMapReview
	}

	// transaction
	smap := datastore.Optional()
	smap.Add("status_id", model.MapfixStatusChangesRequested)
	return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusValidated, model.MapfixStatusAccepted, 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
	}

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

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

// ActionMapfixSubmit invokes actionMapfixSubmit operation.
//
// Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitted.
//
// POST /mapfixes/{MapfixID}/status/submit
func (svc *Service) ActionMapfixSubmit(ctx context.Context, params api.ActionMapfixSubmitParams) 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
	}

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

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

// 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 ErrPermissionDeniedNeedRoleMapUpload
	}

	// transaction
	smap := datastore.Optional()
	smap.Add("status_id", model.MapfixStatusUploading)
	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
	}

	svc.Nats.Publish("maptest.mapfixes.uploadfix", []byte(j))

	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 ErrPermissionDeniedNeedRoleMapUpload
	}

	// 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
	smap := datastore.Optional()
	smap.Add("status_id", model.MapfixStatusValidated)
	return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusUploading}, smap)
}

// 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 ErrPermissionDeniedNeedRoleMapReview
	}

	// 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
	}

	has_role, err = userInfo.IsSubmitter(uint64(mapfix.Submitter))
	if err != nil {
		return err
	}
	// check if caller is NOT the 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
	smap := datastore.Optional()
	smap.Add("status_id", model.MapfixStatusValidating)
	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
	}

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

	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 ErrPermissionDeniedNeedRoleMapReview
	}

	// transaction
	smap := datastore.Optional()
	smap.Add("status_id", model.MapfixStatusValidating)
	mapfix, err := svc.DB.Mapfixes().IfStatusThenUpdateAndGet(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusAccepted}, 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
	}

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

	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 ErrPermissionDeniedNeedRoleMapReview
	}

	// 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
	smap := datastore.Optional()
	smap.Add("status_id", model.MapfixStatusAccepted)
	smap.Add("status_message", "Manually forced reset")
	return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusValidating}, smap)
}