// Copyright 2023 Harness, Inc.
//
// 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 label

import (
	"context"
	"fmt"
	"slices"
	"sort"
	"time"

	"github.com/harness/gitness/errors"
	"github.com/harness/gitness/store"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/types/enum"

	"golang.org/x/exp/maps"
)

type AssignToPullReqOut struct {
	Label         *types.Label
	PullReqLabel  *types.PullReqLabel
	OldLabelValue *types.LabelValue
	NewLabelValue *types.LabelValue
	ActivityType  enum.PullReqLabelActivityType
}

func (s *Service) AssignToPullReq(
	ctx context.Context,
	principalID int64,
	pullreqID int64,
	repoID int64,
	repoParentID int64,
	in *types.PullReqCreateInput,
) (*AssignToPullReqOut,
	error,
) {
	label, err := s.labelStore.FindByID(ctx, in.LabelID)
	if err != nil {
		return nil, fmt.Errorf("failed to find label by id: %w", err)
	}

	if err := s.checkPullreqLabelInScope(ctx, repoParentID, repoID, label); err != nil {
		return nil, err
	}

	oldPullreqLabel, err := s.pullReqLabelAssignmentStore.FindByLabelID(ctx, pullreqID, label.ID)
	if err != nil && !errors.Is(err, store.ErrResourceNotFound) {
		return nil, fmt.Errorf("failed to find label by id: %w", err)
	}

	// if the pullreq label did not have value
	if oldPullreqLabel != nil && oldPullreqLabel.ValueID == nil &&
		// and we don't assign it a new value
		in.Value == "" && in.ValueID == nil {
		return &AssignToPullReqOut{
			Label:         label,
			PullReqLabel:  oldPullreqLabel,
			OldLabelValue: nil,
			NewLabelValue: nil,
			ActivityType:  enum.LabelActivityNoop,
		}, nil
	}

	var oldLabelValue *types.LabelValue
	if oldPullreqLabel != nil && oldPullreqLabel.ValueID != nil {
		oldLabelValue, err = s.labelValueStore.FindByID(ctx, *oldPullreqLabel.ValueID)
		if err != nil {
			return nil, fmt.Errorf("failed to find label value by id: %w", err)
		}
	}

	// if the pullreq label had a value
	if oldLabelValue != nil {
		// and we reassign it the same value
		if in.ValueID != nil && oldLabelValue.ID == *in.ValueID {
			return &AssignToPullReqOut{
				Label:         label,
				PullReqLabel:  oldPullreqLabel,
				OldLabelValue: oldLabelValue,
				NewLabelValue: nil,
				ActivityType:  enum.LabelActivityNoop,
			}, nil
		}
		// and we reassign it the same value
		if in.Value != "" && oldLabelValue.Value == in.Value {
			return &AssignToPullReqOut{
				Label:         label,
				PullReqLabel:  oldPullreqLabel,
				OldLabelValue: oldLabelValue,
				NewLabelValue: nil,
				ActivityType:  enum.LabelActivityNoop,
			}, nil
		}
	}

	var newLabelValue *types.LabelValue
	if in.ValueID != nil {
		newLabelValue, err = s.labelValueStore.FindByID(ctx, *in.ValueID)
		if err != nil {
			return nil, fmt.Errorf("failed to find label value by id: %w", err)
		}
		if label.ID != newLabelValue.LabelID {
			return nil, errors.InvalidArgument("label value is not associated with label")
		}
	}

	newPullreqLabel := newPullReqLabel(pullreqID, principalID, in)
	if in.Value != "" {
		newLabelValue, err = s.getOrDefineValue(ctx, principalID, label, in.Value)
		if err != nil {
			return nil, err
		}
		newPullreqLabel.ValueID = &newLabelValue.ID
	}

	err = s.pullReqLabelAssignmentStore.Assign(ctx, newPullreqLabel)
	if err != nil {
		return nil, fmt.Errorf("failed to assign label to pullreq: %w", err)
	}

	activityType := enum.LabelActivityAssign
	if oldPullreqLabel != nil {
		activityType = enum.LabelActivityReassign
	}

	return &AssignToPullReqOut{
		Label:         label,
		PullReqLabel:  newPullreqLabel,
		OldLabelValue: oldLabelValue,
		NewLabelValue: newLabelValue,
		ActivityType:  activityType,
	}, nil
}

func (s *Service) getOrDefineValue(
	ctx context.Context,
	principalID int64,
	label *types.Label,
	value string,
) (*types.LabelValue, error) {
	if label.Type != enum.LabelTypeDynamic {
		return nil, errors.InvalidArgument("label doesn't allow new value assignment")
	}

	labelValue, err := s.labelValueStore.FindByLabelID(ctx, label.ID, value)
	if err == nil {
		return labelValue, nil
	}
	if !errors.Is(err, store.ErrResourceNotFound) {
		return nil, fmt.Errorf("failed to find label value: %w", err)
	}

	labelValue, err = s.DefineValue(
		ctx,
		principalID,
		label.ID,
		&types.DefineValueInput{
			Value: value,
			Color: label.Color,
		},
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create label value: %w", err)
	}

	return labelValue, nil
}

func (s *Service) UnassignFromPullReq(
	ctx context.Context, repoID, repoParentID, pullreqID, labelID int64,
) (*types.Label, *types.LabelValue, error) {
	label, err := s.labelStore.FindByID(ctx, labelID)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to find label by id: %w", err)
	}

	if err := s.checkPullreqLabelInScope(ctx, repoParentID, repoID, label); err != nil {
		return nil, nil, err
	}

	value, err := s.pullReqLabelAssignmentStore.FindValueByLabelID(ctx, pullreqID, labelID)
	if err != nil && !errors.Is(err, store.ErrResourceNotFound) {
		return nil, nil, fmt.Errorf("failed to find label value: %w", err)
	}

	return label, value, s.pullReqLabelAssignmentStore.Unassign(ctx, pullreqID, labelID)
}

func (s *Service) ListPullReqLabels(
	ctx context.Context,
	repo *types.Repository,
	spaceID int64,
	pullreqID int64,
	filter *types.AssignableLabelFilter,
) (*types.ScopesLabels, int64, error) {
	spaces, err := s.spaceStore.GetAncestors(ctx, spaceID)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to get space hierarchy: %w", err)
	}

	spaceIDs := make([]int64, len(spaces))
	for i, space := range spaces {
		spaceIDs[i] = space.ID
	}

	scopeLabelsMap := make(map[int64]*types.ScopeData)

	pullreqAssignments, err := s.pullReqLabelAssignmentStore.ListAssigned(ctx, pullreqID)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to list labels assigned to pullreq: %w", err)
	}

	if !filter.Assignable {
		sortedAssignments := maps.Values(pullreqAssignments)
		sort.Slice(sortedAssignments, func(i, j int) bool {
			if sortedAssignments[i].Key != sortedAssignments[j].Key {
				return sortedAssignments[i].Key < sortedAssignments[j].Key
			}
			return sortedAssignments[i].Scope < sortedAssignments[j].Scope
		})

		populateScopeLabelsMap(sortedAssignments, scopeLabelsMap, repo, spaces)
		return createScopeLabels(sortedAssignments, scopeLabelsMap), 0, nil
	}

	total, err := s.labelStore.CountInScopes(ctx, repo.ID, spaceIDs, &types.LabelFilter{
		ListQueryFilter: types.ListQueryFilter{
			Query: filter.Query,
		},
	})
	if err != nil {
		return nil, 0, fmt.Errorf("failed to count labels in scopes: %w", err)
	}

	labelInfos, err := s.labelStore.ListInfosInScopes(ctx, repo.ID, spaceIDs, filter)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to list repo and spaces label infos: %w", err)
	}

	labelIDs := make([]int64, len(labelInfos))
	for i, labelInfo := range labelInfos {
		labelIDs[i] = labelInfo.ID
	}

	valueInfos, err := s.labelValueStore.ListInfosByLabelIDs(ctx, labelIDs)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to list label value infos by label ids: %w", err)
	}

	allAssignments := make([]*types.LabelAssignment, len(labelInfos))
	for i, labelInfo := range labelInfos {
		assignment, ok := pullreqAssignments[labelInfo.ID]
		if !ok {
			assignment = &types.LabelAssignment{
				LabelInfo: *labelInfo,
			}
		}
		assignment.LabelInfo.Assigned = &ok
		allAssignments[i] = assignment
		allAssignments[i].Values = valueInfos[labelInfo.ID]
	}

	populateScopeLabelsMap(allAssignments, scopeLabelsMap, repo, spaces)
	return createScopeLabels(allAssignments, scopeLabelsMap), total, nil
}

func (s *Service) Backfill(
	ctx context.Context,
	pullreq *types.PullReq,
) error {
	pullreqAssignments, err := s.pullReqLabelAssignmentStore.ListAssignedByPullreqIDs(
		ctx, []int64{pullreq.ID})
	if err != nil {
		return fmt.Errorf("failed to list labels assigned to pullreq: %w", err)
	}

	pullreq.Labels = pullreqAssignments[pullreq.ID]

	return nil
}

func (s *Service) BackfillMany(
	ctx context.Context,
	pullreqs []*types.PullReq,
) error {
	pullreqIDs := make([]int64, len(pullreqs))
	for i, pr := range pullreqs {
		pullreqIDs[i] = pr.ID
	}

	pullreqAssignments, err := s.pullReqLabelAssignmentStore.ListAssignedByPullreqIDs(
		ctx, pullreqIDs)
	if err != nil {
		return fmt.Errorf("failed to list labels assigned to pullreq: %w", err)
	}

	for _, pullreq := range pullreqs {
		pullreq.Labels = pullreqAssignments[pullreq.ID]
	}

	return nil
}

func populateScopeLabelsMap(
	assignments []*types.LabelAssignment,
	scopeLabelsMap map[int64]*types.ScopeData,
	repo *types.Repository,
	spaces []*types.Space,
) {
	for _, assignment := range assignments {
		_, ok := scopeLabelsMap[assignment.Scope]
		if ok {
			continue
		}
		scopeLabelsMap[assignment.Scope] = &types.ScopeData{Scope: assignment.Scope}
		if assignment.Scope == 0 {
			scopeLabelsMap[assignment.Scope].Repo = repo
		} else {
			for _, space := range spaces {
				if space.ID == *assignment.SpaceID {
					scopeLabelsMap[assignment.Scope].Space = space
				}
			}
		}
	}
}

func createScopeLabels(
	assignments []*types.LabelAssignment,
	scopeLabelsMap map[int64]*types.ScopeData,
) *types.ScopesLabels {
	scopeData := make([]*types.ScopeData, len(scopeLabelsMap))
	for i, scopeLabel := range maps.Values(scopeLabelsMap) {
		scopeData[i] = scopeLabel
	}

	sort.Slice(scopeData, func(i, j int) bool {
		return scopeData[i].Scope < scopeData[j].Scope
	})

	return &types.ScopesLabels{
		LabelData: assignments,
		ScopeData: scopeData,
	}
}

func newPullReqLabel(
	pullreqID int64,
	principalID int64,
	in *types.PullReqCreateInput,
) *types.PullReqLabel {
	now := time.Now().UnixMilli()
	return &types.PullReqLabel{
		PullReqID: pullreqID,
		LabelID:   in.LabelID,
		ValueID:   in.ValueID,
		Created:   now,
		Updated:   now,
		CreatedBy: principalID,
		UpdatedBy: principalID,
	}
}

func (s *Service) checkPullreqLabelInScope(
	ctx context.Context,
	repoParentID, repoID int64,
	label *types.Label,
) error {
	if label.RepoID != nil && *label.RepoID != repoID {
		return errors.InvalidArgument("label %d is not defined in current repo", label.ID)
	}

	if label.SpaceID != nil {
		spaceIDs, err := s.spaceStore.GetAncestorIDs(ctx, repoParentID)
		if err != nil {
			return fmt.Errorf("failed to get parent space ids: %w", err)
		}
		if ok := slices.Contains(spaceIDs, *label.SpaceID); !ok {
			return errors.InvalidArgument("label %d is not defined in current space tree path", label.ID)
		}
	}

	return nil
}
