// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package certmagic

import (
	"net/url"
	"path"
	"regexp"
	"strings"
	"time"
)

// Storage is a type that implements a key-value store.
// Keys are prefix-based, with forward slash '/' as separators
// and without a leading slash.
//
// Processes running in a cluster will wish to use the
// same Storage value (its implementation and configuration)
// in order to share certificates and other TLS resources
// with the cluster.
type Storage interface {
	// Locker provides atomic synchronization
	// operations, making Storage safe to share.
	Locker

	// Store puts value at key.
	Store(key string, value []byte) error

	// Load retrieves the value at key.
	Load(key string) ([]byte, error)

	// Delete deletes key.
	Delete(key string) error

	// Exists returns true if the key exists
	// and there was no error checking.
	Exists(key string) bool

	// List returns all keys that match prefix.
	List(prefix string) ([]string, error)

	// Stat returns information about key.
	Stat(key string) (KeyInfo, error)
}

// Locker facilitates synchronization of certificate tasks across
// machines and networks.
type Locker interface {
	// TryLock will attempt to acquire the lock for key. If a
	// lock could be obtained, nil values are returned as no
	// waiting is required. If not (meaning another process is
	// already working on key), a Waiter value will be returned,
	// upon which you should Wait() until it is finished.
	//
	// The actual implementation of obtaining of a lock must be
	// an atomic operation so that multiple TryLock calls at the
	// same time always results in only one caller receiving the
	// lock. TryLock always returns without waiting.
	//
	// To prevent deadlocks, all implementations (where this concern
	// is relevant) should put a reasonable expiration on the lock in
	// case Unlock is unable to be called due to some sort of network
	// or system failure or crash.
	TryLock(key string) (Waiter, error)

	// Unlock releases the lock for key. This method must ONLY be
	// called after a successful call to TryLock where no Waiter was
	// returned, and only after the operation requiring the lock is
	// finished, even if it errored or timed out. It is INCORRECT to
	// call Unlock if any non-nil value was returned from a call to
	// TryLock or if Unlock was not called at all. Unlock should also
	// clean up any unused resources allocated during TryLock.
	Unlock(key string) error
}

// Waiter is a type that can block until a lock is released.
type Waiter interface {
	Wait()
}

// KeyInfo holds information about a key in storage.
type KeyInfo struct {
	Key      string
	Modified time.Time
	Size     int64
}

// storeTx stores all the values or none at all.
func storeTx(s Storage, all []keyValue) error {
	for i, kv := range all {
		err := s.Store(kv.key, kv.value)
		if err != nil {
			for j := i - 1; j >= 0; j-- {
				s.Delete(all[j].key)
			}
			return err
		}
	}
	return nil
}

// keyValue pairs a key and a value.
type keyValue struct {
	key   string
	value []byte
}

// KeyBuilder provides a namespace for methods that
// build keys and key prefixes, for addressing items
// in a Storage implementation.
type KeyBuilder struct{}

// CAPrefix returns the storage key prefix for
// the given certificate authority URL.
func (keys KeyBuilder) CAPrefix(ca string) string {
	caURL, err := url.Parse(ca)
	if err != nil {
		caURL = &url.URL{Host: ca}
	}
	return path.Join(prefixACME, keys.safe(caURL.Host))
}

// SitePrefix returns a key prefix for items associated with
// the site using the given CA URL.
func (keys KeyBuilder) SitePrefix(ca, domain string) string {
	return path.Join(keys.CAPrefix(ca), "sites", keys.safe(domain))
}

// SiteCert returns the path to the certificate file for domain.
func (keys KeyBuilder) SiteCert(ca, domain string) string {
	return path.Join(keys.SitePrefix(ca, domain), keys.safe(domain)+".crt")
}

// SitePrivateKey returns the path to domain's private key file.
func (keys KeyBuilder) SitePrivateKey(ca, domain string) string {
	return path.Join(keys.SitePrefix(ca, domain), keys.safe(domain)+".key")
}

// SiteMeta returns the path to the domain's asset metadata file.
func (keys KeyBuilder) SiteMeta(ca, domain string) string {
	return path.Join(keys.SitePrefix(ca, domain), keys.safe(domain)+".json")
}

// UsersPrefix returns a key prefix for items related to
// users associated with the given CA URL.
func (keys KeyBuilder) UsersPrefix(ca string) string {
	return path.Join(keys.CAPrefix(ca), "users")
}

// UserPrefix returns a key prefix for items related to
// the user with the given email for the given CA URL.
func (keys KeyBuilder) UserPrefix(ca, email string) string {
	if email == "" {
		email = emptyEmail
	}
	return path.Join(keys.UsersPrefix(ca), keys.safe(email))
}

// UserReg gets the path to the registration file for the user
// with the given email address for the given CA URL.
func (keys KeyBuilder) UserReg(ca, email string) string {
	return keys.safeUserKey(ca, email, "registration", ".json")
}

// UserPrivateKey gets the path to the private key file for the
// user with the given email address on the given CA URL.
func (keys KeyBuilder) UserPrivateKey(ca, email string) string {
	return keys.safeUserKey(ca, email, "private", ".key")
}

// OCSPStaple returns a key for the OCSP staple associated
// with the given certificate. If you have the PEM bundle
// handy, pass that in to save an extra encoding step.
func (keys KeyBuilder) OCSPStaple(cert *Certificate, pemBundle []byte) string {
	var ocspFileName string
	if len(cert.Names) > 0 {
		firstName := keys.safe(cert.Names[0])
		ocspFileName = firstName + "-"
	}
	ocspFileName += fastHash(pemBundle)
	return path.Join(prefixOCSP, ocspFileName)
}

// safeUserKey returns a key for the given email, with the default
// filename, and the filename ending in the given extension.
func (keys KeyBuilder) safeUserKey(ca, email, defaultFilename, extension string) string {
	if email == "" {
		email = emptyEmail
	}
	email = strings.ToLower(email)
	filename := keys.emailUsername(email)
	if filename == "" {
		filename = defaultFilename
	}
	filename = keys.safe(filename)
	return path.Join(keys.UserPrefix(ca, email), filename+extension)
}

// emailUsername returns the username portion of an email address (part before
// '@') or the original input if it can't find the "@" symbol.
func (keys KeyBuilder) emailUsername(email string) string {
	at := strings.Index(email, "@")
	if at == -1 {
		return email
	} else if at == 0 {
		return email[1:]
	}
	return email[:at]
}

// safe standardizes and sanitizes str for use as
// a storage key. This method is idempotent.
func (keys KeyBuilder) safe(str string) string {
	str = strings.ToLower(str)
	str = strings.TrimSpace(str)

	// replace a few specific characters
	repl := strings.NewReplacer(
		" ", "_",
		"+", "_plus_",
		"*", "wildcard_",
		"..", "", // prevent directory traversal (regex allows single dots)
	)
	str = repl.Replace(str)

	// finally remove all non-word characters
	return safeKeyRE.ReplaceAllLiteralString(str, "")
}

// StorageKeys provides methods for accessing
// keys and key prefixes for items in a Storage.
// Typically, you will not need to use this
// because accessing storage is abstracted away
// for most cases. Only use this if you need to
// directly access TLS assets in your application.
var StorageKeys KeyBuilder

const (
	prefixACME = "acme"
	prefixOCSP = "ocsp"
)

// safeKeyRE matches any undesirable characters in storage keys.
// Note that this allows dots, so you'll have to strip ".." manually.
var safeKeyRE = regexp.MustCompile(`[^\w@.-]`)

// ErrNotExist is returned by Storage implementations when
// a resource is not found. It is similar to os.IsNotExist
// except this is a type, not a variable.
type ErrNotExist interface {
	error
}

// defaultFileStorage is a convenient, default storage
// implementation using the local file system.
var defaultFileStorage = FileStorage{Path: dataDir()}

// DefaultStorage is the default Storage implementation.
var DefaultStorage Storage = defaultFileStorage
