package ini //import "github.com/getsops/sops/v3/stores/ini"

import (
	"bytes"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/getsops/sops/v3"
	"github.com/getsops/sops/v3/config"
	"github.com/getsops/sops/v3/stores"
	"gopkg.in/ini.v1"
)

// Store handles storage of ini data.
type Store struct {
	config *config.INIStoreConfig
}

func NewStore(c *config.INIStoreConfig) *Store {
	return &Store{config: c}
}

func (store *Store) Name() string {
	return "ini"
}

func (store Store) encodeTree(branches sops.TreeBranches) ([]byte, error) {
	iniFile := ini.Empty(ini.LoadOptions{AllowNonUniqueSections: true})
	iniFile.DeleteSection(ini.DefaultSection)
	for _, branch := range branches {
		for _, item := range branch {
			if _, ok := item.Key.(sops.Comment); ok {
				continue
			}
			section, err := iniFile.NewSection(item.Key.(string))
			if err != nil {
				return nil, fmt.Errorf("Error encoding section %s: %s", item.Key, err)
			}
			itemTree, ok := item.Value.(sops.TreeBranch)
			if !ok {
				return nil, fmt.Errorf("Error encoding section: Section values should always be TreeBranches")
			}

			first := 0
			if len(itemTree) > 0 {
				if sectionComment, ok := itemTree[0].Key.(sops.Comment); ok {
					section.Comment = sectionComment.Value
					first = 1
				}
			}

			var lastItem *ini.Key
			for i := first; i < len(itemTree); i++ {
				keyVal := itemTree[i]
				if comment, ok := keyVal.Key.(sops.Comment); ok {
					if lastItem != nil {
						lastItem.Comment = comment.Value
					}
				} else {
					lastItem, err = section.NewKey(keyVal.Key.(string), stores.ValToString(keyVal.Value))
					if err != nil {
						return nil, fmt.Errorf("Error encoding key: %s", err)
					}
				}
			}
		}
	}
	var buffer bytes.Buffer
	iniFile.WriteTo(&buffer)
	return buffer.Bytes(), nil
}

func (store Store) stripCommentChar(comment string) string {
	if strings.HasPrefix(comment, ";") {
		comment = strings.TrimLeft(comment, "; ")
	} else if strings.HasPrefix(comment, "#") {
		comment = strings.TrimLeft(comment, "# ")
	}
	return comment
}

func (store Store) iniFromTreeBranches(branches sops.TreeBranches) ([]byte, error) {
	return store.encodeTree(branches)
}

func (store Store) treeBranchesFromIni(in []byte) (sops.TreeBranches, error) {
	iniFile, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, in)
	if err != nil {
		return nil, err
	}
	var branch sops.TreeBranch
	for _, section := range iniFile.Sections() {

		item, err := store.treeItemFromSection(section)
		if err != nil {
			return sops.TreeBranches{branch}, err
		}
		branch = append(branch, item)
	}
	return sops.TreeBranches{branch}, nil
}

func (store Store) treeItemFromSection(section *ini.Section) (sops.TreeItem, error) {
	var sectionItem sops.TreeItem
	sectionItem.Key = section.Name()
	var items sops.TreeBranch

	if section.Comment != "" {
		items = append(items, sops.TreeItem{
			Key: sops.Comment{
				Value: store.stripCommentChar(section.Comment),
			},
			Value: nil,
		})
	}

	for _, key := range section.Keys() {
		item := sops.TreeItem{Key: key.Name(), Value: key.Value()}
		items = append(items, item)
		if key.Comment != "" {
			items = append(items, sops.TreeItem{
				Key: sops.Comment{
					Value: store.stripCommentChar(key.Comment),
				},
				Value: nil,
			})
		}
	}
	sectionItem.Value = items
	return sectionItem, nil
}

// LoadEncryptedFile loads encrypted INI file's bytes onto a sops.Tree runtime object
func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) {
	branches, err := store.LoadPlainFile(in)
	if err != nil {
		return sops.Tree{}, err
	}
	branches, metadata, err := stores.ExtractMetadata(branches, stores.MetadataOpts{
		Flatten: stores.MetadataFlattenBelowTop,
	})
	if err != nil {
		return sops.Tree{}, err
	}
	return sops.Tree{
		Branches: branches,
		Metadata: metadata,
	}, nil
}

// LoadPlainFile loads a plaintext INI file's bytes onto a sops.TreeBranches runtime object
func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) {
	branches, err := store.treeBranchesFromIni(in)
	if err != nil {
		return branches, fmt.Errorf("Could not unmarshal input data: %s", err)
	}
	return branches, nil
}

// EmitEncryptedFile returns encrypted INI file bytes corresponding to a sops.Tree
// runtime object
func (store *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) {
	branches, err := stores.SerializeMetadata(in, stores.MetadataOpts{
		Flatten: stores.MetadataFlattenBelowTop,
	})
	if err != nil {
		return nil, fmt.Errorf("Error marshaling metadata: %s", err)
	}
	return store.EmitPlainFile(branches)
}

// EmitPlainFile returns the plaintext INI file bytes corresponding to a sops.TreeBranches object
func (store *Store) EmitPlainFile(in sops.TreeBranches) ([]byte, error) {
	out, err := store.iniFromTreeBranches(in)
	if err != nil {
		return nil, fmt.Errorf("Error marshaling to INI: %s", err)
	}
	return out, nil
}

func (store Store) encodeValue(v interface{}) ([]byte, error) {
	switch v := v.(type) {
	case sops.TreeBranches:
		return store.encodeTree(v)
	default:
		return json.Marshal(v)
	}
}

// EmitValue returns a single value encoded in a generic interface{} as bytes
func (store *Store) EmitValue(v interface{}) ([]byte, error) {
	return store.encodeValue(v)
}

// EmitExample returns the plaintext INI file bytes corresponding to the SimpleTree example
func (store *Store) EmitExample() []byte {
	bytes, err := store.EmitPlainFile(stores.ExampleSimpleTree.Branches)
	if err != nil {
		panic(err)
	}
	return bytes
}

// HasSopsTopLevelKey checks whether a top-level "sops" key exists.
func (store *Store) HasSopsTopLevelKey(branch sops.TreeBranch) bool {
	return stores.HasSopsTopLevelKey(branch)
}
