// Package store implements the go-micro store interface
package store

import (
	"fmt"
	"io/ioutil"
	"path/filepath"
	"sync"

	"github.com/gofrs/uuid"
	"github.com/owncloud/ocis/settings/pkg/proto/v0"
)

var m = &sync.RWMutex{}

// ListBundles returns all bundles in the dataPath folder that match the given type.
func (s Store) ListBundles(bundleType proto.Bundle_Type, bundleIDs []string) ([]*proto.Bundle, error) {
	// FIXME: list requests should be ran against a cache, not FS
	m.RLock()
	defer m.RUnlock()

	bundlesFolder := s.buildFolderPathForBundles(false)
	bundleFiles, err := ioutil.ReadDir(bundlesFolder)
	if err != nil {
		return []*proto.Bundle{}, nil
	}

	records := make([]*proto.Bundle, 0, len(bundleFiles))
	for _, bundleFile := range bundleFiles {
		record := proto.Bundle{}
		err = s.parseRecordFromFile(&record, filepath.Join(bundlesFolder, bundleFile.Name()))
		if err != nil {
			s.Logger.Warn().Msgf("error reading %v", bundleFile)
			continue
		}
		if record.Type != bundleType {
			continue
		}
		if len(bundleIDs) > 0 && !containsStr(record.Id, bundleIDs) {
			continue
		}
		records = append(records, &record)
	}

	return records, nil
}

// containsStr checks if the strs slice contains str
func containsStr(str string, strs []string) bool {
	for _, s := range strs {
		if s == str {
			return true
		}
	}
	return false
}

// ReadBundle tries to find a bundle by the given id within the dataPath.
func (s Store) ReadBundle(bundleID string) (*proto.Bundle, error) {
	m.RLock()
	defer m.RUnlock()

	filePath := s.buildFilePathForBundle(bundleID, false)
	record := proto.Bundle{}
	if err := s.parseRecordFromFile(&record, filePath); err != nil {
		return nil, err
	}

	s.Logger.Debug().Msgf("read contents from file: %v", filePath)
	return &record, nil
}

// ReadSetting tries to find a setting by the given id within the dataPath.
func (s Store) ReadSetting(settingID string) (*proto.Setting, error) {
	m.RLock()
	defer m.RUnlock()

	bundles, err := s.ListBundles(proto.Bundle_TYPE_DEFAULT, []string{})
	if err != nil {
		return nil, err
	}
	for _, bundle := range bundles {
		for _, setting := range bundle.Settings {
			if setting.Id == settingID {
				return setting, nil
			}
		}
	}
	return nil, fmt.Errorf("could not read setting: %v", settingID)
}

// WriteBundle writes the given record into a file within the dataPath.
func (s Store) WriteBundle(record *proto.Bundle) (*proto.Bundle, error) {
	// FIXME: locking should happen on the file here, not globally.
	m.Lock()
	defer m.Unlock()
	if record.Id == "" {
		record.Id = uuid.Must(uuid.NewV4()).String()
	}
	filePath := s.buildFilePathForBundle(record.Id, true)
	if err := s.writeRecordToFile(record, filePath); err != nil {
		return nil, err
	}

	s.Logger.Debug().Msgf("request contents written to file: %v", filePath)
	return record, nil
}

// AddSettingToBundle adds the given setting to the bundle with the given bundleID.
func (s Store) AddSettingToBundle(bundleID string, setting *proto.Setting) (*proto.Setting, error) {
	bundle, err := s.ReadBundle(bundleID)
	if err != nil {
		return nil, err
	}
	if setting.Id == "" {
		setting.Id = uuid.Must(uuid.NewV4()).String()
	}
	setSetting(bundle, setting)
	_, err = s.WriteBundle(bundle)
	if err != nil {
		return nil, err
	}
	return setting, nil
}

// RemoveSettingFromBundle removes the setting from the bundle with the given ids.
func (s Store) RemoveSettingFromBundle(bundleID string, settingID string) error {
	bundle, err := s.ReadBundle(bundleID)
	if err != nil {
		return nil
	}
	if ok := removeSetting(bundle, settingID); ok {
		if _, err := s.WriteBundle(bundle); err != nil {
			return err
		}
	}
	return nil
}

// indexOfSetting finds the index of the given setting within the given bundle.
// returns -1 if the setting was not found.
func indexOfSetting(bundle *proto.Bundle, settingID string) int {
	for index := range bundle.Settings {
		s := bundle.Settings[index]
		if s.Id == settingID {
			return index
		}
	}
	return -1
}

// setSetting will append or overwrite the given setting within the given bundle
func setSetting(bundle *proto.Bundle, setting *proto.Setting) {
	m.Lock()
	defer m.Unlock()
	index := indexOfSetting(bundle, setting.Id)
	if index == -1 {
		bundle.Settings = append(bundle.Settings, setting)
	} else {
		bundle.Settings[index] = setting
	}
}

// removeSetting will remove the given setting from the given bundle
func removeSetting(bundle *proto.Bundle, settingID string) bool {
	m.Lock()
	defer m.Unlock()
	index := indexOfSetting(bundle, settingID)
	if index == -1 {
		return false
	}
	bundle.Settings = append(bundle.Settings[:index], bundle.Settings[index+1:]...)
	return true
}
