/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright 2021 The LibreGraph Authors.
 */

// Package ldbbolt provides the lower-level Database functions for managing LDAP Entries
// in a	BoltDB database. Some implementation details:
//
// # The database is currently separated in these three buckets
//
//   - id2entry: This bucket contains the GOB encoded ldap.Entry instances keyed
//     by a unique 64bit ID
//
//   - dn2id: This bucket is used as an index to lookup the ID of an entry by its DN. The DN
//     is used in an normalized (case-folded) form here.
//
//   - id2children: This bucket uses the entry-ids as and index and the values contain a list
//     of the entry ids of its direct childdren
//
// Additional buckets will likely be added in the future to create efficient search indexes
package ldbbolt

import (
	"bytes"
	"encoding/binary"
	"encoding/gob"
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/go-ldap/ldap/v3"
	"github.com/sirupsen/logrus"
	bolt "go.etcd.io/bbolt"

	"github.com/libregraph/idm/pkg/ldapdn"
	"github.com/libregraph/idm/pkg/ldapentry"
	"github.com/libregraph/idm/pkg/ldappassword"
)

type LdbBolt struct {
	logger  logrus.FieldLogger
	db      *bolt.DB
	options *bolt.Options
	base    string
}

var (
	ErrEntryAlreadyExists = errors.New("entry already exists")
	ErrEntryNotFound      = errors.New("entry does not exist")
	ErrNonLeafEntry       = errors.New("entry is not a leaf entry")
)

func (bdb *LdbBolt) Configure(logger logrus.FieldLogger, baseDN, dbfile string, options *bolt.Options) error {
	bdb.logger = logger
	logger = logger.WithField("db", dbfile)
	logger.Debug("Open boltdb")
	db, err := bolt.Open(dbfile, 0o600, options)
	if err != nil {
		logger.WithError(err).Error("Error opening database")
		return err
	}
	bdb.db = db
	bdb.options = options
	bdb.base, _ = ldapdn.ParseNormalize(baseDN)
	return nil
}

// Initialize() opens the Database file and create the required buckets if they do not
// exist yet. After calling initialize the database is ready to process transactions
func (bdb *LdbBolt) Initialize() error {
	var err error
	logger := bdb.logger.WithField("db", bdb.db.Path())
	if bdb.options == nil || !bdb.options.ReadOnly {
		logger.Debug("Adding default buckets")
		err = bdb.db.Update(func(tx *bolt.Tx) error {
			_, err = tx.CreateBucketIfNotExists([]byte("dn2id"))
			if err != nil {
				return fmt.Errorf("create bucket 'dn2id': %w", err)
			}
			_, err = tx.CreateBucketIfNotExists([]byte("id2children"))
			if err != nil {
				return fmt.Errorf("create bucket 'dn2id': %w", err)
			}
			_, err = tx.CreateBucketIfNotExists([]byte("id2entry"))
			if err != nil {
				return fmt.Errorf("create bucket 'id2entry': %w", err)
			}
			return nil
		})
		if err != nil {
			logger.WithError(err).Error("Error creating default buckets")
		}
	}
	return err
}

// Performs basic LDAP searches, using the dn2id and id2children buckets to generate
// a list of Result entries. Currently this does strip of the non-request attribute
// Neither does it support LDAP filters. For now we rely on the frontent (LDAPServer)
// to both.
func (bdb *LdbBolt) Search(base string, scope int) ([]*ldap.Entry, error) {
	entries := []*ldap.Entry{}
	nDN, err := ldapdn.ParseNormalize(base)
	if err != nil {
		return entries, err
	}

	err = bdb.db.View(func(tx *bolt.Tx) error {
		entryID := bdb.getIDByDN(tx, nDN)
		var entryIDs []uint64
		if entryID == 0 {
			return fmt.Errorf("not found")
		}
		switch scope {
		case ldap.ScopeBaseObject:
			entryIDs = append(entryIDs, entryID)
		case ldap.ScopeSingleLevel:
			entryIDs = bdb.getChildrenIDs(tx, entryID)
		case ldap.ScopeWholeSubtree:
			entryIDs = append(entryIDs, entryID)
			entryIDs = append(entryIDs, bdb.getSubtreeIDs(tx, entryID)...)
		}
		for _, id := range entryIDs {
			entry, err := bdb.getEntryByID(tx, id)
			if err != nil {
				return err
			}
			entries = append(entries, entry)
		}
		return nil
	})
	return entries, err
}

func idToBytes(id uint64) []byte {
	b := make([]byte, 8)
	binary.LittleEndian.PutUint64(b, id)
	return b
}

func (bdb *LdbBolt) getChildrenIDs(tx *bolt.Tx, parent uint64) []uint64 {
	id2Children := tx.Bucket([]byte("id2children"))
	children := id2Children.Get(idToBytes(parent))
	r := bytes.NewReader(children)
	ids := make([]uint64, len(children)/8)
	if err := binary.Read(r, binary.LittleEndian, &ids); err != nil {
		bdb.logger.Error(err)
	}
	// This logging it too verbose even for the "debug" level. Leaving
	// it here commented out as it can be helpful during development.
	// bdb.logger.WithFields(logrus.Fields{
	// 	"parentid": parent,
	// 	"children": ids,
	// }).Debug("getChildrenIDs")
	return ids
}

func (bdb *LdbBolt) getSubtreeIDs(tx *bolt.Tx, root uint64) []uint64 {
	var res []uint64
	children := bdb.getChildrenIDs(tx, root)
	res = append(res, children...)
	for _, child := range children {
		res = append(res, bdb.getSubtreeIDs(tx, child)...)
	}
	// This logging it too verbose even for the "debug" level. Leaving
	// it here commented out as it can be helpful during development.
	// bdb.logger.WithFields(logrus.Fields{
	// 	"rootid":  root,
	// 	"subtree": res,
	// }).Debug("getSubtreeIDs")
	return res
}

func (bdb *LdbBolt) EntryPut(e *ldap.Entry) error {
	var buf bytes.Buffer
	enc := gob.NewEncoder(&buf)
	if err := enc.Encode(e); err != nil {
		fmt.Printf("%v\n", err)
		panic(err)
	}

	dn, _ := ldap.ParseDN(e.DN)
	parentDN := &ldap.DN{
		RDNs: dn.RDNs[1:],
	}
	nDN := ldapdn.Normalize(dn)

	if !strings.HasSuffix(nDN, bdb.base) {
		return fmt.Errorf("'%s' is not a descendant of '%s'", e.DN, bdb.base)
	}

	nParentDN := ldapdn.Normalize(parentDN)
	err := bdb.db.Update(func(tx *bolt.Tx) error {
		id2entry := tx.Bucket([]byte("id2entry"))
		id := bdb.getIDByDN(tx, nDN)
		if id != 0 {
			return ErrEntryAlreadyExists
		}
		var err error
		if id, err = id2entry.NextSequence(); err != nil {
			return err
		}

		if err := id2entry.Put(idToBytes(id), buf.Bytes()); err != nil {
			return err
		}
		if nDN != bdb.base {
			if err := bdb.addID2Children(tx, nParentDN, id); err != nil {
				return err
			}
		}
		dn2id := tx.Bucket([]byte("dn2id"))
		if err := dn2id.Put([]byte(nDN), idToBytes(id)); err != nil {
			return err
		}
		return nil
	})
	return err
}

func (bdb *LdbBolt) EntryDelete(dn string) error {
	parsed, err := ldap.ParseDN(dn)
	if err != nil {
		return err
	}
	pparentDN := &ldap.DN{
		RDNs: parsed.RDNs[1:],
	}
	pdn := ldapdn.Normalize(pparentDN)

	ndn := ldapdn.Normalize(parsed)
	err = bdb.db.Update(func(tx *bolt.Tx) error {
		// Does this entry even exist?
		entryID := bdb.getIDByDN(tx, ndn)
		if entryID == 0 {
			return ErrEntryNotFound
		}

		// Refuse to delete if the entry has childs
		id2Children := tx.Bucket([]byte("id2children"))
		children := id2Children.Get(idToBytes(entryID))
		if len(children) != 0 {
			return ErrNonLeafEntry
		}

		// Update id2children bucket (remove entryid from parent)
		parentid := bdb.getIDByDN(tx, pdn)
		if parentid == 0 {
			return ErrEntryNotFound
		}
		children = id2Children.Get(idToBytes(parentid))
		r := bytes.NewReader(children)
		var newids []byte
		idBytes := make([]byte, 8)
		for _, err = io.ReadFull(r, idBytes); err == nil; _, err = io.ReadFull(r, idBytes) {
			if entryID != binary.LittleEndian.Uint64(idBytes) {
				newids = append(newids, idBytes...)
			}
		}
		if err = id2Children.Put(idToBytes(parentid), newids); err != nil {
			return fmt.Errorf("error updating id2Children index for %d: %w", parentid, err)
		}

		// Remove entry from dn2id bucket
		dn2id := tx.Bucket([]byte("dn2id"))
		err = dn2id.Delete([]byte(ndn))
		if err != nil {
			return err
		}
		id2entry := tx.Bucket([]byte("id2entry"))
		err = id2entry.Delete(idToBytes(entryID))
		if err != nil {
			return err
		}

		return nil
	})
	return err
}

func (bdb *LdbBolt) EntryModify(req *ldap.ModifyRequest) error {
	ndn, err := ldapdn.ParseNormalize(req.DN)
	if err != nil {
		return err
	}
	err = bdb.db.Update(func(tx *bolt.Tx) error {
		oldEntry, id, innerErr := bdb.getEntryByDN(tx, ndn)
		if innerErr != nil {
			return innerErr
		}
		return bdb.entryModifyWithTxn(tx, id, oldEntry, req)
	})
	return err
}

func (bdb *LdbBolt) entryModifyWithTxn(tx *bolt.Tx, id uint64, entry *ldap.Entry, req *ldap.ModifyRequest) error {
	newEntry, innerErr := ldapentry.ApplyModify(entry, req)
	if innerErr != nil {
		return innerErr
	}
	var buf bytes.Buffer
	enc := gob.NewEncoder(&buf)
	if innerErr := enc.Encode(newEntry); innerErr != nil {
		return innerErr
	}
	id2entry := tx.Bucket([]byte("id2entry"))
	if innerErr := id2entry.Put(idToBytes(id), buf.Bytes()); innerErr != nil {
		return innerErr
	}
	return nil
}

func (bdb *LdbBolt) EntryModifyDN(req *ldap.ModifyDNRequest) error {
	olddn, err := ldap.ParseDN(req.DN)
	if err != nil {
		return err
	}

	newrdn, err := ldap.ParseDN(req.NewRDN)
	if err != nil {
		return err
	}

	var newDN ldap.DN

	newDN.RDNs = []*ldap.RelativeDN{newrdn.RDNs[0]}
	newDN.RDNs = append(newDN.RDNs, olddn.RDNs[1:]...)

	err = bdb.db.Update(func(tx *bolt.Tx) error {
		flatNewDN := ldapdn.Normalize(&newDN)
		flatOldDN := ldapdn.Normalize(olddn)

		// error out if there is an entry with the new name already
		if id := bdb.getIDByDN(tx, flatNewDN); id != 0 {
			return ErrEntryAlreadyExists
		}

		entry, id, innerErr := bdb.getEntryByDN(tx, flatOldDN)
		if innerErr != nil {
			return innerErr
		}

		// only allow renaming leaf entries
		childIds := bdb.getChildrenIDs(tx, id)
		if len(childIds) > 0 {
			return ErrNonLeafEntry
		}

		entry.DN = flatNewDN

		modReq := ldap.ModifyRequest{
			DN: entry.DN,
		}

		// create modify operation for the change attribute values
		if req.DeleteOldRDN {
			oldRDN := olddn.RDNs[0]
			for _, ava := range oldRDN.Attributes {
				modReq.Delete(ava.Type, []string{ava.Value})
			}
		}
		for _, ava := range newrdn.RDNs[0].Attributes {
			modReq.Add(ava.Type, []string{ava.Value})
		}
		innerErr = bdb.entryModifyWithTxn(tx, id, entry, &modReq)
		if innerErr != nil {
			return innerErr
		}

		// update the dn2id index
		dn2id := tx.Bucket([]byte("dn2id"))
		if err := dn2id.Put([]byte(flatNewDN), idToBytes(id)); err != nil {
			return err
		}
		if err := dn2id.Delete([]byte(flatOldDN)); err != nil {
			return err
		}
		return nil
	})
	return err
}

func (bdb *LdbBolt) UpdatePassword(req *ldap.PasswordModifyRequest) error {
	ndn, err := ldapdn.ParseNormalize(req.UserIdentity)
	if err != nil {
		return err
	}

	err = bdb.db.Update(func(tx *bolt.Tx) error {
		userEntry, id, innerErr := bdb.getEntryByDN(tx, ndn)
		if innerErr != nil {
			return innerErr
		}
		// Note: the password check we perform here is more or less unneeded.
		// If the request got here it's either issued by the admin (which does
		// not need the old password to reset a users password) or a user trying
		// to update its own password. In which case the password is already verified
		// as we only allow authenticated users to issue this request. Still, if
		// the request contains an old password we verify it and error out if it
		// doesn't match.
		if req.OldPassword != "" {
			userPassword := userEntry.GetEqualFoldAttributeValue("userPassword")
			match, err := ldappassword.Validate(req.OldPassword, userPassword)
			if err != nil {
				bdb.logger.Error(err)
				return ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("Failed to validate old Password"))
			}
			if !match {
				bdb.logger.Debug("Old password does not match")
				return ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("Failed to validate old Password"))
			}
		}

		mod := ldap.ModifyRequest{}
		mod.DN = req.UserIdentity
		mod.Replace("userPassword", []string{req.NewPassword})
		innerErr = bdb.entryModifyWithTxn(tx, id, userEntry, &mod)
		if innerErr != nil {
			bdb.logger.Debugf("Failed to update password for '%s': '%s'", ndn, err)
			return ldap.NewError(ldap.LDAPResultOperationsError, errors.New("Failed to update Password"))
		}
		return nil
	})
	return err
}

func (bdb *LdbBolt) addID2Children(tx *bolt.Tx, nParentDN string, newChildID uint64) error {
	bdb.logger.Debugf("AddID2Children '%s' id '%d'", nParentDN, newChildID)
	parentID := bdb.getIDByDN(tx, nParentDN)
	if parentID == 0 {
		return fmt.Errorf("parent not found '%s'", nParentDN)
	}

	bdb.logger.Debugf("Parent ID: %v", parentID)

	id2Children := tx.Bucket([]byte("id2children"))
	// FIXME add sanity check here if ID is already present
	children := id2Children.Get(idToBytes(parentID))
	children = append(children, idToBytes(newChildID)...)
	if err := id2Children.Put(idToBytes(parentID), children); err != nil {
		return fmt.Errorf("error updating id2Children index for %d: %w", parentID, err)
	}

	bdb.logger.Debugf("AddID2Children '%d' id '%v'", parentID, children)
	return nil
}

func (bdb *LdbBolt) getIDByDN(tx *bolt.Tx, nDN string) uint64 {
	dn2id := tx.Bucket([]byte("dn2id"))
	if dn2id == nil {
		bdb.logger.Debugf("Bucket 'dn2id' does not exist")
		return 0
	}
	id := dn2id.Get([]byte(nDN))
	if id == nil {
		bdb.logger.Debugf("DN: '%s' not found", nDN)
		return 0
	}
	return binary.LittleEndian.Uint64(id)
}

func (bdb *LdbBolt) getEntryByID(tx *bolt.Tx, id uint64) (entry *ldap.Entry, err error) {
	id2entry := tx.Bucket([]byte("id2entry"))
	entrybytes := id2entry.Get(idToBytes(id))
	buf := bytes.NewBuffer(entrybytes)
	dec := gob.NewDecoder(buf)
	if err := dec.Decode(&entry); err != nil {
		return nil, fmt.Errorf("error decoding entry id: %d, %w", id, err)
	}
	return entry, nil
}

func (bdb *LdbBolt) getEntryByDN(tx *bolt.Tx, ndn string) (entry *ldap.Entry, id uint64, err error) {
	id = bdb.getIDByDN(tx, ndn)
	if id == 0 {
		return nil, id, ErrEntryNotFound
	}
	entry, err = bdb.getEntryByID(tx, id)
	return entry, id, err
}

func (bdb *LdbBolt) Close() {
	bdb.db.Close()
}
