package caller

import (
	"net/http"
	"time"

	"code.justin.tv/sse/malachai/pkg/config"
	"code.justin.tv/sse/malachai/pkg/internal/inventory"
	"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"
	"github.com/cactus/go-statsd-client/statsd"
)

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

// NewRequestSigner instantiates a new request signer
func NewRequestSigner(callerName string, cfg *Config, logger log.S2SLogger) (rs *RequestSigner, err error) {
	if logger == nil {
		logger = &log.NoOpLogger{}
	}

	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
	}

	statter, err := cfg.statsd(cfg.callerName)
	if err != nil {
		return
	}

	rs = &RequestSigner{
		SignatureAPI: &signature.Signer{
			CallerID:   caller.ID,
			Method:     defaultSigningMethod,
			PrivateKey: privateKey.Key,
		},
		sandstorm: sandstorm,
		logger:    logger,
		cfg:       cfg,
		statter:   statter,
	}
	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(
		"request.signature",
		"request.signed",
		func() error {
			return rs.SignatureAPI.SignRequest(req)
		})
}

// SignRequestWithHashedBody ...
func (rs *RequestSigner) SignRequestWithHashedBody(r *http.Request, hashedBody []byte) error {
	return rs.withTimingAndSuccessStat(
		"request.signature",
		"request.signed",
		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(
		"request.signature",
		"request.signed",
		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(
	countMetricNamePrefix string,
	durationMetricName string,
	fn func() error,
) error {
	startTime := time.Now()

	rs.withLoggedError(func() error {
		return rs.statter.Inc(countMetricNamePrefix+".attempted", 1, 1.0)
	})

	if err := fn(); err != nil {
		rs.withLoggedError(func() error {
			return rs.statter.Inc(countMetricNamePrefix+".failure", 1, 1.0)
		})
		return err
	}

	rs.withLoggedError(func() error {
		return rs.statter.Inc(countMetricNamePrefix+".success", 1, 1.0)
	})

	rs.withLoggedError(func() error {
		return rs.statter.TimingDuration(durationMetricName, time.Since(startTime), 1.0)
	})

	return nil
}

func (rs *RequestSigner) withLoggedError(fn func() error) {
	if err := fn(); err != nil {
		rs.logger.Errorf("failed to send stat: %s", err.Error())
	}
}
