// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.

package space

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	apiauth "github.com/harness/gitness/internal/api/auth"
	"github.com/harness/gitness/internal/api/usererror"
	"github.com/harness/gitness/internal/auth"
	"github.com/harness/gitness/internal/paths"
	"github.com/harness/gitness/internal/store"
	"github.com/harness/gitness/internal/store/database/dbtx"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/types/check"
	"github.com/harness/gitness/types/enum"
)

// MoveInput is used for moving a space.
type MoveInput struct {
	UID         *string `json:"uid"`
	ParentID    *int64  `json:"parent_id"`
	KeepAsAlias bool    `json:"keep_as_alias"`
}

func (i *MoveInput) hasChanges(space *types.Space) bool {
	return (i.UID != nil && *i.UID != space.UID) ||
		(i.ParentID != nil && *i.ParentID != space.ParentID)
}

// Move moves a space to a new space and/or name.
//
//nolint:gocognit // refactor if needed
func (c *Controller) Move(ctx context.Context, session *auth.Session,
	spaceRef string, in *MoveInput) (*types.Space, error) {
	space, err := c.spaceStore.FindByRef(ctx, spaceRef)
	if err != nil {
		return nil, err
	}

	permission := enum.PermissionSpaceEdit
	if in.ParentID != nil && *in.ParentID != space.ParentID {
		// ensure user has access to new space (parentId not sanitized!)
		if err = c.checkAuthSpaceCreation(ctx, session, *in.ParentID); err != nil {
			return nil, fmt.Errorf("failed to verify space creation permissions on new parent space: %w", err)
		}

		// TODO: what would be correct permissions on space? (technically we are deleting it from the old space)
		permission = enum.PermissionSpaceDelete
	}
	if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, permission, false); err != nil {
		return nil, err
	}

	if !in.hasChanges(space) {
		return space, nil
	}

	if err = c.sanitizeMoveInput(in, space.ParentID == 0); err != nil {
		return nil, fmt.Errorf("failed to sanitize input: %w", err)
	}

	err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error {
		space, err = c.spaceStore.UpdateOptLock(ctx, space, func(s *types.Space) error {
			if in.UID != nil {
				s.UID = *in.UID
			}
			if in.ParentID != nil {
				s.ParentID = *in.ParentID
			}
			return nil
		})
		if err != nil {
			return fmt.Errorf("failed to update space: %w", err)
		}

		// lock path to ensure it doesn't get updated while we move the space (and its descendants)
		var primaryPath *types.Path
		primaryPath, err = c.pathStore.FindPrimaryWithLock(ctx, enum.PathTargetTypeSpace, space.ID)
		if err != nil {
			return fmt.Errorf("failed to find primary path: %w", err)
		}

		oldPathValue := primaryPath.Value
		newPathValue := space.UID
		if space.ParentID > 0 {
			var parentPath *types.Path
			parentPath, err = c.pathStore.FindPrimary(ctx, enum.PathTargetTypeSpace, space.ParentID)
			if err != nil {
				return fmt.Errorf("failed to find parent space path: %w", err)
			}
			newPathValue = paths.Concatinate(parentPath.Value, space.UID)
		}
		space.Path = newPathValue

		// ensure we don't move space into itself
		if strings.HasPrefix(newPathValue, oldPathValue+types.PathSeparator) {
			return usererror.ErrCyclicHierarchy
		}

		var descendantPaths []*types.Path
		descendantPaths, err = c.pathStore.ListPrimaryDescendantsWithLock(ctx, oldPathValue)
		if err != nil {
			return fmt.Errorf("failed to list all primary descendants: %w", err)
		}

		return c.movePaths(ctx, session.Principal.ID, append(descendantPaths, primaryPath),
			oldPathValue, newPathValue, in.KeepAsAlias)
	})
	if err != nil {
		return nil, err
	}

	return space, nil
}

func (c *Controller) sanitizeMoveInput(in *MoveInput, isRoot bool) error {
	if in.ParentID != nil {
		if *in.ParentID < 0 {
			return errParentIDNegative
		}
		isRoot = *in.ParentID == 0
	}

	if in.UID != nil {
		if err := c.uidCheck(*in.UID, isRoot); err != nil {
			return err
		}
	}

	return nil
}

func (c *Controller) movePaths(ctx context.Context, principalID int64, paths []*types.Path,
	oldPathPrefix string, newPathPrefix string, keepAsAlias bool) error {
	for _, p := range paths {
		oldValue := p.Value
		p.Value = newPathPrefix + p.Value[len(oldPathPrefix):]

		err := check.PathDepth(p.Value, p.TargetType == enum.PathTargetTypeSpace)
		if err != nil {
			return usererror.BadRequestf("resulting path '%s' failed validation: %s", p.Value, err)
		}

		err = c.pathStore.Update(ctx, p)
		if errors.Is(err, store.ErrDuplicate) {
			return usererror.BadRequestf("resulting path '%s' already exists", p.Value)
		}
		if err != nil {
			return fmt.Errorf("failed to update primary path for '%s': %w", oldValue, err)
		}

		if keepAsAlias {
			now := time.Now().UnixMilli()
			err = c.pathStore.Create(ctx, &types.Path{
				Version:    0,
				Value:      oldValue,
				IsPrimary:  false,
				TargetType: p.TargetType,
				TargetID:   p.TargetID,
				CreatedBy:  principalID,
				Created:    now,
				Updated:    now,
			})
			if err != nil {
				return fmt.Errorf("failed to create alias path '%s': %w", oldValue, err)
			}
		}
	}

	return nil
}
