package store

import (
	"context"
	"crypto/tls"
	"fmt"
	"io"

	"github.com/asdine/storm"
	"github.com/coreos/bbolt"
)

type Interface interface {
	Domain() string
	TLS() tls.Certificate
	TLSConfig() *tls.Config

	SetDomain(string) error
	SetTLS([]byte, []byte) error

	io.Closer
}

func New(ctx context.Context, filename string) (Interface, error) {
	db, err := storm.Open(filename, storm.BoltOptions(0600, &bolt.Options{}))
	if err != nil {
		return nil, err
	}

	out := &concrete{
		filename: filename,
		storm:    db,
	}

	if err := out.setup(); err != nil {
		return nil, err
	}

	return out, nil
}

func Init(filename, domain string, certPEM, keyPEM []byte) error {
	db, err := storm.Open(filename, storm.BoltOptions(0600, &bolt.Options{}))
	if err != nil {
		return err
	}

	builder := &concrete{
		filename: filename,
		storm:    db,
	}

	defer builder.Close()

	if err := builder.SetDomain(domain); err != nil {
		return fmt.Errorf("Couldn't set domain: %v", err)
	}

	if err := builder.SetTLS(certPEM, keyPEM); err != nil {
		return fmt.Errorf("Couldn't set TLS: %v", err)
	}

	return nil
}

type concrete struct {
	filename     string
	storm        *storm.DB
	domainBucket storm.Node

	// These are persisted in BoltDB, but we
	// Might as well keep them in memory for the duration, though.
	domain string
	cert   tls.Certificate
}

func (c *concrete) Domain() string {
	return c.domain
}

func (c *concrete) TLS() tls.Certificate {
	return c.cert
}

func (c *concrete) TLSConfig() *tls.Config {
	return &tls.Config{
		GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
			certCopy := c.TLS()

			return &certCopy, nil
		},

		ServerName: c.Domain(),
	}
}

func (c *concrete) SetDomain(domain string) error {
	if err := c.storm.Set("config", "domain", domain); err != nil {
		return err
	}

	c.domain = domain

	return nil
}

func (c *concrete) SetTLS(certPEM, keyPEM []byte) error {
	cert, err := tls.X509KeyPair(certPEM, keyPEM)
	if err != nil {
		return fmt.Errorf("Couldn't parse data as TLS keypair: %v", err)
	}

	if cert.PrivateKey == nil {
		return fmt.Errorf("No private key for TLS certificate")
	}

	domainBucket := c.storm.From("domains").From(c.Domain())

	tx, err := domainBucket.Begin(true)
	if err != nil {
		return err
	}

	if err := tx.Set("config", "cert", certPEM); err != nil {
		return err
	}

	if err := tx.Set("config", "key", keyPEM); err != nil {
		return err
	}

	if err := tx.Commit(); err != nil {
		return err
	}

	c.cert = cert

	return nil
}

func (c *concrete) Close() error {
	return c.storm.Close()
}

// Bootstraps all cached values from storm
func (c *concrete) setup() error {
	var domain string
	var keyPEM []byte
	var certPEM []byte

	if err := c.storm.Get("config", "domain", &domain); err != nil {
		return fmt.Errorf("Couldn't read config/domain: %v", err)
	}

	domainBucket := c.storm.From("domains").From(domain)

	if err := domainBucket.Get("config", "cert", &certPEM); err != nil {
		return fmt.Errorf("Couldn't read domains/%s/config/cert: %v", domain, err)
	}

	if err := domainBucket.Get("config", "key", &keyPEM); err != nil {
		return fmt.Errorf("Couldn't read domains/%s/config/key: %v", domain, err)
	}

	cert, err := tls.X509KeyPair(certPEM, keyPEM)
	if err != nil {
		return fmt.Errorf("Couldn't parse key and certificate: %v", err)
	}

	c.domainBucket = domainBucket
	c.domain = domain
	c.cert = cert

	return nil
}
