package encryption

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"github.com/forceu/gokapi/internal/helper"
	"github.com/forceu/gokapi/internal/models"
	"github.com/secure-io/sio-go"
	"golang.org/x/crypto/scrypt"
	"io"
	"log"
	"os"
	"time"
)

// NoEncryption means all files are stored in plaintext
const NoEncryption = 0

// LocalEncryptionStored means remote files are stored in plaintext, cipher for local files is in plaintext
const LocalEncryptionStored = 1

// LocalEncryptionInput means remote files are stored in plaintext, password needs to be entered on startup
const LocalEncryptionInput = 2

// FullEncryptionStored means all files are encrypted, cipher for local files is in plaintext
const FullEncryptionStored = 3

// FullEncryptionInput means all files are encrypted, password needs to be entered on startup
const FullEncryptionInput = 4

// EndToEndEncryption means all files are encrypted and decrypted client-side
const EndToEndEncryption = 5

var encryptedKey, ramCipher []byte

const blockSize = 32
const nonceSize = 12

// Init needs to be called to load the master key into memory or ask the user for the password
func Init(config models.Configuration) {
	switch config.Encryption.Level {
	case NoEncryption:
		return
	case LocalEncryptionStored, FullEncryptionStored:
		initWithCipher(config.Encryption.Cipher)
	case LocalEncryptionInput, FullEncryptionInput:
		initWithPassword(config.Encryption.Salt, config.Encryption.Checksum, config.Encryption.ChecksumSalt)
	case EndToEndEncryption:
		return
	}
}

func initWithPassword(saltPw, expectedChecksum, saltChecksum string) {
	if saltPw == "" || saltChecksum == "" {
		log.Fatal("Empty salt provided. Please rerun setup with --reconfigure")
	}
	pw := readAndCheckPassword(expectedChecksum, saltChecksum)
	cipherKey, err := scrypt.Key([]byte(pw), []byte(saltPw), 1048576, 8, 1, blockSize)
	if err != nil {
		cipherKey = []byte{}
		log.Fatal(err)
	}

	storeMasterKey(cipherKey)
}

func readAndCheckPassword(expectedChecksum, saltChecksum string) string {
	fmt.Println("Please enter encryption password:")
	pw := helper.ReadPassword()
	if pw == "" {
		log.Fatal("Empty password provided")
	}
	fmt.Print("Checking password")

	checksumFinished := false
	go func() {
		for !checksumFinished {
			fmt.Print(".")
			time.Sleep(time.Second)
		}
	}()

	checkSum := PasswordChecksum(pw, saltChecksum)
	checksumFinished = true

	if checkSum != expectedChecksum {
		pw = ""
		fmt.Println("FAIL")
		log.Fatal("Incorrect password provided")
	}

	fmt.Println("OK")
	return pw
}

// PasswordChecksum creates a checksum which is used to check if the supplied password is correct
func PasswordChecksum(pw, salt string) string {
	cipherKey, err := scrypt.Key([]byte(pw), []byte(salt), 1048576, 8, 1, blockSize)
	if err != nil {
		cipherKey = []byte{}
		log.Fatal(err)
	}

	hasher := sha256.New()
	hasher.Write(cipherKey)
	return hex.EncodeToString(hasher.Sum(nil))
}

func initWithCipher(cipherKey []byte) {
	if len(cipherKey) != 32 {
		log.Fatal("Invalid cipher provided. Please rerun setup with --reconfigure")
	}
	storeMasterKey(cipherKey)
}

func storeMasterKey(cipherKey []byte) {
	var err error
	ramCipher, err = getRandomData(blockSize)
	if err != nil {
		log.Fatal(err)
	}
	encryptedKey, err = EncryptDecryptBytes(cipherKey, ramCipher, make([]byte, nonceSize), true)
	if err != nil {
		log.Fatal(err)
	}
}

func getMasterCipher() []byte {
	key, err := EncryptDecryptBytes(encryptedKey, ramCipher, make([]byte, nonceSize), false)
	if err != nil {
		key = []byte{}
		log.Fatal(err)
	}
	return key
}

// Encrypt encrypts a file
func Encrypt(encInfo *models.EncryptionInfo, input io.Reader, output io.Writer) error {
	key, err := generateNewFileKey(encInfo)
	if err != nil {
		return err
	}
	stream := getStream(key)
	nonce := make([]byte, stream.NonceSize()) // Nonce is not used
	reader := stream.EncryptReader(input, nonce, nil)
	_, err = io.Copy(output, reader)
	return err
}

func createDecryptReader(encInfo models.EncryptionInfo, input io.Reader) (*sio.DecReader, error) {
	key, err := GetCipherFromFile(encInfo)
	if err != nil {
		return nil, err
	}
	stream := getStream(key)
	nonce := make([]byte, stream.NonceSize()) // Nonce is not used
	return stream.DecryptReader(input, nonce, nil), nil
}

// DecryptReader modifies a reader so it can decrypt encrypted files
func DecryptReader(encInfo models.EncryptionInfo, input io.Reader, output io.Writer) error {
	reader, err := createDecryptReader(encInfo, input)
	if err != nil {
		return err
	}
	_, err = io.Copy(output, reader)
	return err
}

// IsCorrectKey checks if correct key is being used. This does not check for complete file authentication.
func IsCorrectKey(encInfo models.EncryptionInfo, input *os.File) bool {
	_, err := createDecryptReader(encInfo, input)
	if err != nil {
		fmt.Println(err)
		return false
	}
	return true
}

// GetDecryptWriter returns a writer that can decrypt encrypted files
func GetDecryptWriter(cipherKey []byte, input io.Writer) (io.Writer, error) {
	stream := getStream(cipherKey)
	nonce := make([]byte, stream.NonceSize()) // Nonce is not used
	return stream.DecryptWriter(input, nonce, nil), nil
}

// GetDecryptReader returns a reader that can decrypt encrypted files
func GetDecryptReader(cipherKey []byte, input io.Reader) (io.Reader, error) {
	stream := getStream(cipherKey)
	nonce := make([]byte, stream.NonceSize()) // Nonce is not used
	return stream.DecryptReader(input, nonce, nil), nil
}

// GetEncryptReader returns a reader that can encrypt plain files
func GetEncryptReader(cipherKey []byte, input io.Reader) (io.Reader, error) {
	stream := getStream(cipherKey)
	nonce := make([]byte, stream.NonceSize()) // Nonce is not used
	return stream.EncryptReader(input, nonce, nil), nil
}

// GetEncryptWriter returns a writer that can encrypt plain files
func GetEncryptWriter(cipherKey []byte, input io.Writer) (*sio.EncWriter, error) {
	stream := getStream(cipherKey)
	nonce := make([]byte, stream.NonceSize()) // Nonce is not used
	return stream.EncryptWriter(input, nonce, nil), nil

}

func generateNewFileKey(encInfo *models.EncryptionInfo) ([]byte, error) {
	encryptionKey, err := getRandomData(blockSize)
	if err != nil {
		return []byte{}, err
	}
	nonce, err := getRandomData(nonceSize)
	if err != nil {
		return []byte{}, err
	}
	encInfo.Nonce = nonce
	encInfo.IsEncrypted = true
	encKey, err := fileCipherEncrypt(encryptionKey, nonce)
	if err != nil {
		return []byte{}, err
	}
	encInfo.DecryptionKey = encKey
	return encryptionKey, nil
}

// CalculateEncryptedFilesize returns the filesize of the encrypted file including the encryption overhead
func CalculateEncryptedFilesize(size int64) int64 {
	return size + getStream(make([]byte, blockSize)).Overhead(size)
}

// GetCipherFromFile loads the cipher from a file model
func GetCipherFromFile(encInfo models.EncryptionInfo) ([]byte, error) {
	cipherFile, err := fileCipherDecrypt(encInfo.DecryptionKey, encInfo.Nonce)
	if err != nil {
		return []byte{}, err
	}
	return cipherFile, nil
}

func getStream(cipherKey []byte) *sio.Stream {
	block, err := aes.NewCipher(cipherKey)
	if err != nil {
		log.Fatal(err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		log.Fatal(err)
	}
	stream := sio.NewStream(gcm, sio.BufSize)
	return stream
}

func fileCipherEncrypt(input, nonce []byte) ([]byte, error) {
	return EncryptDecryptBytes(input, getMasterCipher(), nonce, true)
}
func fileCipherDecrypt(input, nonce []byte) ([]byte, error) {
	return EncryptDecryptBytes(input, getMasterCipher(), nonce, false)
}

// EncryptDecryptBytes encrypts or decrypts a byte array
func EncryptDecryptBytes(input, cipherBlock, nonce []byte, doEncrypt bool) ([]byte, error) {
	block, err := aes.NewCipher(cipherBlock)
	if err != nil {
		return []byte{}, err
	}
	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return []byte{}, err
	}
	if doEncrypt {
		return aesgcm.Seal(nil, nonce, input, nil), nil
	}
	return aesgcm.Open(nil, nonce, input, nil)
}

func getRandomData(size int) ([]byte, error) {
	data := make([]byte, size)
	read, err := rand.Read(data)
	if err != nil {
		return []byte{}, err
	}
	if read != size {
		return []byte{}, errors.New("incorrect size written")
	}
	return data, nil
}

// GetRandomCipher a 32 byte long array with random data
func GetRandomCipher() ([]byte, error) {
	return getRandomData(blockSize)
}

// GetRandomNonce a 12 byte long array with random data
func GetRandomNonce() ([]byte, error) {
	return getRandomData(nonceSize)
}
