// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.

// package registration provides managed instance information
package registration

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/managedInstances/auth"
	"github.com/aws/amazon-ssm-agent/agent/managedInstances/fingerprint"
)

type instanceInfo struct {
	InstanceID            string `json:"instanceID"`
	Region                string `json:"region"`
	InstanceType          string `json:"instanceType"`
	AvailabilityZone      string `json:"availabilityZone"`
	PrivateKey            string `json:"privateKey"`
	PublicKey             string `json:"publicKey"`
	PrivateKeyType        string `json:"privateKeyType"`
	PrivateKeyCreatedDate string `json:"privateKeyCreatedDate"`
}

var (
	lock                       = sync.RWMutex{}
	loadedServerInfo           instanceInfo
	loadedServerInfoKey        string
	loadedServerManifestPrefix string
)

const (
	RegVaultKey             = "RegistrationKey"
	EC2RegistrationVaultKey = "EC2RegistrationKey"

	// If not date is stored with private key, the default age is 10 years
	defaultPrivateKeyAgeInDays = 3650
	defaultDateStringFormat    = "2006-01-02 15:04:05.999999999 -0700 MST"
)

// InstanceID of the managed instance.
func InstanceID(log log.T, manifestFileNamePrefix, vaultKey string) string {
	instance := getInstanceInfo(log, manifestFileNamePrefix, vaultKey)
	return instance.InstanceID
}

// Region of the managed instance.
func Region(log log.T, manifestFileNamePrefix, vaultKey string) string {
	instance := getInstanceInfo(log, manifestFileNamePrefix, vaultKey)
	return instance.Region
}

// PrivateKey of the managed instance.
func PrivateKey(log log.T, manifestFileNamePrefix, vaultKey string) string {
	instance := getInstanceInfo(log, manifestFileNamePrefix, vaultKey)
	return instance.PrivateKey
}

// PublicKey of the instance.
func PublicKey(log log.T, manifestFileNamePrefix, vaultKey string) string {
	instance := getInstanceInfo(log, manifestFileNamePrefix, vaultKey)
	return instance.PublicKey
}

// PrivateKeyType of the managed instance.
func PrivateKeyType(log log.T, manifestFileNamePrefix, vaultKey string) string {
	instance := getInstanceInfo(log, manifestFileNamePrefix, vaultKey)
	return instance.PrivateKeyType
}

// Fingerprint of the managed instance.
func Fingerprint(log log.T) (string, error) {
	return fingerprint.InstanceFingerprint(log)
}

// HasManagedInstancesCredentials returns true when the valid registration information is present
func HasManagedInstancesCredentials(log log.T, manifestFileNamePrefix, vaultKey string) bool {
	info := getInstanceInfo(log, manifestFileNamePrefix, vaultKey)

	// check if we need to activate instance
	return info.PrivateKey != "" && info.Region != "" && info.InstanceID != ""
}

// UpdatePrivateKey saves the private key into the registration persistence store
func UpdatePrivateKey(log log.T, privateKey, privateKeyType, manifestFileNamePrefix, vaultKey string) (err error) {
	info := getInstanceInfo(log, manifestFileNamePrefix, vaultKey)
	info.PrivateKey = privateKey
	info.PrivateKeyType = privateKeyType
	info.PrivateKeyCreatedDate = time.Now().Format(defaultDateStringFormat)
	return updateServerInfo(info, "", vaultKey)
}

// ShouldRotatePrivateKey returns true if serviceSaysRotate or private key has surpassed privateKeyMaxDaysAge
func ShouldRotatePrivateKey(log log.T, executableToRotateKey string, privateKeyMaxDaysAge int, serviceSaysRotate bool, manifestFileNamePrefix, vaultKey string) (bool, error) {
	// only one executable should rotate private key to reduce chances of race condition
	if !strings.HasPrefix(filepath.Base(os.Args[0]), executableToRotateKey) {
		return false, nil
	}

	// check if service tells agent to rotate
	if serviceSaysRotate {
		return true, nil
	}

	// If max age is less or equal to 0, rotation is off
	if privateKeyMaxDaysAge <= 0 {
		return false, nil
	}
	info := getInstanceInfo(log, manifestFileNamePrefix, vaultKey)

	keyAgeInDays := defaultPrivateKeyAgeInDays
	if info.PrivateKeyCreatedDate != "" {
		// Parse stored time using default time format
		date, err := time.Parse(defaultDateStringFormat, info.PrivateKeyCreatedDate)

		if err != nil {
			return false, err
		}
		keyAgeInDays = int(time.Since(date).Hours() / 24)
	}

	return keyAgeInDays >= privateKeyMaxDaysAge, nil
}

func GeneratePublicKey(privateKey string) (publicKey string, err error) {
	var rsaKey auth.RsaKey
	rsaKey, err = auth.DecodePrivateKey(privateKey)
	if err != nil {
		return
	}

	return rsaKey.EncodePublicKey()
}

// UpdateServerInfo saves the instance info into the registration persistence store
func UpdateServerInfo(instanceID, region, publicKey, privateKey, privateKeyType, manifestFileNamePrefix, vaultKey string) (err error) {
	info := instanceInfo{
		InstanceID:            instanceID,
		Region:                region,
		PrivateKey:            privateKey,
		PrivateKeyType:        privateKeyType,
		PrivateKeyCreatedDate: time.Now().Format(defaultDateStringFormat),
		PublicKey:             publicKey, // Value will be present only for EC2
	}

	return updateServerInfo(info, manifestFileNamePrefix, vaultKey)
}

// GenerateKeyPair generate a new keypair
func GenerateKeyPair() (publicKey, privateKey, keyType string, err error) {
	var keyPair auth.RsaKey

	keyPair, err = auth.CreateKeypair()
	if err != nil {
		return
	}

	privateKey, err = keyPair.EncodePrivateKey()
	if err != nil {
		return
	}

	publicKey, err = keyPair.EncodePublicKey()
	if err != nil {
		return
	}

	keyType = auth.KeyType
	return
}

func updateServerInfo(info instanceInfo, manifestFileNamePrefix, vaultKey string) (err error) {
	lock.Lock()
	defer lock.Unlock()

	var data []byte
	if data, err = json.Marshal(info); err != nil {
		return fmt.Errorf("failed to marshal instance info. %v", err)
	}

	//call vault apis here and update the refId
	if err = vault.Store(manifestFileNamePrefix, vaultKey, data); err != nil {
		return fmt.Errorf("failed to store instance info in vault. %v", err)
	}

	loadedServerInfo = info
	loadedServerInfoKey = vaultKey
	loadedServerManifestPrefix = manifestFileNamePrefix
	return
}

func loadServerInfo(manifestFileNamePrefix, vaultKey string) (loadErr error) {
	lock.RLock()
	defer lock.RUnlock()

	var info instanceInfo = instanceInfo{}

	if !vault.IsManifestExists(manifestFileNamePrefix) {
		loadedServerInfo = info
		return nil
	}

	if d, err := vault.Retrieve(manifestFileNamePrefix, vaultKey); err != nil {
		return fmt.Errorf("Failed to load instance info from vault. %v", err)
	} else {
		if err = json.Unmarshal(d, &info); err != nil {
			return fmt.Errorf("Failed to unmarshal instance info. %v", err)
		}
	}

	loadedServerInfo = info
	loadedServerInfoKey = vaultKey
	loadedServerManifestPrefix = manifestFileNamePrefix
	return nil
}

func getInstanceInfo(log log.T, manifestFileNamePrefix, vaultKey string) instanceInfo {
	if loadedServerInfo.InstanceID == "" || loadedServerManifestPrefix != manifestFileNamePrefix || loadedServerInfoKey != vaultKey {
		if err := loadServerInfo(manifestFileNamePrefix, vaultKey); err != nil {
			log.Warnf("Error while loading server info %v", err)
		}
	}

	return loadedServerInfo
}

func ReloadInstanceInfo(log log.T, manifestFileNamePrefix, vaultKey string) {
	if err := loadServerInfo(manifestFileNamePrefix, vaultKey); err != nil {
		log.Warnf("Error while loading server info %v", err)
	}
}

// Temp moved here, should refactor entire file to be behind interface
func NewOnpremRegistrationInfo() IOnpremRegistrationInfo {
	return onpremRegistation{}
}

type IOnpremRegistrationInfo interface {
	InstanceID(log.T, string, string) string
	Region(log.T, string, string) string
	PrivateKey(log.T, string, string) string
	PrivateKeyType(log.T, string, string) string
	Fingerprint(log.T) (string, error)
	GenerateKeyPair() (string, string, string, error)
	UpdatePrivateKey(log.T, string, string, string, string) error
	HasManagedInstancesCredentials(log.T, string, string) bool
	GeneratePublicKey(string) (string, error)
	ShouldRotatePrivateKey(log.T, string, int, bool, string, string) (bool, error)
	ReloadInstanceInfo(log.T, string, string)
}

type onpremRegistation struct{}

// InstanceID returns the managed instance ID
func (onpremRegistation) InstanceID(log log.T, manifestFileNamePrefix, vaultKey string) string {
	return InstanceID(log, manifestFileNamePrefix, vaultKey)
}

// Region returns the managed instance region
func (onpremRegistation) Region(log log.T, manifestFileNamePrefix, vaultKey string) string {
	return Region(log, manifestFileNamePrefix, vaultKey)
}

// PrivateKey returns the managed instance PrivateKey
func (onpremRegistation) PrivateKey(log log.T, manifestFileNamePrefix, vaultKey string) string {
	return PrivateKey(log, manifestFileNamePrefix, vaultKey)
}

// PrivateKeyType returns the managed instance PrivateKey
func (onpremRegistation) PrivateKeyType(log log.T, manifestFileNamePrefix, vaultKey string) string {
	return PrivateKeyType(log, manifestFileNamePrefix, vaultKey)
}

// Fingerprint returns the managed instance fingerprint
func (onpremRegistation) Fingerprint(log log.T) (string, error) { return Fingerprint(log) }

// GenerateKeyPair generate a new keypair
func (onpremRegistation) GenerateKeyPair() (publicKey, privateKey, keyType string, err error) {
	return GenerateKeyPair()
}

// UpdatePrivateKey saves the private key into the registration persistence store
func (onpremRegistation) UpdatePrivateKey(log log.T, privateKey, privateKeyType, manifestFileNamePrefix, vaultKey string) (err error) {
	return UpdatePrivateKey(log, privateKey, privateKeyType, manifestFileNamePrefix, vaultKey)
}

// HasManagedInstancesCredentials returns if the instance has registration
func (onpremRegistation) HasManagedInstancesCredentials(log log.T, manifestFileNamePrefix, vaultKey string) bool {
	return HasManagedInstancesCredentials(log, manifestFileNamePrefix, vaultKey)
}

// ShouldRotatePrivateKey returns true of the age of the private key is greater or equal than argument.
func (onpremRegistation) ShouldRotatePrivateKey(log log.T, executableToRotateKey string, privateKeyMaxDaysAge int, serviceSaysRotate bool, manifestFileNamePrefix, vaultKey string) (bool, error) {
	return ShouldRotatePrivateKey(log, executableToRotateKey, privateKeyMaxDaysAge, serviceSaysRotate, manifestFileNamePrefix, vaultKey)
}

// GeneratePublicKey generate the public key of a provided private key
func (onpremRegistation) GeneratePublicKey(privateKey string) (string, error) {
	return GeneratePublicKey(privateKey)
}

// ReloadInstanceInfo reloads instance info from disk
func (onpremRegistation) ReloadInstanceInfo(log log.T, manifestFileNamePrefix string, vaultKey string) {
	ReloadInstanceInfo(log, manifestFileNamePrefix, vaultKey)
}
