package caller

import (
	"crypto"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"sync"
	"time"

	"code.justin.tv/sse/malachai/pkg/backoff"
	"code.justin.tv/sse/malachai/pkg/jwtvalidation"
	"code.justin.tv/sse/malachai/pkg/log"
	"code.justin.tv/systems/sandstorm/manager"

	joseCrypto "github.com/SermoDigital/jose/crypto"
)

var defaultSigningMethod = joseCrypto.SigningMethodRS256

var (
	errKeyNotLoaded = errors.New("signing key is not loaded")
	errKeyNotFound  = errors.New("signing key plaintext not found in sandstorm")
	errKeyInvalid   = errors.New("signing key plaintext in sandstorm was invalid")
)

type privateKeyStorer struct {
	Sandstorm           manager.API
	SandstormSecretName string
	Logger              log.S2SLogger

	initSync    sync.Once
	key         crypto.Signer
	fingerprint string
}

func (p *privateKeyStorer) Key() (key crypto.Signer, fingerprint string, err error) {
	if err = p.init(); err != nil {
		return
	}
	if p.key == nil {
		err = errKeyNotLoaded
		return
	}
	return p.key, p.fingerprint, err
}

func (p *privateKeyStorer) init() error {
	var err error
	p.initSync.Do(func() {
		p.Sandstorm.RegisterSecretUpdateCallback(
			p.SandstormSecretName,
			p.reloadSecret,
		)

		err = backoff.ConstantRetry(func() (err error) {
			return p.loadSecret()
		}, 3, 10*time.Second)
	})
	return err
}

func (p *privateKeyStorer) reloadSecret(input manager.SecretChangeset) {
	err := backoff.ConstantRetry(func() (err error) {
		err = p.loadSecret()
		if err != nil {
			p.Logger.Warn("failed to load secret: %s", err.Error())
		}
		return err
	}, 10, 30*time.Second)
	if err != nil {
		p.Logger.Warnf("error refreshing secret on update: %s", err.Error())
	}
}

func (p *privateKeyStorer) loadSecret() error {
	secret, err := p.Sandstorm.Get(p.SandstormSecretName)
	if err != nil {
		return err
	}

	if secret == nil {
		return errKeyNotFound
	}

	block, _ := pem.Decode(secret.Plaintext)
	if block == nil {
		return errKeyInvalid
	}

	privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return err
	}

	if p.fingerprint, err = jwtvalidation.CalculatePubkeyFingerprintFromPrivateKey(privateKey); err != nil {
		return err
	}

	p.key = privateKey
	return nil
}
