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

import (
	"fmt"
	"reflect"
	"sort"

	"golang.org/x/crypto/blake2b"

	"gitlab.com/NebulousLabs/merkletree"

	bundlev1 "github.com/elastic/harp/api/gen/go/harp/bundle/v1"
	csov1 "github.com/elastic/harp/pkg/cso/v1"
)

// Annotate a bundle object.
func Annotate(obj AnnotationOwner, key, value string) {
	updateStringMap(obj, obj.GetAnnotations(), "Annotations", key, value)
}

// Labelize a bundle object.
func Labelize(obj LabelOwner, key, value string) {
	updateStringMap(obj, obj.GetLabels(), "Labels", key, value)
}

// Tree returns a merkle tree based on secrets hierarchy
func Tree(b *bundlev1.Bundle) (*merkletree.Tree, *Statistic, error) {
	// Check bundle
	if b == nil {
		return nil, nil, fmt.Errorf("unable to process nil bundle")
	}

	// Calculate merkle tree root
	h, err := blake2b.New512(nil)
	if err != nil {
		return nil, nil, fmt.Errorf("unable to initialize hash function for merkle tree")
	}

	// Initialize merkle tree
	tree := merkletree.New(h)
	if err = tree.SetIndex(1); err != nil {
		return nil, nil, fmt.Errorf("unable to initialize merkle tree")
	}

	// Prepare statistics
	stats := &Statistic{
		SecretCount:                  0,
		PackageCount:                 0,
		CSOCompliantPackageNameCount: 0,
	}

	// Ensure packages order
	sort.SliceStable(b.Packages, func(i, j int) bool {
		return b.Packages[i].Name < b.Packages[j].Name
	})

	// All packages
	for _, p := range b.Packages {
		// Increment package count
		stats.PackageCount++

		// Check compliance with CSO
		if errValidate := csov1.Validate(p.Name); errValidate == nil {
			stats.CSOCompliantPackageNameCount++
		}

		// Prepare secret uri list
		uris := []string{}

		// Follow secret chain
		if p.Secrets != nil {
			for _, s := range p.Secrets.Data {
				// Increment secret count
				stats.SecretCount++

				// Build merkle tree leaf
				uris = append(uris, fmt.Sprintf("%s:%d:%s:%x", p.Name, p.Secrets.Version, s.Key, blake2b.Sum512(s.Value)))
			}

			// Sort them
			sort.Strings(uris)

			// Push sorted secret uri as proof
			for _, u := range uris {
				tree.Push([]byte(u))
			}
		}
	}

	// Return the tree
	return tree, stats, nil
}

// Paths returns ordered bundle secret paths.
func Paths(b *bundlev1.Bundle) ([]string, error) {
	// Check input
	if b == nil {
		return nil, fmt.Errorf("unable to process nil bundle")
	}

	// Get all paths
	res := []string{}
	for _, p := range b.Packages {
		res = append(res, p.Name)
	}

	// Sort result
	sort.SliceStable(res, func(i, j int) bool {
		return res[i] < res[j]
	})

	// No error
	return res, nil
}

// SecretReader is used by template engine to resolve secret from secret container.
func SecretReader(b *bundlev1.Bundle) func(path string) (map[string]interface{}, error) {
	return func(secretPath string) (map[string]interface{}, error) {
		return Read(b, secretPath)
	}
}

// -----------------------------------------------------------------------------
func updateStringMap(obj interface{}, m map[string]string, fieldName, key, value string) {
	// Check allocation
	if m == nil {
		m = map[string]string{}
	}

	// Check if map key is already assigned
	if _, ok := m[key]; ok {
		return
	}

	// Assign value
	m[key] = value

	// Reaffect map to owner
	// Really not fan of this ... but protobuf doesn't generate setters for go
	reflect.ValueOf(obj).Elem().FieldByName(fieldName).Set(reflect.ValueOf(m))
}
