package database

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/bluele/gcache"
	"github.com/tevino/abool"

	"github.com/safing/portmaster/base/database/accessor"
	"github.com/safing/portmaster/base/database/iterator"
	"github.com/safing/portmaster/base/database/query"
	"github.com/safing/portmaster/base/database/record"
)

const (
	getDBFromKey = ""
)

// Interface provides a method to access the database with attached options.
type Interface struct {
	options *Options
	cache   gcache.Cache

	writeCache        map[string]record.Record
	writeCacheLock    sync.Mutex
	triggerCacheWrite chan struct{}
}

// Options holds options that may be set for an Interface instance.
type Options struct {
	// Local specifies if the interface is used by an actor on the local device.
	// Setting both the Local and Internal flags will bring performance
	// improvements because less checks are needed.
	Local bool

	// Internal specifies if the interface is used by an actor within the
	// software. Setting both the Local and Internal flags will bring performance
	// improvements because less checks are needed.
	Internal bool

	// AlwaysMakeSecret will have the interface mark all saved records as secret.
	// This means that they will be only accessible by an internal interface.
	AlwaysMakeSecret bool

	// AlwaysMakeCrownjewel will have the interface mark all saved records as
	// crown jewels. This means that they will be only accessible by a local
	// interface.
	AlwaysMakeCrownjewel bool

	// AlwaysSetRelativateExpiry will have the interface set a relative expiry,
	// based on the current time, on all saved records.
	AlwaysSetRelativateExpiry int64

	// AlwaysSetAbsoluteExpiry will have the interface set an absolute expiry on
	// all saved records.
	AlwaysSetAbsoluteExpiry int64

	// CacheSize defines that a cache should be used for this interface and
	// defines it's size.
	// Caching comes with an important caveat: If database records are changed
	// from another interface, the cache will not be invalidated for these
	// records. It will therefore serve outdated data until that record is
	// evicted from the cache.
	CacheSize int

	// DelayCachedWrites defines a database name for which cache writes should
	// be cached and batched. The database backend must support the Batcher
	// interface. This option is only valid if used with a cache.
	// Additionally, this may only be used for internal and local interfaces.
	// Please note that this means that other interfaces will not be able to
	// guarantee to serve the latest record if records are written this way.
	DelayCachedWrites string
}

// Apply applies options to the record metadata.
func (o *Options) Apply(r record.Record) {
	r.UpdateMeta()
	if o.AlwaysMakeSecret {
		r.Meta().MakeSecret()
	}
	if o.AlwaysMakeCrownjewel {
		r.Meta().MakeCrownJewel()
	}
	if o.AlwaysSetAbsoluteExpiry > 0 {
		r.Meta().SetAbsoluteExpiry(o.AlwaysSetAbsoluteExpiry)
	} else if o.AlwaysSetRelativateExpiry > 0 {
		r.Meta().SetRelativateExpiry(o.AlwaysSetRelativateExpiry)
	}
}

// HasAllPermissions returns whether the options specify the highest possible
// permissions for operations.
func (o *Options) HasAllPermissions() bool {
	return o.Local && o.Internal
}

// hasAccessPermission checks if the interface options permit access to the
// given record, locking the record for accessing it's attributes.
func (o *Options) hasAccessPermission(r record.Record) bool {
	// Check if the options specify all permissions, which makes checking the
	// record unnecessary.
	if o.HasAllPermissions() {
		return true
	}

	r.Lock()
	defer r.Unlock()

	// Check permissions against record.
	return r.Meta().CheckPermission(o.Local, o.Internal)
}

// NewInterface returns a new Interface to the database.
func NewInterface(opts *Options) *Interface {
	if opts == nil {
		opts = &Options{}
	}

	newIface := &Interface{
		options: opts,
	}
	if opts.CacheSize > 0 {
		cacheBuilder := gcache.New(opts.CacheSize).ARC()
		if opts.DelayCachedWrites != "" {
			cacheBuilder.EvictedFunc(newIface.cacheEvictHandler)
			newIface.writeCache = make(map[string]record.Record, opts.CacheSize/2)
			newIface.triggerCacheWrite = make(chan struct{})
		}
		newIface.cache = cacheBuilder.Build()
	}
	return newIface
}

// Exists return whether a record with the given key exists.
func (i *Interface) Exists(key string) (bool, error) {
	_, err := i.Get(key)
	if err != nil {
		switch {
		case errors.Is(err, ErrNotFound):
			return false, nil
		case errors.Is(err, ErrPermissionDenied):
			return true, nil
		default:
			return false, err
		}
	}
	return true, nil
}

// Get return the record with the given key.
func (i *Interface) Get(key string) (record.Record, error) {
	r, _, err := i.getRecord(getDBFromKey, key, false)
	return r, err
}

func (i *Interface) getRecord(dbName string, dbKey string, mustBeWriteable bool) (r record.Record, db *Controller, err error) { //nolint:unparam
	if dbName == "" {
		dbName, dbKey = record.ParseKey(dbKey)
	}

	db, err = getController(dbName)
	if err != nil {
		return nil, nil, err
	}

	if mustBeWriteable && db.ReadOnly() {
		return nil, db, ErrReadOnly
	}

	r = i.checkCache(dbName + ":" + dbKey)
	if r != nil {
		if !i.options.hasAccessPermission(r) {
			return nil, db, ErrPermissionDenied
		}
		return r, db, nil
	}

	r, err = db.Get(dbKey)
	if err != nil {
		return nil, db, err
	}

	if !i.options.hasAccessPermission(r) {
		return nil, db, ErrPermissionDenied
	}

	r.Lock()
	ttl := r.Meta().GetRelativeExpiry()
	r.Unlock()
	i.updateCache(
		r,
		false, // writing
		false, // remove
		ttl,   // expiry
	)

	return r, db, nil
}

func (i *Interface) getMeta(dbName string, dbKey string, mustBeWriteable bool) (m *record.Meta, db *Controller, err error) { //nolint:unparam
	if dbName == "" {
		dbName, dbKey = record.ParseKey(dbKey)
	}

	db, err = getController(dbName)
	if err != nil {
		return nil, nil, err
	}

	if mustBeWriteable && db.ReadOnly() {
		return nil, db, ErrReadOnly
	}

	r := i.checkCache(dbName + ":" + dbKey)
	if r != nil {
		if !i.options.hasAccessPermission(r) {
			return nil, db, ErrPermissionDenied
		}
		return r.Meta(), db, nil
	}

	m, err = db.GetMeta(dbKey)
	if err != nil {
		return nil, db, err
	}

	if !m.CheckPermission(i.options.Local, i.options.Internal) {
		return nil, db, ErrPermissionDenied
	}

	return m, db, nil
}

// InsertValue inserts a value into a record.
func (i *Interface) InsertValue(key string, attribute string, value interface{}) error {
	r, db, err := i.getRecord(getDBFromKey, key, true)
	if err != nil {
		return err
	}

	r.Lock()
	defer r.Unlock()

	var acc accessor.Accessor
	if r.IsWrapped() {
		wrapper, ok := r.(*record.Wrapper)
		if !ok {
			return errors.New("record is malformed (reports to be wrapped but is not of type *record.Wrapper)")
		}
		acc = accessor.NewJSONBytesAccessor(&wrapper.Data)
	} else {
		acc = accessor.NewStructAccessor(r)
	}

	err = acc.Set(attribute, value)
	if err != nil {
		return fmt.Errorf("failed to set value with %s: %w", acc.Type(), err)
	}

	i.options.Apply(r)
	return db.Put(r)
}

// Put saves a record to the database.
func (i *Interface) Put(r record.Record) (err error) {
	// get record or only database
	var db *Controller
	if !i.options.HasAllPermissions() {
		_, db, err = i.getMeta(r.DatabaseName(), r.DatabaseKey(), true)
		if err != nil && !errors.Is(err, ErrNotFound) {
			return err
		}
	} else {
		db, err = getController(r.DatabaseName())
		if err != nil {
			return err
		}
	}

	// Check if database is read only.
	if db.ReadOnly() {
		return ErrReadOnly
	}

	r.Lock()
	i.options.Apply(r)
	remove := r.Meta().IsDeleted()
	ttl := r.Meta().GetRelativeExpiry()
	r.Unlock()

	// The record may not be locked when updating the cache.
	written := i.updateCache(r, true, remove, ttl)
	if written {
		return nil
	}

	r.Lock()
	defer r.Unlock()
	return db.Put(r)
}

// PutNew saves a record to the database as a new record (ie. with new timestamps).
func (i *Interface) PutNew(r record.Record) (err error) {
	// get record or only database
	var db *Controller
	if !i.options.HasAllPermissions() {
		_, db, err = i.getMeta(r.DatabaseName(), r.DatabaseKey(), true)
		if err != nil && !errors.Is(err, ErrNotFound) {
			return err
		}
	} else {
		db, err = getController(r.DatabaseName())
		if err != nil {
			return err
		}
	}

	// Check if database is read only.
	if db.ReadOnly() {
		return ErrReadOnly
	}

	r.Lock()
	if r.Meta() != nil {
		r.Meta().Reset()
	}
	i.options.Apply(r)
	remove := r.Meta().IsDeleted()
	ttl := r.Meta().GetRelativeExpiry()
	r.Unlock()

	// The record may not be locked when updating the cache.
	written := i.updateCache(r, true, remove, ttl)
	if written {
		return nil
	}

	r.Lock()
	defer r.Unlock()
	return db.Put(r)
}

// PutMany stores many records in the database.
// Warning: This is nearly a direct database access and omits many things:
// - Record locking
// - Hooks
// - Subscriptions
// - Caching
// Use with care.
func (i *Interface) PutMany(dbName string) (put func(record.Record) error) {
	interfaceBatch := make(chan record.Record, 100)

	// permission check
	if !i.options.HasAllPermissions() {
		return func(r record.Record) error {
			return ErrPermissionDenied
		}
	}

	// get database
	db, err := getController(dbName)
	if err != nil {
		return func(r record.Record) error {
			return err
		}
	}

	// Check if database is read only.
	if db.ReadOnly() {
		return func(r record.Record) error {
			return ErrReadOnly
		}
	}

	// start database access
	dbBatch, errs := db.PutMany()
	finished := abool.New()
	var internalErr error

	// interface options proxy
	go func() {
		defer close(dbBatch) // signify that we are finished
		for {
			select {
			case r := <-interfaceBatch:
				// finished?
				if r == nil {
					return
				}
				// apply options
				i.options.Apply(r)
				// pass along
				dbBatch <- r
			case <-time.After(1 * time.Second):
				// bail out
				internalErr = errors.New("timeout: putmany unused for too long")
				finished.Set()
				return
			}
		}
	}()

	return func(r record.Record) error {
		// finished?
		if finished.IsSet() {
			// check for internal error
			if internalErr != nil {
				return internalErr
			}
			// check for previous error
			select {
			case err := <-errs:
				return err
			default:
				return errors.New("batch is closed")
			}
		}

		// finish?
		if r == nil {
			finished.Set()
			interfaceBatch <- nil // signify that we are finished
			// do not close, as this fn could be called again with nil.
			return <-errs
		}

		// check record scope
		if r.DatabaseName() != dbName {
			return errors.New("record out of database scope")
		}

		// submit
		select {
		case interfaceBatch <- r:
			return nil
		case err := <-errs:
			return err
		}
	}
}

// SetAbsoluteExpiry sets an absolute record expiry.
func (i *Interface) SetAbsoluteExpiry(key string, time int64) error {
	r, db, err := i.getRecord(getDBFromKey, key, true)
	if err != nil {
		return err
	}

	r.Lock()
	defer r.Unlock()

	i.options.Apply(r)
	r.Meta().SetAbsoluteExpiry(time)
	return db.Put(r)
}

// SetRelativateExpiry sets a relative (self-updating) record expiry.
func (i *Interface) SetRelativateExpiry(key string, duration int64) error {
	r, db, err := i.getRecord(getDBFromKey, key, true)
	if err != nil {
		return err
	}

	r.Lock()
	defer r.Unlock()

	i.options.Apply(r)
	r.Meta().SetRelativateExpiry(duration)
	return db.Put(r)
}

// MakeSecret marks the record as a secret, meaning interfacing processes, such as an UI, are denied access to the record.
func (i *Interface) MakeSecret(key string) error {
	r, db, err := i.getRecord(getDBFromKey, key, true)
	if err != nil {
		return err
	}

	r.Lock()
	defer r.Unlock()

	i.options.Apply(r)
	r.Meta().MakeSecret()
	return db.Put(r)
}

// MakeCrownJewel marks a record as a crown jewel, meaning it will only be accessible locally.
func (i *Interface) MakeCrownJewel(key string) error {
	r, db, err := i.getRecord(getDBFromKey, key, true)
	if err != nil {
		return err
	}

	r.Lock()
	defer r.Unlock()

	i.options.Apply(r)
	r.Meta().MakeCrownJewel()
	return db.Put(r)
}

// Delete deletes a record from the database.
func (i *Interface) Delete(key string) error {
	r, db, err := i.getRecord(getDBFromKey, key, true)
	if err != nil {
		return err
	}

	// Check if database is read only.
	if db.ReadOnly() {
		return ErrReadOnly
	}

	i.options.Apply(r)
	r.Meta().Delete()
	return db.Put(r)
}

// Query executes the given query on the database.
// Will not see data that is in the write cache, waiting to be written.
// Use with care with caching.
func (i *Interface) Query(q *query.Query) (*iterator.Iterator, error) {
	_, err := q.Check()
	if err != nil {
		return nil, err
	}

	db, err := getController(q.DatabaseName())
	if err != nil {
		return nil, err
	}

	// TODO: Finish caching system integration.
	// Flush the cache before we query the database.
	// i.FlushCache()

	return db.Query(q, i.options.Local, i.options.Internal)
}

// Purge deletes all records that match the given query. It returns the number
// of successful deletes and an error.
func (i *Interface) Purge(ctx context.Context, q *query.Query) (int, error) {
	_, err := q.Check()
	if err != nil {
		return 0, err
	}

	db, err := getController(q.DatabaseName())
	if err != nil {
		return 0, err
	}

	// Check if database is read only before we add to the cache.
	if db.ReadOnly() {
		return 0, ErrReadOnly
	}

	return db.Purge(ctx, q, i.options.Local, i.options.Internal)
}

// PurgeOlderThan deletes all records last updated before the given time.
// It returns the number of successful deletes and an error.
func (i *Interface) PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time) (int, error) {
	dbName, dbKeyPrefix := record.ParseKey(prefix)
	if dbName == "" {
		return 0, errors.New("unknown database")
	}

	db, err := getController(dbName)
	if err != nil {
		return 0, err
	}

	// Check if database is read only before we add to the cache.
	if db.ReadOnly() {
		return 0, ErrReadOnly
	}

	return db.PurgeOlderThan(ctx, dbKeyPrefix, purgeBefore, i.options.Local, i.options.Internal)
}

// Subscribe subscribes to updates matching the given query.
func (i *Interface) Subscribe(q *query.Query) (*Subscription, error) {
	_, err := q.Check()
	if err != nil {
		return nil, err
	}

	c, err := getController(q.DatabaseName())
	if err != nil {
		return nil, err
	}

	sub := &Subscription{
		q:        q,
		local:    i.options.Local,
		internal: i.options.Internal,
		Feed:     make(chan record.Record, 1000),
	}
	c.addSubscription(sub)
	return sub, nil
}
