// 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 (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"

	"google.golang.org/protobuf/encoding/protojson"

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

// FromDump creates a bundle from a JSON Dump.
func FromDump(r io.Reader) (*bundlev1.Bundle, error) {
	// Check parameters
	if types.IsNil(r) {
		return nil, fmt.Errorf("unable to process nil reader")
	}

	// Drain input content
	content, err := io.ReadAll(r)
	if err != nil {
		return nil, fmt.Errorf("unable to read input content: %w", err)
	}

	// Build the container from json
	var b bundlev1.Bundle
	if err = protojson.Unmarshal(content, &b); err != nil {
		return nil, fmt.Errorf("unable to decode JSON bundle: %w", err)
	}

	// Convert secret values to current value packing method.
	for _, p := range b.Packages {
		for _, s := range p.Secrets.Data {
			// Decode json encoded value
			var data interface{}
			if errJSON := json.Unmarshal(s.Value, &data); errJSON != nil {
				return nil, fmt.Errorf("unable to decode '%s' - '%s' secret value as json: %w", p.Name, s.Key, errJSON)
			}

			// Pack secret value
			payload, err := secret.Pack(data)
			if err != nil {
				return nil, fmt.Errorf("unable to pack '%s' - '%s' secret value: %w", p.Name, s.Key, err)
			}

			// Replace current json encoded secret value by packed one.
			s.Value = payload
		}
	}

	// No error
	return &b, nil
}

// FromOpLog convert oplog to a bundle.
func FromOpLog(oplog compare.OpLog) (*bundlev1.Bundle, error) {
	// Create an empty bundle.
	b := &bundlev1.Bundle{}

	packageMap := map[string]*bundlev1.Package{}

	// Generate patch rules
	for _, op := range oplog {
		switch op.Type {
		case "package":
			// Ignore package operation
			continue
		case "secret":
			pathParts := strings.SplitN(op.Path, "#", 2)
			pkg, ok := packageMap[pathParts[0]]
			if !ok {
				packageMap[pathParts[0]] = &bundlev1.Package{
					Name: pathParts[0],
					Secrets: &bundlev1.SecretChain{
						Data: []*bundlev1.KV{},
					},
				}
				pkg = packageMap[pathParts[0]]
			}

			// Process oplog event
			switch op.Operation {
			case compare.Add, compare.Replace:
				// Pack secret value
				payload, err := secret.Pack(op.Value)
				if err != nil {
					return nil, fmt.Errorf("unable to pack secret value for '%s' / '%s': %w", pathParts[0], pathParts[1], err)
				}

				// Assign secret data
				pkg.Secrets.Data = append(pkg.Secrets.Data, &bundlev1.KV{
					Key:   pathParts[1],
					Type:  "string",
					Value: payload,
				})
			case compare.Remove:
				// Ignore secret removal
			}
		default:
			return nil, fmt.Errorf("unknown oplog type '%s'", op.Type)
		}
	}

	// Assign packages
	for _, p := range packageMap {
		b.Packages = append(b.Packages, p)
	}

	// No error
	return b, nil
}

// FromMap builds a secret container from map K/V.
func FromMap(input map[string]KV) (*bundlev1.Bundle, error) {
	// Check input
	if input == nil {
		return nil, fmt.Errorf("unable to process nil map")
	}

	res := &bundlev1.Bundle{
		Packages: []*bundlev1.Package{},
	}
	for packageName, secretKv := range input {
		// Prepare a package
		p := &bundlev1.Package{
			Name:    packageName,
			Secrets: &bundlev1.SecretChain{},
		}

		// Prepare secret data
		for k, v := range secretKv {
			// Pack secret value
			packed, err := secret.Pack(v)
			if err != nil {
				return nil, fmt.Errorf("unable to pack secret value for `%s`: %w", fmt.Sprintf("%s.%s", packageName, k), err)
			}

			// Add to secret package
			p.Secrets.Data = append(p.Secrets.Data, &bundlev1.KV{
				Key:   k,
				Type:  fmt.Sprintf("%T", v),
				Value: packed,
			})
		}

		// Add package to result
		res.Packages = append(res.Packages, p)
	}

	// No error
	return res, nil
}

// FromHCL convert HCL-DSL to a bundle.
func FromHCL(input *hcl.Config) (*bundlev1.Bundle, error) {
	// Check arguments
	if input == nil {
		return nil, errors.New("unable to process nil hcl object")
	}

	// Create an empty bundle.
	res := &bundlev1.Bundle{
		Labels:      input.Labels,
		Annotations: input.Annotations,
		Packages:    []*bundlev1.Package{},
	}

	for _, pkg := range input.Packages {
		// Prepare a package
		p := &bundlev1.Package{
			Name:        pkg.Path,
			Annotations: pkg.Annotations,
			Labels:      pkg.Labels,
			Secrets:     &bundlev1.SecretChain{},
		}

		// Prepare secret data
		for k, v := range pkg.Secrets {
			// Pack secret value
			packed, err := secret.Pack(v)
			if err != nil {
				return nil, fmt.Errorf("unable to pack secret value for `%s`: %w", fmt.Sprintf("%s.%s", pkg.Path, k), err)
			}

			// Add to secret package
			p.Secrets.Data = append(p.Secrets.Data, &bundlev1.KV{
				Key:   k,
				Type:  fmt.Sprintf("%T", v),
				Value: packed,
			})
		}

		// Add package to result
		res.Packages = append(res.Packages, p)
	}

	// No error
	return res, nil
}
