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

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

	"github.com/harness/gitness/app/store"
	"github.com/harness/gitness/errors"
	gitness_store "github.com/harness/gitness/store"
	"github.com/harness/gitness/store/database/dbtx"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/types/enum"

	"github.com/lucasb-eyer/go-colorful"
	"github.com/rs/zerolog/log"
)

const defaultLabelValueColor = enum.LabelColorGreen

// Label is label migrate.
type Label struct {
	labelStore      store.LabelStore
	labelValueStore store.LabelValueStore
	spaceStore      store.SpaceStore
	tx              dbtx.Transactor
}

func NewLabel(
	labelStore store.LabelStore,
	labelValueStore store.LabelValueStore,
	spaceStore store.SpaceStore,
	tx dbtx.Transactor,
) *Label {
	return &Label{
		labelStore:      labelStore,
		labelValueStore: labelValueStore,
		spaceStore:      spaceStore,
		tx:              tx,
	}
}

//nolint:gocognit
func (migrate Label) Import(
	ctx context.Context,
	migrator types.Principal,
	space *types.Space,
	extLabels []*ExternalLabel,
) ([]*types.Label, error) {
	labels := make([]*types.Label, len(extLabels))
	labelValues := make(map[string][]string)

	spaceIDs, err := migrate.spaceStore.GetAncestorIDs(ctx, space.ID)
	if err != nil {
		return nil, fmt.Errorf("failed to get space ids hierarchy: %w", err)
	}
	scope := int64(len(spaceIDs))
	for i, extLabel := range extLabels {
		label, err := convertLabelWithSanitization(ctx, migrator, space.ID, scope, *extLabel)
		if err != nil {
			return nil, fmt.Errorf("failed to sanitize and convert external label input: %w", err)
		}
		labels[i] = label

		if extLabel.Value != "" {
			valueIn := &types.DefineValueInput{
				Value: extLabel.Value,
				Color: defaultLabelValueColor,
			}
			if err := valueIn.Sanitize(); err != nil {
				return nil, fmt.Errorf("failed to sanitize external label value input: %w", err)
			}
			labelValues[label.Key] = append(labelValues[label.Key], valueIn.Value)
		}
	}

	err = migrate.tx.WithTx(ctx, func(ctx context.Context) error {
		for _, label := range labels {
			err := migrate.defineLabelsAndValues(ctx, migrator.ID, space.ID, label, labelValues[label.Key])
			if err != nil {
				return fmt.Errorf("failed to define labels and/or values: %w", err)
			}
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("failed to define external labels: %w", err)
	}

	return labels, nil
}

func (migrate Label) defineLabelsAndValues(
	ctx context.Context,
	migratorID int64,
	spaceID int64,
	labelIn *types.Label,
	extValues []string) error {
	var label *types.Label
	var err error
	// try to find the label first as it might have been defined already.
	label, err = migrate.labelStore.Find(ctx, &spaceID, nil, labelIn.Key)
	if errors.Is(err, gitness_store.ErrResourceNotFound) {
		err := migrate.labelStore.Define(ctx, labelIn)
		if err != nil {
			return fmt.Errorf("failed to define label: %w", err)
		}
		label = labelIn
	} else if err != nil {
		return fmt.Errorf("failed to find the label: %w", err)
	}

	values, err := migrate.labelValueStore.List(ctx, label.ID, &types.ListQueryFilter{})
	if err != nil {
		return fmt.Errorf("failed to list label values: %w", err)
	}

	now := time.Now().UnixMilli()
	existingValues := make(map[string]bool)
	for _, val := range values {
		existingValues[val.Value] = true
	}

	var newValuesCount int
	for _, val := range extValues {
		if existingValues[val] {
			continue
		}
		// define new label values
		if err := migrate.labelValueStore.Define(ctx, &types.LabelValue{
			LabelID:   label.ID,
			Value:     val,
			Color:     defaultLabelValueColor,
			Created:   now,
			Updated:   now,
			CreatedBy: migratorID,
			UpdatedBy: migratorID,
		}); err != nil {
			return fmt.Errorf("failed to create label value: %w", err)
		}
		newValuesCount++
	}

	_, err = migrate.labelStore.IncrementValueCount(ctx, label.ID, newValuesCount)
	if err != nil {
		return fmt.Errorf("failed to update label value count: %w", err)
	}

	return nil
}

func convertLabelWithSanitization(
	ctx context.Context,
	migrator types.Principal,
	spaceID int64,
	scope int64,
	extLabel ExternalLabel,
) (*types.Label, error) {
	in := &types.DefineLabelInput{
		Key:         extLabel.Name,
		Type:        enum.LabelTypeStatic,
		Description: extLabel.Description,
		Color:       findClosestColor(ctx, extLabel.Color),
	}

	if err := in.Sanitize(); err != nil {
		return nil, fmt.Errorf("failed to sanitize external labels input: %w", err)
	}

	now := time.Now().UnixMilli()
	label := &types.Label{
		SpaceID:     &spaceID,
		RepoID:      nil,
		Scope:       scope,
		Key:         in.Key,
		Color:       in.Color,
		Description: in.Description,
		Type:        in.Type,
		Created:     now,
		Updated:     now,
		CreatedBy:   migrator.ID,
		UpdatedBy:   migrator.ID,
	}

	return label, nil
}

// findClosestColor finds the visually closest color to a provided value using go-colorful library.
func findClosestColor(ctx context.Context, extColor string) enum.LabelColor {
	supportedColors, defColor := enum.GetAllLabelColors()
	if len(extColor) > 1 && string(extColor[0]) != "#" {
		extColor = "#" + extColor
	}
	targetColor, err := colorful.Hex(strings.ToUpper(extColor))
	if err != nil {
		log.Ctx(ctx).Warn().Err(err).Msg("failed to convert the color to hex. choosing default color instead.")
		return defColor
	}
	closestColor := supportedColors[0]
	minDistance := targetColor.DistanceLab(convertToColorful(supportedColors[0]))

	for _, labelColor := range supportedColors[1:] {
		distance := targetColor.DistanceLab(convertToColorful(labelColor))
		if distance < minDistance {
			closestColor = labelColor
			minDistance = distance
		}
	}

	return closestColor
}

// convertToColorful converts Gitness supported label colors to the hex (text value) using web/src/utils:ColorDetails.
func convertToColorful(color enum.LabelColor) colorful.Color {
	var hexColor colorful.Color
	switch color {
	case enum.LabelColorRed:
		hexColor, _ = colorful.Hex("#C7292F")
	case enum.LabelColorGreen:
		hexColor, _ = colorful.Hex("#16794C")
	case enum.LabelColorYellow:
		hexColor, _ = colorful.Hex("#92582D")
	case enum.LabelColorBlue:
		hexColor, _ = colorful.Hex("#236E93")
	case enum.LabelColorPink:
		hexColor, _ = colorful.Hex("#C41B87")
	case enum.LabelColorPurple:
		hexColor, _ = colorful.Hex("#9C2AAD")
	case enum.LabelColorViolet:
		hexColor, _ = colorful.Hex("#5645AF")
	case enum.LabelColorIndigo:
		hexColor, _ = colorful.Hex("#3250B2")
	case enum.LabelColorCyan:
		hexColor, _ = colorful.Hex("#0B7792")
	case enum.LabelColorOrange:
		hexColor, _ = colorful.Hex("#995137")
	case enum.LabelColorBrown:
		hexColor, _ = colorful.Hex("#805C43")
	case enum.LabelColorMint:
		hexColor, _ = colorful.Hex("#247469")
	case enum.LabelColorLime:
		hexColor, _ = colorful.Hex("#586729")
	default:
		// blue is the default color on Gitness
		hexColor, _ = colorful.Hex("#236E93")
	}

	return hexColor
}
