// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 compare

import (
	"fmt"
	"sort"
	"strings"

	bundlev1 "github.com/elastic/harp/api/gen/go/harp/bundle/v1"
	"github.com/elastic/harp/pkg/bundle/secret"
	"github.com/elastic/harp/pkg/sdk/security"
)

const (
	// Add describes an operation where the traget path object has been added.
	Add string = "add"
	// Remove describes that entity has been removed.
	Remove string = "remove"
	// Replace describes an operation to replace content of the target path object.
	Replace string = "replace"
)

// OpLog represents operation log calculate from bundle differences.
type OpLog []DiffItem

// DiffItem represents bundle comparison operations.
type DiffItem struct {
	Operation string `json:"op"`
	Type      string `json:"type"`
	Path      string `json:"path"`
	Value     string `json:"value,omitempty"`
}

// -----------------------------------------------------------------------------

// Diff calculates bundle differences.
//nolint:funlen,gocognit,gocyclo // To refactor
func Diff(src, dst *bundlev1.Bundle) ([]DiffItem, error) {
	// Check arguments
	if src == nil {
		return nil, fmt.Errorf("unable to diff with a nil source")
	}
	if dst == nil {
		return nil, fmt.Errorf("unable to diff with a nil destination")
	}

	diffs := []DiffItem{}

	// Index source packages
	srcIndex := map[string]*bundlev1.Package{}
	for _, srcPkg := range src.Packages {
		if srcPkg == nil || srcPkg.Secrets == nil {
			continue
		}

		srcIndex[srcPkg.Name] = srcPkg
	}

	// Index destination packages
	dstIndex := map[string]*bundlev1.Package{}
	for _, dstPkg := range dst.Packages {
		if dstPkg == nil || dstPkg.Secrets == nil {
			continue
		}

		dstIndex[dstPkg.Name] = dstPkg
		if _, ok := srcIndex[dstPkg.Name]; !ok {
			// Package has been added
			diffs = append(diffs, DiffItem{
				Operation: Add,
				Type:      "package",
				Path:      dstPkg.Name,
			})

			// Add keys
			for _, s := range dstPkg.Secrets.Data {
				if s == nil {
					continue
				}

				// Unpack secret value
				var data string
				if err := secret.Unpack(s.Value, &data); err != nil {
					return nil, fmt.Errorf("unable to unpack '%s' - '%s' secret value: %w", dstPkg.Name, s.Key, err)
				}

				diffs = append(diffs, DiffItem{
					Operation: Add,
					Type:      "secret",
					Path:      fmt.Sprintf("%s#%s", dstPkg.Name, s.Key),
					Value:     data,
				})
			}
		}
	}

	// Compute package changes
	for n, sp := range srcIndex {
		dp, ok := dstIndex[n]
		if !ok {
			// Not exist in destination bundle
			diffs = append(diffs, DiffItem{
				Operation: Remove,
				Type:      "package",
				Path:      sp.Name,
			})
			continue
		}

		// Index secret data
		srcSecretIndex := map[string]*bundlev1.KV{}
		for _, ss := range sp.Secrets.Data {
			if ss == nil {
				continue
			}
			srcSecretIndex[ss.Key] = ss
		}
		dstSecretIndex := map[string]*bundlev1.KV{}
		for _, ds := range dp.Secrets.Data {
			if ds == nil {
				continue
			}

			dstSecretIndex[ds.Key] = ds
			oldValue, ok := srcSecretIndex[ds.Key]
			if !ok {
				// Secret has been added
				var data string
				if err := secret.Unpack(ds.Value, &data); err != nil {
					return nil, fmt.Errorf("unable to unpack '%s' - '%s' secret value: %w", dp.Name, ds.Key, err)
				}

				diffs = append(diffs, DiffItem{
					Operation: Add,
					Type:      "secret",
					Path:      fmt.Sprintf("%s#%s", dp.Name, ds.Key),
					Value:     data,
				})
				continue
			}

			// Skip if key does not match
			if !strings.EqualFold(oldValue.Key, ds.Key) {
				continue
			}

			// Compare values
			if !security.SecureCompare(oldValue.Value, ds.Value) {
				// Secret has been replaced
				var data string
				if err := secret.Unpack(ds.Value, &data); err != nil {
					return nil, fmt.Errorf("unable to unpack '%s' - '%s' secret value: %w", dp.Name, ds.Key, err)
				}

				diffs = append(diffs, DiffItem{
					Operation: Replace,
					Type:      "secret",
					Path:      fmt.Sprintf("%s#%s", dp.Name, ds.Key),
					Value:     data,
				})
			}
		}

		// Clean removed source secrets
		for k := range srcSecretIndex {
			if _, ok := dstSecretIndex[k]; !ok {
				diffs = append(diffs, DiffItem{
					Operation: Remove,
					Type:      "secret",
					Path:      fmt.Sprintf("%s#%s", dp.Name, k),
				})
			}
		}
	}

	// Sort diff
	sort.SliceStable(diffs, func(i, j int) bool {
		var (
			x = diffs[i]
			y = diffs[j]
		)

		// Sort by patch descending
		return x.Path < y.Path
	})

	// No error
	return diffs, nil
}
