package secretstore

import (
	"bufio"
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/sha512"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"strconv"
	"syscall"

	"golang.org/x/crypto/argon2"
)

type Store struct {
	Salt [aes.BlockSize]byte
	Keys []KeyEntry

	unlocked bool
	key      [32]byte
}

type KeyEntry struct {
	Name   string
	Hash   [64]byte
	EncKey [32]byte
}

func New() (s *Store) {
	s = &Store{}
	syscall.Mlock(s.key[:])
	return
}

func Open(path string) (s *Store, err error) {
	f, err := os.Open(path)
	if err != nil {
		return
	}

	defer f.Close()

	s = New()
	_, err = s.ReadFrom(bufio.NewReader(f))

	return
}

func (s *Store) SaveTo(path string) (err error) {
	f, err := os.OpenFile(path, syscall.O_CREAT|syscall.O_TRUNC|syscall.O_WRONLY, 0600)
	if err != nil {
		return
	}

	defer f.Close()

	out := bufio.NewWriter(f)

	_, err = s.WriteTo(out)
	if err != nil {
		return
	}

	err = out.Flush()
	if err != nil {
		return
	}

	return
}

func (s *Store) Close() {
	memzero(s.key[:])
	syscall.Munlock(s.key[:])
	s.unlocked = false
}

func (s *Store) IsNew() bool {
	return len(s.Keys) == 0
}

func (s *Store) Unlocked() bool {
	return s.unlocked
}

func (s *Store) Init(name string, passphrase []byte) (err error) {
	err = randRead(s.key[:])
	if err != nil {
		return
	}
	err = randRead(s.Salt[:])
	if err != nil {
		return
	}

	s.AddKey(name, passphrase)

	s.unlocked = true

	return
}

var jsonFormatHdr = []byte("{json}")

func (s *Store) ReadFrom(in io.Reader) (n int64, err error) {
	memzero(s.key[:])
	s.unlocked = false

	defer func() {
		if err != nil {
			log.Output(2, fmt.Sprintf("failed after %d bytes", n))
		}
	}()

	readFull := func(ba []byte) {
		var nr int
		nr, err = io.ReadFull(in, ba)
		n += int64(nr)
	}

	// read the file's start (json header or start of salt)

	readFull(s.Salt[:len(jsonFormatHdr)])
	if err != nil {
		return
	}

	if !bytes.Equal(s.Salt[:len(jsonFormatHdr)], jsonFormatHdr) {
		// old key file

		// finish reading the salt
		readFull(s.Salt[len(jsonFormatHdr):])
		if err != nil {
			return
		}

		// read the (encrypted) keys
		s.Keys = make([]KeyEntry, 0)
		for {
			k := KeyEntry{Name: "key-" + strconv.Itoa(len(s.Keys))}
			readFull(k.Hash[:])
			if err != nil {
				if err == io.EOF {
					err = nil
				}
				return
			}
			readFull(k.EncKey[:])
			if err != nil {
				return
			}

			s.Keys = append(s.Keys, k)
		}
	}

	err = json.NewDecoder(in).Decode(s)
	return
}

func (s *Store) WriteTo(out io.Writer) (n int64, err error) {
	_, err = out.Write(jsonFormatHdr)
	if err != nil {
		return
	}
	err = json.NewEncoder(out).Encode(s)
	return
}

var ErrNoSuchKey = errors.New("no such key")

func (s *Store) HasKey(passphrase []byte) bool {
	key, hash := s.keyPairFromPassword(passphrase)
	defer memzero(key[:])

	for _, k := range s.Keys {
		if k.Hash == hash {
			return true
		}
	}

	return false
}

func (s *Store) Unlock(passphrase []byte) (ok bool) {
	key, hash := s.keyPairFromPassword(passphrase)
	defer memzero(key[:])

	var idx = -1
	for i := range s.Keys {
		if hash == s.Keys[i].Hash {
			idx = i
			break
		}
	}

	if idx == -1 {
		return
	}

	s.decryptTo(s.key[:], s.Keys[idx].EncKey[:], &key)

	s.unlocked = true
	return true
}

func (s *Store) AddKey(name string, passphrase []byte) {
	key, _ := s.keyPairFromPassword(passphrase)
	memzero(passphrase)
	s.AddRawKey(name, key)
}

func (s *Store) AddRawKey(name string, key [32]byte) {
	hash := sha512.Sum512(key[:])

	k := KeyEntry{Name: name, Hash: hash}

	encKey := s.encrypt(s.key[:], &key)
	copy(k.EncKey[:], encKey)

	s.Keys = append(s.Keys, k)
	memzero(key[:])
}

func (s *Store) keyPairFromPassword(password []byte) (key [32]byte, hash [64]byte) {
	keySlice := argon2.IDKey(password, s.Salt[:], 1, 64*1024, 4, 32)

	copy(key[:], keySlice)
	memzero(keySlice)

	hash = sha512.Sum512(key[:])

	return
}

func (s *Store) NewEncrypter(iv [aes.BlockSize]byte) cipher.Stream {
	if !s.unlocked {
		panic("not unlocked")
	}
	return newEncrypter(iv, &s.key)
}

func (s *Store) NewDecrypter(iv [aes.BlockSize]byte) cipher.Stream {
	if !s.unlocked {
		panic("not unlocked")
	}
	return newDecrypter(iv, &s.key)
}

func (s *Store) encrypt(src []byte, key *[32]byte) (dst []byte) {
	dst = make([]byte, len(src))
	newEncrypter(s.Salt, key).XORKeyStream(dst, src)
	return
}

func (s *Store) decryptTo(dst []byte, src []byte, key *[32]byte) {
	newDecrypter(s.Salt, key).XORKeyStream(dst, src)
}

func newEncrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
	c, err := aes.NewCipher(key[:])
	if err != nil {
		panic(fmt.Errorf("failed to init AES: %w", err))
	}

	return cipher.NewCFBEncrypter(c, iv[:])
}

func newDecrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
	c, err := aes.NewCipher(key[:])
	if err != nil {
		panic(fmt.Errorf("failed to init AES: %w", err))
	}

	return cipher.NewCFBDecrypter(c, iv[:])
}
