package certificates

import (
	"crypto/rsa"
	"crypto/tls"
	"encoding/base64"
	"encoding/json"
	"io/ioutil"
	"time"

	"github.com/go-acme/lego/v4/certcrypto"
	log "github.com/sirupsen/logrus"
)

// A convenience wrapper around a TLS certificate
type CertificateWrapper struct {
	TlsCertificate    *tls.Certificate `json:"-"`
	Domain            string           `json:"domain"`
	NotAfter          time.Time        `json:"not_after"`
	PrivateKeyEncoded string           `json:"private_key"`
	Certificate       []byte           `json:"certificate"`
	IssuerCertificate []byte           `json:"issuer_certificate"`
	CertificateUrl    string           `json:"certificate_url"`
}

// A structure to store all the certificates we know of in.
type CertificatesCache struct {
	// The certificate to use as a fallback if all else fails.
	FallbackCertificate *CertificateWrapper

	// Mapping of domain name to certificate.
	Certificates map[string]CertificateWrapper
}

// Internal type to let encoding JSON handle the bulk of the work.
type certificatesStore struct {
	FallbackCertificate CertificateWrapper   `json:"fallback"`
	Certificates        []CertificateWrapper `json:"certificates"`
}

// Decodes the private key of the certificate wrapper.
func (c *CertificateWrapper) GetPrivateKey() *rsa.PrivateKey {
	data, _ := base64.StdEncoding.DecodeString(c.PrivateKeyEncoded)
	pk, _ := certcrypto.ParsePEMPrivateKey(data)

	return pk.(*rsa.PrivateKey)
}

// Populate the certificate's TlsCertificate field.
func (c *CertificateWrapper) initTlsCertificate() {
	pk, _ := base64.StdEncoding.DecodeString(c.PrivateKeyEncoded)
	tlsCert, _ := tls.X509KeyPair(
		c.Certificate,
		pk,
	)
	c.TlsCertificate = &tlsCert
}

// Checks if the certificate is still valid now.
func (c *CertificateWrapper) IsValid() bool {
	return time.Now().Compare(c.NotAfter) <= -1
}

// Serializes the certificate cache to a JSON string for writing to a file.
func (c *CertificatesCache) toStoreData() []byte {
	certs := make([]CertificateWrapper, 0)
	for _, cert := range c.Certificates {
		certs = append(certs, cert)
	}

	result, err := json.Marshal(certificatesStore{
		FallbackCertificate: *c.FallbackCertificate,
		Certificates:        certs,
	})
	if err != nil {
		log.Errorf("Failed to Marshal cache: %v", err)
	}
	return result
}

// Saves the cache to disk.
func (c *CertificatesCache) FlushToDisk(path string) {
	ioutil.WriteFile(path, c.toStoreData(), 0600)
}

func (c *CertificatesCache) AddCert(cert CertificateWrapper, path string) {
	c.Certificates[cert.Domain] = cert
	c.FlushToDisk(path)
}

// Load the certificate cache from the file.
func CertificateCacheFromFile(path string) (CertificatesCache, error) {
	content, err := ioutil.ReadFile(path)
	if err != nil {
		return CertificatesCache{}, err
	}

	var store certificatesStore
	_ = json.Unmarshal(content, &store)

	store.FallbackCertificate.initTlsCertificate()
	cache := CertificatesCache{
		FallbackCertificate: &store.FallbackCertificate,
	}

	certs := make(map[string]CertificateWrapper)
	for _, cert := range store.Certificates {
		cert.initTlsCertificate()
		certs[cert.Domain] = cert
	}
	cache.Certificates = certs

	return cache, nil
}
