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

package boltdb

import (
	"context"
	"errors"
	"fmt"
	"net"
	"path/filepath"
	"strings"

	"github.com/go-ldap/ldap/v3"
	"github.com/sirupsen/logrus"

	"github.com/libregraph/idm/pkg/ldapdn"
	"github.com/libregraph/idm/pkg/ldapentry"
	"github.com/libregraph/idm/pkg/ldappassword"
	"github.com/libregraph/idm/pkg/ldapserver"
	"github.com/libregraph/idm/pkg/ldbbolt"
	"github.com/libregraph/idm/server/handler"
)

type boltdbHandler struct {
	logger                  logrus.FieldLogger
	dbfile                  string
	baseDN                  string
	adminDN                 string
	allowLocalAnonymousBind bool
	ctx                     context.Context
	bdb                     *ldbbolt.LdbBolt
}

type Options struct {
	BaseDN  string
	AdminDN string

	AllowLocalAnonymousBind bool
}

func NewBoltDBHandler(logger logrus.FieldLogger, fn string, options *Options) (handler.Handler, error) {
	if fn == "" {
		return nil, fmt.Errorf("file name is empty")
	}
	if options.BaseDN == "" {
		return nil, fmt.Errorf("base dn is empty")
	}

	fn, err := filepath.Abs(fn)
	if err != nil {
		return nil, err
	}

	h := &boltdbHandler{
		logger: logger,
		dbfile: fn,

		allowLocalAnonymousBind: options.AllowLocalAnonymousBind,
		ctx:                     context.Background(),
	}
	if h.baseDN, err = ldapdn.ParseNormalize(options.BaseDN); err != nil {
		return nil, err
	}
	if h.adminDN, err = ldapdn.ParseNormalize(options.AdminDN); err != nil {
		return nil, err
	}

	err = h.setup()
	if err != nil {
		return nil, err
	}
	return h, nil
}

func (h *boltdbHandler) setup() error {
	bdb := &ldbbolt.LdbBolt{}

	if err := bdb.Configure(h.logger, h.baseDN, h.dbfile, nil); err != nil {
		return err
	}

	if err := bdb.Initialize(); err != nil {
		return err
	}
	h.bdb = bdb
	return nil
}

func (h *boltdbHandler) Add(boundDN string, req *ldap.AddRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
	logger := h.logger.WithFields(logrus.Fields{
		"op":          "add",
		"bind_dn":     boundDN,
		"remote_addr": conn.RemoteAddr().String(),
	})

	if !h.writeAllowed(boundDN) {
		return ldap.LDAPResultInsufficientAccessRights, nil
	}

	e := ldapentry.EntryFromAddRequest(req)

	if err := h.bdb.EntryPut(e); err != nil {
		logger.WithError(err).WithField("entrydn", e.DN).Debugln("ldap add failed")
		if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
			return ldap.LDAPResultEntryAlreadyExists, nil
		}
		return ldap.LDAPResultUnwillingToPerform, err
	}
	return ldap.LDAPResultSuccess, nil
}

func (h *boltdbHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldapserver.LDAPResultCode, error) {
	logger := h.logger.WithFields(logrus.Fields{
		"op":          "bind",
		"bind_dn":     bindDN,
		"remote_addr": conn.RemoteAddr().String(),
	})

	// Handle anoymous bind
	if bindDN == "" {
		if !h.allowLocalAnonymousBind {
			logger.Debugln("ldap anonymous Bind disabled")
			return ldap.LDAPResultInvalidCredentials, nil
		} else if bindSimplePw == "" {
			return ldap.LDAPResultSuccess, nil
		}
	}

	bindDN, err := ldapdn.ParseNormalize(bindDN)
	if err != nil {
		logger.WithError(err).Debugln("ldap bind request BindDN validation failed")
		return ldap.LDAPResultInvalidDNSyntax, nil
	}

	if !strings.HasSuffix(bindDN, h.baseDN) {
		logger.WithError(err).Debugln("ldap bind request BindDN outside of Database tree")
		return ldap.LDAPResultInvalidCredentials, nil
	}

	// Disallow empty password
	if bindSimplePw == "" {
		logger.Debugf("BindDN without password")
		return ldap.LDAPResultInvalidCredentials, nil
	}

	return h.validatePassword(logger, bindDN, bindSimplePw)
}

func (h *boltdbHandler) validatePassword(logger logrus.FieldLogger, bindDN, bindSimplePw string) (ldapserver.LDAPResultCode, error) {
	// Lookup Bind DN in database
	entries, err := h.bdb.Search(bindDN, ldap.ScopeBaseObject)
	if err != nil || len(entries) != 1 {
		if err != nil {
			logger.Error(err)
		}
		if len(entries) != 1 {
			logger.Debugf("Entry '%s' does not exist", bindDN)
		}
		return ldap.LDAPResultInvalidCredentials, nil
	}
	userPassword := entries[0].GetEqualFoldAttributeValue("userPassword")
	match, err := ldappassword.Validate(bindSimplePw, userPassword)
	if err != nil {
		logger.Error(err)
		return ldap.LDAPResultInvalidCredentials, nil
	}
	if match {
		logger.Debug("success")
		return ldap.LDAPResultSuccess, nil
	}
	return ldap.LDAPResultInvalidCredentials, nil
}

func (h *boltdbHandler) Delete(boundDN string, req *ldap.DelRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
	logger := h.logger.WithFields(logrus.Fields{
		"op":          "delete",
		"bind_dn":     boundDN,
		"remote_addr": conn.RemoteAddr().String(),
	})

	if !h.writeAllowed(boundDN) {
		return ldap.LDAPResultInsufficientAccessRights, nil
	}

	logger.Debug("Calling boltdb delete")
	if err := h.bdb.EntryDelete(req.DN); err != nil {
		logger.WithError(err).WithField("entrydn", req.DN).Debugln("ldap delete failed")
		if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
			return ldap.LDAPResultEntryAlreadyExists, nil
		}
		return ldap.LDAPResultUnwillingToPerform, err
	}
	logger.Debug("delete succeeded")
	return ldap.LDAPResultSuccess, nil
}

func (h *boltdbHandler) Modify(boundDN string, req *ldap.ModifyRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
	logger := h.logger.WithFields(logrus.Fields{
		"op":          "modify",
		"bind_dn":     boundDN,
		"remote_addr": conn.RemoteAddr().String(),
		"entrydn":     req.DN,
	})

	if !h.writeAllowed(boundDN) {
		return ldap.LDAPResultInsufficientAccessRights, nil
	}

	logger.Debug("Calling boltdb modify")
	if err := h.bdb.EntryModify(req); err != nil {
		logger.WithError(err).Debug("ldap modify failed")
		if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
			return ldap.LDAPResultEntryAlreadyExists, nil
		}
		ldapError, ok := err.(*ldap.Error)
		if !ok {
			return ldap.LDAPResultUnwillingToPerform, err
		}
		return ldapserver.LDAPResultCode(ldapError.ResultCode), ldapError.Err
	}
	logger.Debug("modify succeeded")
	return ldap.LDAPResultSuccess, nil
}

func (h *boltdbHandler) ModifyDN(boundDN string, req *ldap.ModifyDNRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
	logger := h.logger.WithFields(logrus.Fields{
		"op":          "modifyDN",
		"bind_dn":     boundDN,
		"remote_addr": conn.RemoteAddr().String(),
		"entrydn":     req.DN,
	})

	if !h.writeAllowed(boundDN) {
		return ldap.LDAPResultInsufficientAccessRights, nil
	}
	logger.Debug("Calling boltdb modify DN")
	if err := h.bdb.EntryModifyDN(req); err != nil {
		logger.WithError(err).Debug("ldap modifyDN failed")
		if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
			return ldap.LDAPResultEntryAlreadyExists, nil
		}
		ldapError, ok := err.(*ldap.Error)
		if !ok {
			return ldap.LDAPResultUnwillingToPerform, err
		}
		return ldapserver.LDAPResultCode(ldapError.ResultCode), ldapError.Err
	}
	logger.Debug("modify DN succeeded")
	return ldap.LDAPResultSuccess, nil
	return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
}

func (h *boltdbHandler) ModifyPasswordExop(boundDN string, req *ldap.PasswordModifyRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
	logger := h.logger.WithFields(logrus.Fields{
		"op":           "modpw_exop",
		"binddn":       boundDN,
		"UserIdentity": req.UserIdentity,
		"OldPWPresent": req.OldPassword != "",
		"NewPwPresent": req.NewPassword != "",
	})

	if boundDN == "" {
		return ldap.LDAPResultInsufficientAccessRights, nil
	}

	if req.UserIdentity != "" {
		pwUserDN, err := ldapdn.ParseNormalize(req.UserIdentity)
		if err != nil {
			return ldap.LDAPResultOperationsError, errors.New("Error parsing DN in password modify extended operation.")
		}
		if boundDN != pwUserDN && boundDN != h.adminDN {
			return ldap.LDAPResultInsufficientAccessRights, nil
		}
	}

	logger.Debug("Calling boltdb UpdatePassword")
	err := h.bdb.UpdatePassword(req)
	if err != nil {
		logger.Debugf("boltdb UpdatePassword returned '%s'", err)
		return ldap.LDAPResultOther, err
	}
	return ldap.LDAPResultSuccess, err
}

func (h *boltdbHandler) Search(boundDN string, req *ldap.SearchRequest, conn net.Conn) (ldapserver.ServerSearchResult, error) {
	logger := h.logger.WithFields(logrus.Fields{
		"op":     "search",
		"binddn": boundDN,
		"basedn": req.BaseDN,
		"filter": req.Filter,
		"attrs":  req.Attributes,
	})

	logger.Debug("Calling boltdb search")
	entries, _ := h.bdb.Search(req.BaseDN, req.Scope)
	logger.Debugf("boltdb search returned %d entries", len(entries))

	return ldapserver.ServerSearchResult{
		Entries:    entries,
		Referrals:  []string{},
		Controls:   []ldap.Control{},
		ResultCode: ldap.LDAPResultSuccess,
	}, nil
}

func (h *boltdbHandler) Close(boundDN string, conn net.Conn) error {
	return nil
}

func (h *boltdbHandler) WithContext(ctx context.Context) handler.Handler {
	if ctx == nil {
		panic("nil context")
	}

	h2 := new(boltdbHandler)
	*h2 = *h
	h2.ctx = ctx
	return h2
}

func (h *boltdbHandler) Reload(ctx context.Context) error {
	return nil
}

func (h *boltdbHandler) writeAllowed(boundDN string) bool {
	if h.adminDN != "" && h.adminDN == boundDN {
		return true
	}
	return false
}
