// Copyright 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.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 ssmec2roleprovider

import (
	"context"
	"fmt"
	"time"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/backoffconfig"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/managedInstances/registration"
	"github.com/aws/amazon-ssm-agent/agent/ssm/authregister"
	"github.com/aws/amazon-ssm-agent/agent/ssm/authtokenrequest"
	"github.com/aws/amazon-ssm-agent/agent/ssm/rsaauth"
	"github.com/aws/amazon-ssm-agent/common/identity/credentialproviders/iirprovider"

	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/service/ssm"
)

const RegistrationType = "EC2"

var (
	newIirRsaAuth           = rsaauth.NewIirRsaClient
	getStoredInstanceId     = registration.InstanceID
	getStoredPrivateKey     = registration.PrivateKey
	getStoredPublicKey      = registration.PublicKey
	getStoredPrivateKeyType = registration.PrivateKeyType
)

// SSMEC2RoleProvider sends requests for credentials to systems manager signed with AWS SigV4
type SSMEC2RoleProvider struct {
	credentials.Expiry
	// ExpiryWindow will allow the credentials to trigger refreshing prior to
	// the credentials actually expiring. This is beneficial so race conditions
	// with expiring credentials do not cause request to fail unexpectedly
	// due to ExpiredTokenException exceptions.
	//
	// So a ExpiryWindow of 10s would cause calls to IsExpired() to return true
	// 10 seconds before the credentials are actually expired.
	//
	// If ExpiryWindow is 0 or less it will be ignored.
	ExpiryWindow time.Duration
	Config       *appconfig.SsmagentConfig
	Log          log.T

	IMDSClient         iirprovider.IEC2MdsSdkClient
	tokenRequestClient authtokenrequest.IClient
	InstanceInfo       *InstanceInfo

	registrationInfo *authregister.RegistrationInfo
}

func (p *SSMEC2RoleProvider) isEC2InstanceRegistered() bool {
	if p.registrationInfo == nil {
		registrationInfo := p.loadRegistrationInfo(p.InstanceInfo.InstanceId)
		if registrationInfo == nil || registrationInfo.InstanceId == "" {
			p.Log.Debug("EC2 instance is not yet registered with Systems Manager")
			return false
		}

		p.registrationInfo = registrationInfo
	}

	return p.registrationInfo.PrivateKey != "" && p.registrationInfo.KeyType != ""
}

// GetInstanceRegion gets the region of the instance for this provider.
func (p *SSMEC2RoleProvider) GetInstanceRegion() string {
	if p.InstanceInfo == nil {
		return "" // Should never happen with proper initialization
	}
	return p.InstanceInfo.Region
}

func (p *SSMEC2RoleProvider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) {
	var err error
	var roleCreds *ssm.RequestManagedInstanceRoleTokenOutput

	if !p.isEC2InstanceRegistered() {
		return EmptyCredentials(), fmt.Errorf("ec2 instance not yet registered with Systems Manager")
	}

	if p.tokenRequestClient == nil {
		p.tokenRequestClient = newIirRsaAuth(p.Log.WithContext("[TokenRequestService]"),
			p.Config,
			p.IMDSClient,
			p.GetInstanceRegion(),
			p.registrationInfo.PrivateKey)
	}

	exponentialBackoff, err := backoffconfig.GetDefaultExponentialBackoff()
	if err != nil {
		p.Log.Errorf("failed to create backoff Config. Error: %v", exponentialBackoff)
		return EmptyCredentials(), err
	}

	// Get role token
	roleCreds, err = p.tokenRequestClient.RequestManagedInstanceRoleTokenWithContext(ctx, p.InstanceInfo.InstanceId)
	if err != nil {
		return EmptyCredentials(), fmt.Errorf("error calling RequestManagedInstanceRoleToken: %w", err)
	}

	// Set the expiration of our credentials
	p.SetExpiration(*roleCreds.TokenExpirationDate, p.ExpiryWindow)

	return credentials.Value{
		AccessKeyID:     *roleCreds.AccessKeyId,
		SecretAccessKey: *roleCreds.SecretAccessKey,
		SessionToken:    *roleCreds.SessionToken,
		ProviderName:    ProviderName,
	}, nil
}

// Retrieve retrieves EC2 credentials from Systems Manager
func (p *SSMEC2RoleProvider) Retrieve() (credentials.Value, error) {
	return p.RetrieveWithContext(context.Background())
}

// EmptyCredentials returns empty SSMEC2RoleProvider credentials
func EmptyCredentials() credentials.Value {
	return credentials.Value{ProviderName: ProviderName}
}

func (p *SSMEC2RoleProvider) loadRegistrationInfo(instanceId string) *authregister.RegistrationInfo {
	registrationInfo := &authregister.RegistrationInfo{
		InstanceId: getStoredInstanceId(p.Log, RegistrationType, registration.EC2RegistrationVaultKey),
		PrivateKey: getStoredPrivateKey(p.Log, RegistrationType, registration.EC2RegistrationVaultKey),
		KeyType:    getStoredPrivateKeyType(p.Log, RegistrationType, registration.EC2RegistrationVaultKey),
		PublicKey:  getStoredPublicKey(p.Log, RegistrationType, registration.EC2RegistrationVaultKey),
	}

	if registrationInfo.InstanceId == "" || registrationInfo.PrivateKey == "" ||
		registrationInfo.KeyType == "" || registrationInfo.InstanceId != instanceId {
		registrationInfo.InstanceId = "" // setting it as blank to try registration
	}

	return registrationInfo
}
