// Copyright 2017 clair authors
//
// 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 database defines the Clair's models and a common interface for
// database implementations.
package database

import (
	"errors"
	"fmt"
	"time"
)

var (
	// ErrBackendException is an error that occurs when the database backend does
	// not work properly (ie. unreachable).
	ErrBackendException = errors.New("database: an error occured when querying the backend")

	// ErrInconsistent is an error that occurs when a database consistency check
	// fails (i.e. when an entity which is supposed to be unique is detected
	// twice)
	ErrInconsistent = errors.New("database: inconsistent database")
)

// RegistrableComponentConfig is a configuration block that can be used to
// determine which registrable component should be initialized and pass custom
// configuration to it.
type RegistrableComponentConfig struct {
	Type    string
	Options map[string]interface{}
}

var drivers = make(map[string]Driver)

// Driver is a function that opens a Datastore specified by its database driver type and specific
// configuration.
type Driver func(RegistrableComponentConfig) (Datastore, error)

// Register makes a Constructor available by the provided name.
//
// If this function is called twice with the same name or if the Constructor is
// nil, it panics.
func Register(name string, driver Driver) {
	if driver == nil {
		panic("database: could not register nil Driver")
	}
	if _, dup := drivers[name]; dup {
		panic("database: could not register duplicate Driver: " + name)
	}
	drivers[name] = driver
}

// Open opens a Datastore specified by a configuration.
func Open(cfg RegistrableComponentConfig) (Datastore, error) {
	driver, ok := drivers[cfg.Type]
	if !ok {
		return nil, fmt.Errorf("database: unknown Driver %q (forgotten configuration or import?)", cfg.Type)
	}
	return driver(cfg)
}

// Datastore represents the required operations on a persistent data store for
// a Clair deployment.
type Datastore interface {
	// ListNamespaces returns the entire list of known Namespaces.
	ListNamespaces() ([]Namespace, error)

	// InsertLayer stores a Layer in the database.
	//
	// A Layer is uniquely identified by its Name.
	// The Name and EngineVersion fields are mandatory.
	// If a Parent is specified, it is expected that it has been retrieved using
	// FindLayer.
	// If a Layer that already exists is inserted and the EngineVersion of the
	// given Layer is higher than the stored one, the stored Layer should be
	// updated.
	// The function has to be idempotent, inserting a layer that already exists
	// shouldn't return an error.
	InsertLayer(Layer) error

	// FindLayer retrieves a Layer from the database.
	//
	// When `withFeatures` is true, the Features field should be filled.
	// When `withVulnerabilities` is true, the Features field should be filled
	// and their AffectedBy fields should contain every vulnerabilities that
	// affect them.
	FindLayer(name string, withFeatures, withVulnerabilities bool) (Layer, error)

	// DeleteLayer deletes a Layer from the database and every layers that are
	// based on it, recursively.
	DeleteLayer(name string) error

	// ListVulnerabilities returns the list of vulnerabilities of a particular
	// Namespace.
	//
	// The Limit and page parameters are used to paginate the return list.
	// The first given page should be 0.
	// The function should return the next available page. If there are no more
	// pages, -1 has to be returned.
	ListVulnerabilities(namespaceName string, limit int, page int) ([]Vulnerability, int, error)

	// InsertVulnerabilities stores the given Vulnerabilities in the database,
	// updating them if necessary.
	//
	// A vulnerability is uniquely identified by its Namespace and its Name.
	// The FixedIn field may only contain a partial list of Features that are
	// affected by the Vulnerability, along with the version in which the
	// vulnerability is fixed. It is the responsibility of the implementation to
	// update the list properly.
	// A version equals to versionfmt.MinVersion means that the given Feature is
	// not being affected by the Vulnerability at all and thus, should be removed
	// from the list.
	// It is important that Features should be unique in the FixedIn list. For
	// example, it doesn't make sense to have two `openssl` Feature listed as a
	// Vulnerability can only be fixed in one Version. This is true because
	// Vulnerabilities and Features are namespaced (i.e. specific to one
	// operating system).
	// Each vulnerability insertion or update has to create a Notification that
	// will contain the old and the updated Vulnerability, unless
	// createNotification equals to true.
	InsertVulnerabilities(vulnerabilities []Vulnerability, createNotification bool) error

	// FindVulnerability retrieves a Vulnerability from the database, including
	// the FixedIn list.
	FindVulnerability(namespaceName, name string) (Vulnerability, error)

	// DeleteVulnerability removes a Vulnerability from the database.
	//
	// It has to create a Notification that will contain the old Vulnerability.
	DeleteVulnerability(namespaceName, name string) error

	// InsertVulnerabilityFixes adds new FixedIn Feature or update the Versions
	// of existing ones to the specified Vulnerability in the database.
	//
	// It has has to create a Notification that will contain the old and the
	// updated Vulnerability.
	InsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName string, fixes []FeatureVersion) error

	// DeleteVulnerabilityFix removes a FixedIn Feature from the specified
	// Vulnerability in the database. It can be used to store the fact that a
	// Vulnerability no longer affects the given Feature in any Version.
	//
	// It has has to create a Notification that will contain the old and the
	// updated Vulnerability.
	DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName string) error

	// GetAvailableNotification returns the Name, Created, Notified and Deleted
	// fields of a Notification that should be handled.
	//
	// The renotify interval defines how much time after being marked as Notified
	// by SetNotificationNotified, a Notification that hasn't been deleted should
	// be returned again by this function.
	// A Notification for which there is a valid Lock with the same Name should
	// not be returned.
	GetAvailableNotification(renotifyInterval time.Duration) (VulnerabilityNotification, error)

	// GetNotification returns a Notification, including its OldVulnerability and
	// NewVulnerability fields.
	//
	// On these Vulnerabilities, LayersIntroducingVulnerability should be filled
	// with every Layer that introduces the Vulnerability (i.e. adds at least one
	// affected FeatureVersion).
	// The Limit and page parameters are used to paginate
	// LayersIntroducingVulnerability. The first given page should be
	// VulnerabilityNotificationFirstPage. The function will then return the next
	// available page. If there is no more page, NoVulnerabilityNotificationPage
	// has to be returned.
	GetNotification(name string, limit int, page VulnerabilityNotificationPageNumber) (VulnerabilityNotification, VulnerabilityNotificationPageNumber, error)

	// SetNotificationNotified marks a Notification as notified and thus, makes
	// it unavailable for GetAvailableNotification, until the renotify duration
	// is elapsed.
	SetNotificationNotified(name string) error

	// DeleteNotification marks a Notification as deleted, and thus, makes it
	// unavailable for GetAvailableNotification.
	DeleteNotification(name string) error

	// InsertKeyValue stores or updates a simple key/value pair in the database.
	InsertKeyValue(key, value string) error

	// GetKeyValue retrieves a value from the database from the given key.
	//
	// It returns an empty string if there is no such key.
	GetKeyValue(key string) (string, error)

	// Lock creates or renew a Lock in the database with the given name, owner
	// and duration.
	//
	// After the specified duration, the Lock expires by itself if it hasn't been
	// unlocked, and thus, let other users create a Lock with the same name.
	// However, the owner can renew its Lock by setting renew to true.
	// Lock should not block, it should instead returns whether the Lock has been
	// successfully acquired/renewed. If it's the case, the expiration time of
	// that Lock is returned as well.
	Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time)

	// Unlock releases an existing Lock.
	Unlock(name, owner string)

	// FindLock returns the owner of a Lock specified by the name, and its
	// expiration time if it exists.
	FindLock(name string) (string, time.Time, error)

	// Ping returns the health status of the database.
	Ping() bool

	// Close closes the database and frees any allocated resource.
	Close()
}