package caller

import (
	"net/http"
	"time"

	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	"code.justin.tv/sse/malachai/pkg/config"
	"code.justin.tv/sse/malachai/pkg/internal/inventory"
	"code.justin.tv/sse/malachai/pkg/internal/stats/statsiface"
	"code.justin.tv/sse/malachai/pkg/log"
	"code.justin.tv/sse/malachai/pkg/s2s"
	"code.justin.tv/sse/malachai/pkg/s2s/internal"
	"code.justin.tv/sse/malachai/pkg/signature"
	"code.justin.tv/sse/malachai/pkg/signature/signatureiface"
	"code.justin.tv/systems/sandstorm/manager"
	"code.justin.tv/systems/sandstorm/queue"
)

const (
	metricNameS2SRequestSigned             = "request.signed"
	metricNameS2SRequestSignatureAttempted = "request.signature.attempted"
	metricNameS2SRequestSignatureFailure   = "request.signature.failure"
	metricNameS2SRequestSignatureSuccess   = "request.signature.success"
)

// RequestSignerAPI is the interface RequestSigner implements
type RequestSignerAPI interface {
	Close() error
	SignRequest(*http.Request) error
	SignRequestWithHashedBody(*http.Request, []byte) error
	GenerateSignature(r *http.Request, hashedBody []byte) (key string, value string, err error)
}

// RequestSigner signs http requests
type RequestSigner struct {
	signatureiface.SignatureAPI
	sandstorm manager.API
	statter   statsiface.ReporterAPI
	logger    log.S2SLogger
	cfg       *Config
}

// NewRequestSigner instantiates a new request signer
//
// If logger is nil, it will be defaulted to a nil logger.
func NewRequestSigner(callerName string, cfg *Config, logger log.S2SLogger) (rs *RequestSigner, err error) {

	logger = log.NoOpIfNil(logger)

	if cfg == nil {
		cfg = &Config{}
	}
	cfg.callerName = callerName
	err = cfg.FillDefaults()
	if err != nil {
		return
	}

	logger.Debugf("config options: %#v", cfg)
	logger.Debugf("retrieving service registration for caller name '%s'", cfg.callerName)
	caller, err := internal.GetServiceRegistration(cfg.callerName, cfg.Environment, cfg.AWSConfigBase)
	if err != nil {
		return
	}
	logger.Debugf("service registered is: %#v", caller)

	inventoryClient, err := inventory.New(&inventory.Config{
		Environment:   cfg.Environment,
		AWSConfigBase: cfg.AWSConfigBase,
		RoleArn:       caller.RoleArn,
	})
	if err != nil {
		return
	}

	instanceID, err := internal.NewInstanceID()
	if err != nil {
		return
	}

	err = inventoryClient.Put(&inventory.Instance{
		ServiceID:   caller.ID,
		ServiceName: caller.Name,
		InstanceID:  instanceID,
		Version:     s2s.Version,
	})
	if err != nil {
		return
	}

	logger.Debugf("configuring sandstorm manager using caller role '%s', sandstorm role '%s'", caller.RoleArn, caller.SandstormRoleArn)
	awsConfig := config.AWSConfig(
		cfg.AWSConfigBase,
		cfg.Region,
		cfg.roleArn,
		caller.RoleArn,
		caller.SandstormRoleArn)

	sandstorm := manager.New(manager.Config{
		AWSConfig: awsConfig,
		Queue: queue.Config{
			TopicArn: cfg.SandstormTopicArn,
		},
		InstanceID:  instanceID,
		Environment: cfg.Environment,
	})

	if !cfg.DisableSecretRotationListener {
		logger.Debug("listening for sandstorm updates")
		err = sandstorm.ListenForUpdates()
		if err != nil {
			return
		}
	}

	privateKey := &privateKeyStorer{
		Sandstorm:           sandstorm,
		SandstormSecretName: caller.SandstormSecretName,
		Logger:              logger,
	}
	// load key into cache
	if _, _, err = privateKey.Key(); err != nil {
		return
	}

	rs = &RequestSigner{
		SignatureAPI: &signature.Signer{
			CallerID:   caller.ID,
			Method:     defaultSigningMethod,
			PrivateKey: privateKey.Key,
		},
		sandstorm: sandstorm,
		logger:    logger,
		cfg:       cfg,
		statter:   cfg.StatsReporter,
	}
	return
}

// Close cleans up the sandstorm notification pipeline
func (rs *RequestSigner) Close() (err error) {
	rs.logger.Debugf("stopped listening for sandstorm updates")
	return rs.sandstorm.StopListeningForUpdates()
}

// SignRequest ...
func (rs *RequestSigner) SignRequest(req *http.Request) error {
	return rs.withTimingAndSuccessStat(
		func() error {
			return rs.SignatureAPI.SignRequest(req)
		})

}

// SignRequestWithHashedBody ...
func (rs *RequestSigner) SignRequestWithHashedBody(r *http.Request, hashedBody []byte) error {
	return rs.withTimingAndSuccessStat(
		func() error {
			return rs.SignatureAPI.SignRequestWithHashedBody(r, hashedBody)
		})
}

// GenerateSignature ...
func (rs *RequestSigner) GenerateSignature(r *http.Request, hashedBody []byte) (string, string, error) {
	var key, value string
	err := rs.withTimingAndSuccessStat(
		func() error {
			var err error
			key, value, err = rs.SignatureAPI.GenerateSignature(r, hashedBody)
			return err
		})

	if err != nil {
		return "", "", err
	}

	return key, value, nil
}

func (rs *RequestSigner) withTimingAndSuccessStat(
	fn func() error,
) error {

	rs.statter.Report(metricNameS2SRequestSignatureAttempted, 1, telemetry.UnitCount)

	startTime := time.Now()
	if err := fn(); err != nil {
		rs.statter.Report(metricNameS2SRequestSignatureFailure, 1, telemetry.UnitCount)
		return err
	}

	rs.statter.ReportDurationSample(metricNameS2SRequestSigned, time.Since(startTime))
	rs.statter.Report(metricNameS2SRequestSignatureSuccess, 1, telemetry.UnitCount)
	return nil
}
