/*
Package files defines a Provider that stores all data in the filesystem

We assume the same validator hash may be reused by many different
headers/Commits, and thus store it separately. This leaves us
with three issues:

  1. Given a validator hash, retrieve the validator set if previously stored
  2. Given a block height, find the Commit with the highest height <= h
  3. Given a FullCommit, store it quickly to satisfy 1 and 2

Note that we do not worry about caching, as that can be achieved by
pairing this with a MemStoreProvider and CacheProvider from certifiers
*/
package files

import (
	"encoding/hex"
	"fmt"
	"math"
	"os"
	"path/filepath"
	"sort"

	"github.com/pkg/errors"

	"github.com/tendermint/tendermint/lite"
	liteErr "github.com/tendermint/tendermint/lite/errors"
)

// nolint
const (
	Ext      = ".tsd"
	ValDir   = "validators"
	CheckDir = "checkpoints"
	dirPerm  = os.FileMode(0755)
	//filePerm = os.FileMode(0644)
)

type provider struct {
	valDir   string
	checkDir string
}

// NewProvider creates the parent dir and subdirs
// for validators and checkpoints as needed
func NewProvider(dir string) lite.Provider {
	valDir := filepath.Join(dir, ValDir)
	checkDir := filepath.Join(dir, CheckDir)
	for _, d := range []string{valDir, checkDir} {
		err := os.MkdirAll(d, dirPerm)
		if err != nil {
			panic(err)
		}
	}
	return &provider{valDir: valDir, checkDir: checkDir}
}

func (p *provider) encodeHash(hash []byte) string {
	return hex.EncodeToString(hash) + Ext
}

func (p *provider) encodeHeight(h int64) string {
	// pad up to 10^12 for height...
	return fmt.Sprintf("%012d%s", h, Ext)
}

// StoreCommit saves a full commit after it has been verified.
func (p *provider) StoreCommit(fc lite.FullCommit) error {
	// make sure the fc is self-consistent before saving
	err := fc.ValidateBasic(fc.Commit.Header.ChainID)
	if err != nil {
		return err
	}

	paths := []string{
		filepath.Join(p.checkDir, p.encodeHeight(fc.Height())),
		filepath.Join(p.valDir, p.encodeHash(fc.Header.ValidatorsHash)),
	}
	for _, path := range paths {
		err := SaveFullCommit(fc, path)
		// unknown error in creating or writing immediately breaks
		if err != nil {
			return err
		}
	}
	return nil
}

// GetByHeight returns the closest commit with height <= h.
func (p *provider) GetByHeight(h int64) (lite.FullCommit, error) {
	// first we look for exact match, then search...
	path := filepath.Join(p.checkDir, p.encodeHeight(h))
	fc, err := LoadFullCommit(path)
	if liteErr.IsCommitNotFoundErr(err) {
		path, err = p.searchForHeight(h)
		if err == nil {
			fc, err = LoadFullCommit(path)
		}
	}
	return fc, err
}

// LatestCommit returns the newest commit stored.
func (p *provider) LatestCommit() (fc lite.FullCommit, err error) {
	// Note to future: please update by 2077 to avoid rollover
	return p.GetByHeight(math.MaxInt32 - 1)
}

// search for height, looks for a file with highest height < h
// return certifiers.ErrCommitNotFound() if not there...
func (p *provider) searchForHeight(h int64) (string, error) {
	d, err := os.Open(p.checkDir)
	if err != nil {
		return "", errors.WithStack(err)
	}
	files, err := d.Readdirnames(0)

	d.Close()
	if err != nil {
		return "", errors.WithStack(err)
	}

	desired := p.encodeHeight(h)
	sort.Strings(files)
	i := sort.SearchStrings(files, desired)
	if i == 0 {
		return "", liteErr.ErrCommitNotFound()
	}
	found := files[i-1]
	path := filepath.Join(p.checkDir, found)
	return path, errors.WithStack(err)
}

// GetByHash returns a commit exactly matching this validator hash.
func (p *provider) GetByHash(hash []byte) (lite.FullCommit, error) {
	path := filepath.Join(p.valDir, p.encodeHash(hash))
	return LoadFullCommit(path)
}