// Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
// Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
// Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors
// may be used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package keykeeperv1

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"encoding/hex"
	"fmt"
	"github.com/ProtonMail/gopenpgp/v2/crypto"
	"github.com/google/uuid"
	"os/exec"
	"peridot.resf.org/peridot/db/models"
	"peridot.resf.org/utils"
	"strings"
	"sync"
)

// LoadedKey keeps the key and some other information in memory
// todo(mustafa): Add TTL, rotation check, etc.
type LoadedKey struct {
	sync.Mutex
	keyUuid uuid.UUID
	gpgId   string
}

func logCmdRun(cmd *exec.Cmd) (*bytes.Buffer, error) {
	var outBuf bytes.Buffer
	cmd.Stdout = &outBuf
	cmd.Stderr = &outBuf
	return &outBuf, cmd.Run()
}

func gpgCmdEnv(cmd *exec.Cmd) *exec.Cmd {
	cmd.Env = append(cmd.Env, "GNUPGHOME=/keykeeper/gpg")
	return cmd
}

func (s *Server) importGpgKey(armoredKey string) error {
	cmd := gpgCmdEnv(exec.Command("gpg", "--batch", "--yes", "--import", "-"))
	cmd.Stdin = strings.NewReader(armoredKey)
	out, err := logCmdRun(cmd)
	if err != nil {
		s.log.Errorf("failed to import gpg key: %s", out.String())
	}
	return err
}

// WarmGPGKey warms up a specific GPG key
// This involves shelling out to GPG to import the key
func (s *Server) WarmGPGKey(key string, armoredKey string, gpgKey *crypto.Key, db *models.Key) (*LoadedKey, error) {
	cachedKeyAny, ok := s.keys.Load(key)
	// This means that the key is already loaded
	if ok {
		return cachedKeyAny.(*LoadedKey), nil
	}

	err := s.importGpgKey(armoredKey)
	if err != nil {
		return nil, err
	}

	cachedKey := &LoadedKey{
		keyUuid: db.ID,
		gpgId:   gpgKey.GetHexKeyID(),
	}
	s.keys.Store(key, cachedKey)

	return cachedKey, nil
}

// EnsureGPGKey ensures that the key is loaded
func (s *Server) EnsureGPGKey(key string) (*LoadedKey, error) {
	cachedKeyAny, ok := s.keys.Load(key)
	if ok {
		return cachedKeyAny.(*LoadedKey), nil
	}

	// Key not found in cache, fetch from database
	// Fetch the encryption key, nonce and external key ID from the database
	k, err := s.db.GetKeyByName(key)
	if err != nil {
		return nil, err
	}
	encBytes, err := hex.DecodeString(k.EncKey)
	if err != nil {
		return nil, err
	}
	nonce, err := hex.DecodeString(k.Nonce)
	if err != nil {
		return nil, err
	}

	// Fetch the encrypted key from secret store
	store := s.stores[k.ExtStoreType]
	if store == nil {
		return nil, fmt.Errorf("no store found for type %s, which key with name \"%s\" relies on. manual rotation may be required #TODO-DOCS", k.ExtStoreType, k.Name)
	}
	encryptedArmoredKeyHex, err := store.Get(k.ExtStoreId)
	if err != nil {
		return nil, err
	}
	encryptedArmoredKey, err := hex.DecodeString(encryptedArmoredKeyHex)
	if err != nil {
		return nil, err
	}

	// Decrypt the key
	block, err := aes.NewCipher(encBytes)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	armoredKey, err := gcm.Open(nil, nonce, encryptedArmoredKey, nil)
	if err != nil {
		return nil, err
	}
	keyObj, err := crypto.NewKeyFromArmored(string(armoredKey))
	if err != nil {
		s.log.Errorf("could not get key from armored string: %v", err)
		return nil, utils.InternalError
	}

	loadedKey, err := s.WarmGPGKey(key, string(armoredKey), keyObj, k)
	if err != nil {
		return nil, err
	}

	return loadedKey, nil
}
