package main

import (
	"fmt"
	"strings"

	ldap "bottin/ldapserver"

	message "bottin/goldap"
)

// Generic read utility functions ----------

func (server *Server) getAttribute(dn string, attr string) ([]string, error) {
	path, err := dnToConsul(dn)
	if err != nil {
		return nil, err
	}

	// List all attributes of the object, this is needed because the attribute we are
	// looking for can exist with different cases than the one specified here
	pairs, _, err := server.kv.List(path+"/attribute=", &server.readOpts)
	if err != nil {
		return nil, err
	}

	// Collect values for the attribute, case-insensitively
	values := []string{}
	for _, pair := range pairs {
		if strings.EqualFold(pair.Key, path+"/attribute="+attr) {
			newVals, err := parseValue(pair.Value)
			if err != nil {
				return nil, err
			}
			values = append(values, newVals...)
		}
	}

	return values, nil
}

func (server *Server) objectExists(dn string) (bool, error) {
	prefix, err := dnToConsul(dn)
	if err != nil {
		return false, err
	}

	data, _, err := server.kv.List(prefix+"/attribute=", &server.readOpts)
	if err != nil {
		return false, err
	}
	return len(data) > 0, nil
}

// Compare request -------------------------

func (server *Server) handleCompare(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) {
	state := s.(*State)
	r := m.GetCompareRequest()

	code, err := server.handleCompareInternal(state, &r)

	res := ldap.NewResponse(code)
	if err != nil {
		res.SetDiagnosticMessage(err.Error())
	}
	w.Write(message.CompareResponse(res))
}

func (server *Server) handleCompareInternal(state *State, r *message.CompareRequest) (int, error) {
	attr := string(r.Ava().AttributeDesc())
	expected := string(r.Ava().AssertionValue())

	dn, err := server.checkDN(string(r.Entry()), false)
	if err != nil {
		return ldap.LDAPResultInvalidDNSyntax, err
	}

	// Check permissions
	if !server.config.Acl.Check(&state.login, dn, "read", []string{attr}) {
		return ldap.LDAPResultInsufficientAccessRights, nil
	}

	// Do query
	exists, err := server.objectExists(dn)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}
	if !exists {
		return ldap.LDAPResultNoSuchObject, fmt.Errorf("Not found: %s", dn)
	}

	values, err := server.getAttribute(dn, attr)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}

	for _, v := range values {
		if valueMatch(attr, v, expected) {
			return ldap.LDAPResultCompareTrue, nil
		}
	}

	return ldap.LDAPResultCompareFalse, nil
}

// Search request -------------------------

func (server *Server) handleSearch(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) {
	state := s.(*State)
	r := m.GetSearchRequest()

	code, err := server.handleSearchInternal(state, w, &r)

	res := ldap.NewResponse(code)
	if err != nil {
		res.SetDiagnosticMessage(err.Error())
	}
	if code != ldap.LDAPResultSuccess {
		server.logger.Printf("Failed to do search %#v (%s)", r, err)
	}
	w.Write(message.SearchResultDone(res))
}

func (server *Server) handleSearchInternal(state *State, w ldap.ResponseWriter, r *message.SearchRequest) (int, error) {

	baseObject, err := server.checkDN(string(r.BaseObject()), true)
	if err != nil {
		return ldap.LDAPResultInvalidDNSyntax, err
	}

	server.logger.Tracef("-- SEARCH REQUEST: --")
	server.logger.Tracef("Request BaseDn=%s", baseObject)
	server.logger.Tracef("Request Filter=%s", r.Filter())
	server.logger.Tracef("Request FilterString=%s", r.FilterString())
	server.logger.Tracef("Request Attributes=%s", r.Attributes())
	server.logger.Tracef("Request TimeLimit=%d", r.TimeLimit().Int())

	if !server.config.Acl.Check(&state.login, "read", baseObject, []string{}) {
		return ldap.LDAPResultInsufficientAccessRights, fmt.Errorf("Please specify a base object on which you have read rights")
	}

	baseObjectLevel := len(strings.Split(baseObject, ","))

	basePath, err := dnToConsul(baseObject)
	if err != nil {
		return ldap.LDAPResultInvalidDNSyntax, err
	}
	if r.Scope() == message.SearchRequestScopeBaseObject {
		basePath += "/attribute="
	} else {
		basePath += "/"
	}

	data, _, err := server.kv.List(basePath, &server.readOpts)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}

	entries, err := parseConsulResult(data)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}

	server.logger.Tracef("in %s: %#v", basePath, data)
	server.logger.Tracef("%#v", entries)

	for dn, entry := range entries {
		if r.Scope() == message.SearchRequestScopeBaseObject {
			if dn != baseObject {
				continue
			}
		} else if r.Scope() == message.SearchRequestSingleLevel {
			objectLevel := len(strings.Split(dn, ","))
			if objectLevel != baseObjectLevel+1 {
				continue
			}
		}
		// Filter out if we don't match requested filter
		matched, err := applyFilter(entry, r.Filter())
		if err != nil {
			return ldap.LDAPResultUnwillingToPerform, err
		}
		if !matched {
			continue
		}

		// Filter out if user is not allowed to read this
		if !server.config.Acl.Check(&state.login, "read", dn, []string{}) {
			continue
		}

		e := ldap.NewSearchResultEntry(dn)
		for attr, val := range entry {
			// If attribute is not in request, exclude it from returned entry
			if len(r.Attributes()) > 0 {
				found := false
				for _, requested := range r.Attributes() {
					if string(requested) == "1.1" && len(r.Attributes()) == 1 {
						break
					}
					if string(requested) == "*" || strings.EqualFold(string(requested), attr) {
						found = true
						break
					}
				}
				if !found {
					continue
				}
			}
			// If we are not allowed to read attribute, exclude it from returned entry
			if !server.config.Acl.Check(&state.login, "read", dn, []string{attr}) {
				continue
			}
			// Send result
			resultVals := []message.AttributeValue{}
			for _, v := range val {
				resultVals = append(resultVals, message.AttributeValue(v))
			}
			e.AddAttribute(message.AttributeDescription(attr), resultVals...)
		}
		w.Write(e)
	}

	return ldap.LDAPResultSuccess, nil
}

func applyFilter(entry Entry, filter message.Filter) (bool, error) {
	if fAnd, ok := filter.(message.FilterAnd); ok {
		for _, cond := range fAnd {
			res, err := applyFilter(entry, cond)
			if err != nil {
				return false, err
			}
			if !res {
				return false, nil
			}
		}
		return true, nil
	} else if fOr, ok := filter.(message.FilterOr); ok {
		for _, cond := range fOr {
			res, err := applyFilter(entry, cond)
			if err != nil {
				return false, err
			}
			if res {
				return true, nil
			}
		}
		return false, nil
	} else if fNot, ok := filter.(message.FilterNot); ok {
		res, err := applyFilter(entry, fNot.Filter)
		if err != nil {
			return false, err
		}
		return !res, nil
	} else if fPresent, ok := filter.(message.FilterPresent); ok {
		what := string(fPresent)
		// Case insensitive search
		for desc, values := range entry {
			if strings.EqualFold(what, desc) {
				return len(values) > 0, nil
			}
		}
		return false, nil
	} else if fEquality, ok := filter.(message.FilterEqualityMatch); ok {
		desc := string(fEquality.AttributeDesc())
		target := string(fEquality.AssertionValue())
		// Case insensitive attribute search
		for entry_desc, value := range entry {
			if strings.EqualFold(entry_desc, desc) {
				for _, val := range value {
					if valueMatch(entry_desc, val, target) {
						return true, nil
					}
				}
				return false, nil
			}
		}
		return false, nil
	} else {
		return false, fmt.Errorf("Unsupported filter: %#v %T", filter, filter)
	}
}
