package truststores

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/safing/jess"
)

const (
	signetSuffix        = ".signet"
	recipientSuffix     = ".recipient"
	envelopeSuffix      = ".envelope"
	permittedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789- ._+@"
)

// TrustStore errors.
var (
	errInvalidSignetIDChars     = fmt.Errorf("this trust store only allows these characters in signet IDs: %s", permittedCharacters)
	errInvalidEnvelopeNameChars = fmt.Errorf("this trust store only allows these characters in envelope names: %s", permittedCharacters)
)

// DirTrustStore is a simple trust store using a filesystem directory as the storage backend.
type DirTrustStore struct {
	lock       sync.Mutex
	storageDir string
}

// GetSignet returns the Signet with the given ID.
func (dts *DirTrustStore) GetSignet(id string, recipient bool) (*jess.Signet, error) {
	// check ID
	ok := NamePlaysNiceWithFS(id)
	if !ok {
		return nil, errInvalidSignetIDChars
	}

	// synchronize fs access
	dts.lock.Lock()
	defer dts.lock.Unlock()

	// read from storage
	filename := filepath.Join(dts.storageDir, makeStorageID(id, recipient))
	signet, err := LoadSignetFromFile(filename)
	if err != nil {
		return nil, err
	}

	return signet, nil
}

// StoreSignet stores a Signet in the TrustStore.
func (dts *DirTrustStore) StoreSignet(signet *jess.Signet) error {
	// synchronize fs access
	dts.lock.Lock()
	defer dts.lock.Unlock()

	// write
	filename := filepath.Join(dts.storageDir, makeStorageID(signet.ID, signet.Public))
	return WriteSignetToFile(signet, filename)
}

// DeleteSignet deletes the Signet or Recipient with the given ID.
func (dts *DirTrustStore) DeleteSignet(id string, recipient bool) error {
	// check ID
	ok := NamePlaysNiceWithFS(id)
	if !ok {
		return errInvalidSignetIDChars
	}

	// synchronize fs access
	dts.lock.Lock()
	defer dts.lock.Unlock()

	// delete
	filename := filepath.Join(dts.storageDir, makeStorageID(id, recipient))
	return os.Remove(filename)
}

// SelectSignets returns a selection of the signets in the trust store. Results are filtered by tool/algorithm and whether it you're looking for a signet (private key) or a recipient (public key).
func (dts *DirTrustStore) SelectSignets(filter uint8, schemes ...string) ([]*jess.Signet, error) {
	dts.lock.Lock()
	defer dts.lock.Unlock()

	var selection []*jess.Signet

	// walk the storage
	err := filepath.Walk(dts.storageDir, func(path string, info os.FileInfo, err error) error {
		// consider errors
		if err != nil {
			return err
		}

		// skip dirs
		if info.IsDir() {
			return nil
		}

		// check for suffix
		if !strings.HasSuffix(path, signetSuffix) &&
			!strings.HasSuffix(path, recipientSuffix) {
			return nil
		}

		signet, err := LoadSignetFromFile(path)
		if err != nil {
			// add failed signet to list
			selection = append(selection, &jess.Signet{
				Info: &jess.SignetInfo{
					Name: "[failed to load]",
				},
				ID:     strings.Split(filepath.Base(path), ".")[0],
				Public: strings.HasSuffix(path, recipientSuffix),
			})
			return err
		}

		// check signet scheme
		if len(schemes) > 0 && !stringInSlice(signet.Scheme, schemes) {
			return nil
		}

		// check type filter
		switch filter {
		case jess.FilterSignetOnly:
			if signet.Public {
				return nil
			}
		case jess.FilterRecipientOnly:
			if !signet.Public {
				return nil
			}
		}

		selection = append(selection, signet)
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("failed to access trust store entry: %w", err)
	}

	return selection, nil
}

// GetEnvelope returns the Envelope with the given name.
func (dts *DirTrustStore) GetEnvelope(name string) (*jess.Envelope, error) {
	// check name
	ok := NamePlaysNiceWithFS(name)
	if !ok {
		return nil, errInvalidEnvelopeNameChars
	}

	// synchronize fs access
	dts.lock.Lock()
	defer dts.lock.Unlock()

	// read from storage
	filename := filepath.Join(dts.storageDir, name+envelopeSuffix)
	envelope, err := LoadEnvelopeFromFile(filename)
	if err != nil {
		return nil, err
	}

	return envelope, nil
}

// StoreEnvelope stores an Envelope.
func (dts *DirTrustStore) StoreEnvelope(envelope *jess.Envelope) error {
	// synchronize fs access
	dts.lock.Lock()
	defer dts.lock.Unlock()

	// write
	filename := filepath.Join(dts.storageDir, envelope.Name+envelopeSuffix)
	return WriteEnvelopeToFile(envelope, filename)
}

// DeleteEnvelope deletes the Envelope with the given name.
func (dts *DirTrustStore) DeleteEnvelope(name string) error {
	// check name
	ok := NamePlaysNiceWithFS(name)
	if !ok {
		return errInvalidEnvelopeNameChars
	}

	// synchronize fs access
	dts.lock.Lock()
	defer dts.lock.Unlock()

	// delete
	filename := filepath.Join(dts.storageDir, name+envelopeSuffix)
	return os.Remove(filename)
}

// AllEnvelopes returns all envelopes.
func (dts *DirTrustStore) AllEnvelopes() ([]*jess.Envelope, error) {
	dts.lock.Lock()
	defer dts.lock.Unlock()

	var all []*jess.Envelope

	// walk the storage
	err := filepath.Walk(dts.storageDir, func(path string, info os.FileInfo, err error) error {
		// consider errors
		if err != nil {
			return err
		}

		// skip dirs
		if info.IsDir() {
			return nil
		}

		// check for suffix
		if !strings.HasSuffix(path, envelopeSuffix) {
			return nil
		}

		envelope, err := LoadEnvelopeFromFile(path)
		if err != nil {
			// add failed envelope to list
			all = append(all, &jess.Envelope{
				Name: fmt.Sprintf("%s [failed to load]",
					strings.TrimSuffix(filepath.Base(path), envelopeSuffix)),
			})
			return err
		}

		all = append(all, envelope)
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("failed to access trust store entry: %w", err)
	}

	return all, nil
}

// NewDirTrustStore returns a new trust store using a filesystem directory as the storage backend.
func NewDirTrustStore(storageDir string) (*DirTrustStore, error) {
	cleanedPath := filepath.Clean(storageDir)

	// validate path
	info, err := os.Stat(cleanedPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("trust store does not exist: %w", err)
		}
		return nil, fmt.Errorf("failed to access trust store: %w", err)
	}
	if !info.IsDir() {
		return nil, errors.New("truststore storage dir is a file, not a directory")
	}

	return &DirTrustStore{
		storageDir: cleanedPath,
	}, nil
}

// NamePlaysNiceWithFS checks if the given string plays nice with filesystems.
func NamePlaysNiceWithFS(s string) (ok bool) {
	for _, c := range s {
		n := countRuneInString(permittedCharacters, c)
		if n == 0 {
			return false
		}
	}
	return true
}

func countRuneInString(s string, r rune) (n int) {
	for {
		i := strings.IndexRune(s, r)
		if i < 0 {
			return
		}
		n++
		s = s[i+1:]
	}
}

func makeStorageID(id string, recipient bool) string {
	if recipient {
		return id + recipientSuffix
	}
	return id + signetSuffix
}