//go:build !keyring_nofile

package keyring

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/byteness/percent"
	jose "github.com/dvsekhvalnov/jose2go"
)

func init() {
	supportedBackends[FileBackend] = opener(func(cfg Config) (Keyring, error) {
		return &fileKeyring{
			dir:          cfg.FileDir,
			passwordFunc: cfg.FilePasswordFunc,
		}, nil
	})
}

var filenameEscape = func(s string) string {
	return percent.Encode(s, "/")
}
var filenameUnescape = percent.Decode

type fileKeyring struct {
	dir          string
	passwordFunc PromptFunc
	password     string
}

func (k *fileKeyring) resolveDir() (string, error) {
	if k.dir == "" {
		return "", fmt.Errorf("no directory provided for file keyring")
	}

	dir, err := ExpandTilde(k.dir)
	if err != nil {
		return "", err
	}

	stat, err := os.Stat(dir)
	if os.IsNotExist(err) {
		err = os.MkdirAll(dir, 0700)
	} else if err != nil && stat != nil && !stat.IsDir() {
		err = fmt.Errorf("%s is a file, not a directory", dir)
	}

	return dir, err
}

func (k *fileKeyring) unlock() error {
	dir, err := k.resolveDir()
	if err != nil {
		return err
	}

	if k.password == "" {
		pwd, err := k.passwordFunc(fmt.Sprintf("Enter passphrase to unlock %q", dir))
		if err != nil {
			return err
		}
		k.password = pwd
	}

	return nil
}

func (k *fileKeyring) Get(key string) (Item, error) {
	filename, err := k.filename(key)
	if err != nil {
		return Item{}, err
	}

	bytes, err := os.ReadFile(filename)
	if os.IsNotExist(err) {
		return Item{}, ErrKeyNotFound
	} else if err != nil {
		return Item{}, err
	}

	if err = k.unlock(); err != nil {
		return Item{}, err
	}

	payload, _, err := jose.Decode(string(bytes), k.password)
	if err != nil {
		return Item{}, err
	}

	var decoded Item
	err = json.Unmarshal([]byte(payload), &decoded)

	return decoded, err
}

func (k *fileKeyring) GetMetadata(key string) (Metadata, error) {
	filename, err := k.filename(key)
	if err != nil {
		return Metadata{}, err
	}

	stat, err := os.Stat(filename)
	if os.IsNotExist(err) {
		return Metadata{}, ErrKeyNotFound
	} else if err != nil {
		return Metadata{}, err
	}

	// For the File provider, all internal data is encrypted, not just the
	// credentials.  Thus we only have the timestamps.  Return a nil *Item.
	//
	// If we want to change this ... how portable are extended file attributes
	// these days?  Would it break user expectations of the security model to
	// leak data into those?  I'm hesitant to do so.

	return Metadata{
		ModificationTime: stat.ModTime(),
	}, nil
}

func (k *fileKeyring) Set(i Item) error {
	bytes, err := json.Marshal(i)
	if err != nil {
		return err
	}

	if err = k.unlock(); err != nil {
		return err
	}

	token, err := jose.Encrypt(string(bytes), jose.PBES2_HS256_A128KW, jose.A256GCM, k.password,
		jose.Headers(map[string]interface{}{
			"created": time.Now().String(),
		}))
	if err != nil {
		return err
	}

	filename, err := k.filename(i.Key)
	if err != nil {
		return err
	}
	return os.WriteFile(filename, []byte(token), 0600)
}

func (k *fileKeyring) filename(key string) (string, error) {
	dir, err := k.resolveDir()
	if err != nil {
		return "", err
	}

	return filepath.Join(dir, filenameEscape(key)), nil
}

func (k *fileKeyring) Remove(key string) error {
	filename, err := k.filename(key)
	if err != nil {
		return err
	}

	return os.Remove(filename)
}

func (k *fileKeyring) Keys() ([]string, error) {
	dir, err := k.resolveDir()
	if err != nil {
		return nil, err
	}

	var keys = []string{}
	files, _ := os.ReadDir(dir)
	for _, f := range files {
		keys = append(keys, filenameUnescape(f.Name()))
	}

	return keys, nil
}
