package json

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"unicode"

	"github.com/pkg/errors"
)

var ErrUnsafePermission = errors.New("secrets.json file has unsafe permission")

type Provider struct {
	dir string
}

func NewProvider(dir string) Provider {
	return Provider{dir}
}

func (p Provider) open(service string, write bool) (*os.File, error) {
	var flags int
	if write {
		// If file does not exist, then create a new one. Else, erase the file.
		// The file can only be written onto.
		flags = os.O_CREATE | os.O_WRONLY | os.O_TRUNC
	} else {
		// If file does not exist, then error out. The file can only be read
		// from.
		flags = os.O_RDONLY
	}

	// Make a filename using the given service.
	var filename = fmt.Sprintf("%s_secret.json", SanitizeName(service))

	f, err := os.OpenFile(filepath.Join(p.dir, filename), flags, 0600)
	if err != nil {
		return nil, errors.Wrap(err, "Failed to open file")
	}

	// Stat the file and verify that the permissions are safe.
	s, err := f.Stat()
	if err != nil {
		return nil, errors.Wrap(err, "Failed to stat file")
	}
	if m := s.Mode(); m != 0600 {
		return nil, fmt.Errorf("%s file has unsafe permission %04o", filename, m)
	}

	return f, nil
}

// Get unmarshals the service from the JSON secret file. It errors out if the
// secret file does not exist.
func (p Provider) Get(service string, v interface{}) error {
	f, err := p.open(service, false)
	if err != nil {
		return err
	}
	defer f.Close()

	if err := json.NewDecoder(f).Decode(v); err != nil {
		return errors.Wrap(err, "Failed to decode JSON")
	}

	return nil
}

// Set writes the service's data into a JSON secret file.
func (p Provider) Set(service string, v interface{}) error {
	f, err := p.open(service, true)
	if err != nil {
		return err
	}
	defer f.Close()

	// Create a new formatted encoder.
	enc := json.NewEncoder(f)
	enc.SetIndent("", "\n")

	// Encode using created encoder.
	if err := enc.Encode(v); err != nil {
		return errors.Wrap(err, "Failed to encode JSON")
	}

	return nil
}

// SanitizeName sanitizes the name so that it's safe to use as a filename.
func SanitizeName(name string) string {
	// Escape all weird characters in a filename.
	name = strings.Map(underscoreNonAlphanum, name)

	// Lower-case everything.
	name = strings.ToLower(name)

	return name
}

func underscoreNonAlphanum(r rune) rune {
	// especially does not cover slashes
	if unicode.IsLetter(r) || unicode.IsNumber(r) {
		return r
	}
	return '_'
}
