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

package ldif

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strings"
	"sync/atomic"
	"time"

	"github.com/go-ldap/ldap/v3"
	"github.com/go-ldap/ldif"
	"github.com/longsleep/rndm"
	cmap "github.com/orcaman/concurrent-map"
	"github.com/sirupsen/logrus"

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

type ldifHandler struct {
	logger  logrus.FieldLogger
	fn      string
	options *Options

	baseDN                  string
	adminDN                 string
	allowLocalAnonymousBind bool

	ctx context.Context

	current atomic.Value

	activeSearchPagings cmap.ConcurrentMap
}

var _ handler.Handler = (*ldifHandler)(nil) // Verify that *ldifHandler implements handler.Handler.

func NewLDIFHandler(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
	}
	logger = logger.WithField("fn", fn)

	h := &ldifHandler{
		logger:  logger,
		fn:      fn,
		options: options,

		allowLocalAnonymousBind: options.AllowLocalAnonymousBind,

		ctx: context.Background(),

		activeSearchPagings: cmap.New(),
	}
	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.open()
	if err != nil {
		return nil, err
	}

	return h, nil
}

func (h *ldifHandler) open() error {
	if !strings.EqualFold(h.options.BaseDN, h.baseDN) {
		return fmt.Errorf("mismatched BaseDN")
	}

	info, err := os.Stat(h.fn)
	if err != nil {
		return fmt.Errorf("failed to open LDIF: %w", err)
	}

	var l *ldif.LDIF
	index := newIndexMapRegister()

	if info.IsDir() {
		h.logger.Debugln("loading LDIF files from folder")
		h.options.templateBasePath = h.fn
		var parseErrors []error
		l, parseErrors, err = parseLDIFDirectory(h.fn, h.options)
		if err != nil {
			return err
		}
		if len(parseErrors) > 0 {
			for _, parseErr := range parseErrors {
				h.logger.WithError(parseErr).Errorln("LDIF error")
			}
			return fmt.Errorf("error in LDIF files")
		}
	} else {
		h.logger.Debugln("loading LDIF")
		h.options.templateBasePath = filepath.Dir(h.fn)
		l, err = parseLDIFFile(h.fn, h.options)
		if err != nil {
			return err
		}
	}

	t, err := treeFromLDIF(l, index, h.options)
	if err != nil {
		return err
	}

	// Store parsed data as memory value.
	value := &ldifMemoryValue{
		l: l,
		t: t,

		index: index,
	}
	h.current.Store(value)

	h.logger.WithFields(logrus.Fields{
		"version":       l.Version,
		"entries_count": len(l.Entries),
		"tree_length":   t.Len(),
		"base_dn":       h.options.BaseDN,
		"indexes":       len(index),
	}).Debugln("loaded LDIF")

	return nil
}

func (h *ldifHandler) load() *ldifMemoryValue {
	value := h.current.Load()
	return value.(*ldifMemoryValue)
}

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

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

func (h *ldifHandler) Reload(ctx context.Context) error {
	return h.open()
}

func (h *ldifHandler) Add(_ string, _ *ldap.AddRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
	return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
}

func (h *ldifHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldapserver.LDAPResultCode, error) {
	bindDN = strings.ToLower(bindDN)

	logger := h.logger.WithFields(logrus.Fields{
		"bind_dn":     bindDN,
		"remote_addr": conn.RemoteAddr().String(),
	})

	if err := h.validateBindDN(bindDN, conn); err != nil {
		logger.WithError(err).Debugln("ldap bind request BindDN validation failed")
		return ldap.LDAPResultInsufficientAccessRights, nil
	}

	if bindSimplePw == "" {
		logger.Debugf("ldap anonymous bind request")
		if bindDN == "" {
			return ldap.LDAPResultSuccess, nil
		} else {
			return ldap.LDAPResultUnwillingToPerform, nil
		}
	} else {
		logger.Debugf("ldap bind request")
	}

	current := h.load()

	entryRecord, found := current.t.Get([]byte(bindDN))
	if !found {
		err := fmt.Errorf("user not found")
		logger.WithError(err).Debugf("ldap bind error")
		return ldap.LDAPResultInvalidCredentials, nil
	}
	entry := entryRecord.(*ldifEntry)

	if err := entry.validatePassword(bindSimplePw); err != nil {
		logger.WithError(err).Debugf("ldap bind credentials error")
		return ldap.LDAPResultInvalidCredentials, nil
	}
	return ldap.LDAPResultSuccess, nil
}

func (h *ldifHandler) Delete(_ string, _ *ldap.DelRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
	return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
}

func (h *ldifHandler) Modify(_ string, _ *ldap.ModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
	return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
}

func (h *ldifHandler) ModifyDN(_ string, _ *ldap.ModifyDNRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
	return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
}

func (h *ldifHandler) ModifyPasswordExop(_ string, _ *ldap.PasswordModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
	return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
}

func (h *ldifHandler) Search(bindDN string, searchReq *ldap.SearchRequest, conn net.Conn) (ldapserver.ServerSearchResult, error) {
	bindDN = strings.ToLower(bindDN)
	searchBaseDN := strings.ToLower(searchReq.BaseDN)
	logger := h.logger.WithFields(logrus.Fields{
		"bind_dn":        bindDN,
		"search_base_dn": searchBaseDN,
		"remote_addr":    conn.RemoteAddr().String(),
		"controls":       searchReq.Controls,
		"size_limit":     searchReq.SizeLimit,
	})

	logger.Debugf("ldap search request for %s", searchReq.Filter)

	if err := h.validateBindDN(bindDN, conn); err != nil {
		logger.WithError(err).Debugln("ldap search request BindDN validation failed")
		return ldapserver.ServerSearchResult{
			ResultCode: ldap.LDAPResultInsufficientAccessRights,
		}, err
	}

	indexFilter, _ := parseFilterToIndexFilter(searchReq.Filter)

	if !strings.HasSuffix(searchBaseDN, h.baseDN) {
		err := fmt.Errorf("ldap search BaseDN is not in our BaseDN %s", h.baseDN)
		return ldapserver.ServerSearchResult{
			ResultCode: ldap.LDAPResultInsufficientAccessRights,
		}, err
	}

	doneControls := []ldap.Control{}
	var pagingControl *ldap.ControlPaging
	var pagingCookie []byte
	if paging := ldap.FindControl(searchReq.Controls, ldap.ControlTypePaging); paging != nil {
		pagingControl = paging.(*ldap.ControlPaging)
		if searchReq.SizeLimit > 0 && pagingControl.PagingSize >= uint32(searchReq.SizeLimit) {
			pagingControl = nil
		} else {
			pagingCookie = pagingControl.Cookie
		}
	}

	pumpCh, resultCode := func() (<-chan *ldifEntry, ldapserver.LDAPResultCode) {
		var pumpCh chan *ldifEntry
		var start = true
		if pagingControl != nil {
			if len(pagingCookie) == 0 {
				pagingCookie = []byte(base64.RawStdEncoding.EncodeToString(rndm.GenerateRandomBytes(8)))
				pagingControl.Cookie = pagingCookie
				pumpCh = make(chan *ldifEntry)
				h.activeSearchPagings.Set(string(pagingControl.Cookie), pumpCh)
				logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump start")
			} else {
				pumpChRecord, ok := h.activeSearchPagings.Get(string(pagingControl.Cookie))
				if !ok {
					return nil, ldap.LDAPResultUnwillingToPerform
				}
				if pagingControl.PagingSize > 0 {
					logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump continue")
					pumpCh = pumpChRecord.(chan *ldifEntry)
					start = false
				} else {
					// No paging size with cookie, means abandon.
					start = false
					logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("search paging pump abandon")
					// TODO(longsleep): Cancel paging pump context.
					h.activeSearchPagings.Remove(string(pagingControl.Cookie))
				}
			}
		} else {
			pumpCh = make(chan *ldifEntry)
		}
		if start {
			current := h.load()
			go h.searchEntriesPump(h.ctx, current, pumpCh, searchReq, pagingControl, indexFilter)
		}

		return pumpCh, ldap.LDAPResultSuccess
	}()
	if resultCode != ldap.LDAPResultSuccess {
		err := fmt.Errorf("search unable to perform: %d", resultCode)
		return ldapserver.ServerSearchResult{
			ResultCode: resultCode,
		}, err
	}

	filterPacket, err := ldapserver.CompileFilter(searchReq.Filter)
	if err != nil {
		return ldapserver.ServerSearchResult{
			ResultCode: ldap.LDAPResultOperationsError,
		}, err
	}

	var entryRecord *ldifEntry
	var entries []*ldap.Entry
	var entry *ldap.Entry
	var count uint32
	var keep bool
results:
	for {
		select {
		case entryRecord = <-pumpCh:
			if entryRecord == nil {
				// All done, set cookie to empty.
				pagingCookie = []byte{}
				break results

			} else {
				entry = entryRecord.Entry

				// Apply filter.
				keep, resultCode = ldapserver.ServerApplyFilter(filterPacket, entry)
				if resultCode != ldap.LDAPResultSuccess {
					return ldapserver.ServerSearchResult{
						ResultCode: resultCode,
					}, errors.New("search filter apply error")
				}
				if !keep {
					continue
				}

				// Filter scope.
				keep, resultCode = ldapserver.ServerFilterScope(searchReq.BaseDN, searchReq.Scope, entry)
				if resultCode != ldap.LDAPResultSuccess {
					return ldapserver.ServerSearchResult{
						ResultCode: resultCode,
					}, errors.New("search scope apply error")
				}
				if !keep {
					continue
				}

				// Make a copy, before filtering attributes.
				e := &ldap.Entry{
					DN:         entry.DN,
					Attributes: make([]*ldap.EntryAttribute, len(entry.Attributes)),
				}
				copy(e.Attributes, entry.Attributes)

				// Filter attributes from entry.
				resultCode, err = ldapserver.ServerFilterAttributes(searchReq.Attributes, e)
				if err != nil {
					return ldapserver.ServerSearchResult{
						ResultCode: resultCode,
					}, err
				}

				// Append entry as result.
				entries = append(entries, e)

				// Count and more.
				count++
				if pagingControl != nil {
					if count >= pagingControl.PagingSize {
						break results
					}
				}
				if searchReq.SizeLimit > 0 && count >= uint32(searchReq.SizeLimit) {
					// TODO(longsleep): handle total sizelimit for paging.
					break results
				}
			}
		}
	}

	if pagingControl != nil {
		doneControls = append(doneControls, &ldap.ControlPaging{
			PagingSize: 0,
			Cookie:     pagingCookie,
		})
	}

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

func (h *ldifHandler) searchEntriesPump(ctx context.Context, current *ldifMemoryValue, pumpCh chan<- *ldifEntry, searchReq *ldap.SearchRequest, pagingControl *ldap.ControlPaging, indexFilter [][]string) {
	defer func() {
		if pagingControl != nil {
			h.activeSearchPagings.Remove(string(pagingControl.Cookie))
			close(pumpCh)
			h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump ended")
		} else {
			close(pumpCh)
		}
	}()

	pump := func(entryRecord *ldifEntry) bool {
		select {
		case pumpCh <- entryRecord:
		case <-ctx.Done():
			if pagingControl != nil {
				h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Warnln("ldap search paging pump context done")
			} else {
				h.logger.Warnln("ldap search pump context done")
			}
			return false
		case <-time.After(1 * time.Minute):
			if pagingControl != nil {
				h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Warnln("ldap search paging pump timeout")
			} else {
				h.logger.Warnln("ldap search pump timeout")
			}
			return false
		}
		return true
	}

	searchBaseDN := strings.ToLower(searchReq.BaseDN)

	load := true
	if len(indexFilter) > 0 {
		// Get entries with help of index.
		load = false
		var results []*[]*ldifEntry
		for _, f := range indexFilter {
			indexed, found := current.index.Load(f[0], f[1], f[2:]...)
			if !found {
				load = true
				break
			}
			results = append(results, &indexed)
		}
		if !load {
			cache := make(map[*ldifEntry]struct{})
			for _, indexed := range results {
				for _, entryRecord := range *indexed {
					if _, cached := cache[entryRecord]; cached {
						// Prevent duplicates.
						continue
					}
					if strings.HasSuffix(entryRecord.DN, searchBaseDN) {
						if ok := pump(entryRecord); !ok {
							return
						}
					}
					cache[entryRecord] = struct{}{}
				}
			}
		}
	}
	if load {
		// Walk through all entries (this is slow).
		h.logger.WithField("filter", searchReq.Filter).Warnln("ldap search filter does not match any index, using slow walk")
		current.t.WalkSuffix([]byte(searchBaseDN), func(key []byte, entryRecord interface{}) bool {
			if ok := pump(entryRecord.(*ldifEntry)); !ok {
				return true
			}
			return false
		})
	}
}

func (h *ldifHandler) validateBindDN(bindDN string, conn net.Conn) error {
	if bindDN == "" {
		if h.allowLocalAnonymousBind {
			host, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
			if net.ParseIP(host).IsLoopback() {
				return nil
			}
			return fmt.Errorf("anonymous BindDN rejected")
		}
		return fmt.Errorf("anonymous BindDN not allowed")
	}

	if strings.HasSuffix(bindDN, h.baseDN) {
		return nil
	}
	return fmt.Errorf("the BindDN is not in our BaseDN: %s", h.baseDN)
}

func (h *ldifHandler) Close(bindDN string, conn net.Conn) error {
	h.logger.WithFields(logrus.Fields{
		"bind_dn":     bindDN,
		"remote_addr": conn.RemoteAddr().String(),
	}).Debugln("ldap close")

	return nil
}
