package main

import (
	"encoding/json"
	"fmt"
	"strings"

	ldap "bottin/ldapserver"

	message "bottin/goldap"

	consul "github.com/hashicorp/consul/api"
)

// Generic item modification function --------

func (server *Server) putAttributes(dn string, attrs Entry) error {
	prefix, err := dnToConsul(dn)
	if err != nil {
		return err
	}

	// Normalize attribute names: if we have several times the same attr
	// but with different cases, put that in the same attr
	normalized := make(Entry)
	for k, values := range attrs {
		found := false
		for k2 := range normalized {
			if strings.EqualFold(k, k2) {
				normalized[k2] = append(normalized[k2], values...)
				found = true
				break
			}
		}
		if !found {
			normalized[k] = values
		}
	}

	// Retreieve previously existing attributes, which we will use to delete
	// entries with the wrong case
	previous_pairs, _, err := server.kv.List(prefix+"/attribute=", &server.readOpts)
	if err != nil {
		return err
	}

	for k, valuesNC := range normalized {
		path := prefix + "/attribute=" + k

		// Trim spaces and remove empty values
		values := []string{}
		for _, v := range valuesNC {
			vv := strings.TrimSpace(v)
			if len(vv) > 0 {
				values = append(values, vv)
			}
		}

		// If previously existing pairs with the wrong case exist, delete them
		for _, prev_pair := range previous_pairs {
			if strings.EqualFold(prev_pair.Key, path) && prev_pair.Key != path {
				_, err := server.kv.Delete(prev_pair.Key, nil)
				if err != nil {
					return err
				}
			}
		}

		// if the password is not yet hashed we hash it
		if k == ATTR_USERPASSWORD {
			tmpValues := []string{}
			for _, pw := range values {
				_, err := determineHashType(pw)
				if err != nil {
					encodedPassword, err := SSHAEncode(pw)
					if err != nil {
						return err
					}
					tmpValues = append(tmpValues, encodedPassword)
				} else {
					tmpValues = append(tmpValues, pw)
				}
			}
			values = tmpValues
		}

		// If we have zero values, delete associated k/v pair
		// Otherwise, write new values
		if len(values) == 0 {
			_, err := server.kv.Delete(path, nil)
			if err != nil {
				return err
			}
		} else {
			json, err := json.MarshalIndent(values, "", "  ")
			if err != nil {
				return err
			}
			pair := &consul.KVPair{Key: path, Value: json}
			_, err = server.kv.Put(pair, nil)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// Add request ------------------------

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

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

	res := ldap.NewResponse(code)
	if err != nil {
		res.SetDiagnosticMessage(err.Error())
	}
	if code == ldap.LDAPResultSuccess {
		server.logger.Printf("Successfully added %s", string(r.Entry()))
	} else {
		server.logger.Printf("Failed to add %s (%s)", string(r.Entry()), err)
	}
	w.Write(message.AddResponse(res))
}

func (server *Server) handleAddInternal(state *State, r *message.AddRequest) (int, error) {
	dn, err := server.checkDN(string(r.Entry()), false)
	if err != nil {
		return ldap.LDAPResultInvalidDNSyntax, err
	}

	dnSplit, err := parseDN(dn)
	if err != nil {
		return ldap.LDAPResultInvalidDNSyntax, err
	}

	// Check permissions
	attrListStr := []string{}
	for _, attribute := range r.Attributes() {
		attrListStr = append(attrListStr, string(attribute.Type_()))
	}
	if !server.config.Acl.Check(&state.login, "add", dn, attrListStr) {
		return ldap.LDAPResultInsufficientAccessRights, nil
	}

	// Check that object does not already exist
	exists, err := server.objectExists(dn)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}
	if exists {
		return ldap.LDAPResultEntryAlreadyExists, nil
	}

	// Check that parent object exists
	parentDn := unparseDN(dnSplit[1:])
	parentExists, err := server.objectExists(parentDn)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}
	if !parentExists {
		return ldap.LDAPResultNoSuchObject, fmt.Errorf(
			"Parent object %s does not exist", parentDn)
	}

	// If adding a group, track of who the members will be so that their memberOf field can be updated later
	members := []string{}

	// Check attributes
	entry := Entry{}
	for _, attribute := range r.Attributes() {
		key := string(attribute.Type_())
		vals_str := []string{}
		for _, val := range attribute.Vals() {
			vals_str = append(vals_str, string(val))
		}

		// Fail if they are trying to write memberOf, we manage this ourselves
		err = checkRestrictedAttr(key)
		if err != nil {
			return ldap.LDAPResultObjectClassViolation, err
		}
		if strings.EqualFold(key, ATTR_MEMBER) {
			// If they are writing a member list, we have to check they are adding valid members
			// Also, rewrite member list to use canonical DN syntax (no spaces, all lowercase)
			for _, member := range vals_str {
				member_canonical, err := server.checkDN(member, false)
				if err != nil {
					return ldap.LDAPResultInvalidDNSyntax, err
				}
				exists, err = server.objectExists(member_canonical)
				if err != nil {
					return ldap.LDAPResultOperationsError, err
				}
				if !exists {
					return ldap.LDAPResultNoSuchObject, fmt.Errorf(
						"Cannot add %s to members, it does not exist!",
						member_canonical)
				}
				members = append(members, member_canonical)
			}
			entry[key] = members
		} else {
			if prev, ok := entry[key]; ok {
				entry[key] = append(prev, vals_str...)
			} else {
				entry[key] = vals_str
			}
		}
	}

	// Ensure object has at least one objectclass value
	hasObjectClass := false
	for k := range entry {
		if strings.EqualFold(k, ATTR_OBJECTCLASS) {
			hasObjectClass = true
			break
		}
	}
	if !hasObjectClass {
		entry[ATTR_OBJECTCLASS] = []string{"top"}
	}

	// Write system attributes
	entry[ATTR_CREATORSNAME] = []string{state.login.user}
	entry[ATTR_CREATETIMESTAMP] = []string{genTimestamp()}
	entry[ATTR_ENTRYUUID] = []string{genUuid()}
	entry[dnSplit[0].Type] = []string{dnSplit[0].Value}

	// Add our item in the DB
	err = server.putAttributes(dn, entry)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}
	// ~~ from this point on, our operation succeeded ~~
	// ~~ future errors cause inconsistencies in the DB and are logged ~~

	// If our item has a member list, add it to all of its member's memberOf attribute
	for _, member := range members {
		server.memberOfAdd(member, dn)
	}

	return ldap.LDAPResultSuccess, nil
}

// Delete request ------------------------

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

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

	res := ldap.NewResponse(code)
	if err != nil {
		res.SetDiagnosticMessage(err.Error())
	}
	if code == ldap.LDAPResultSuccess {
		server.logger.Printf("Successfully deleted %s", string(r))
	} else {
		server.logger.Printf("Failed to delete %s (%s)", string(r), err)
	}
	w.Write(message.DelResponse(res))
}

func (server *Server) handleDeleteInternal(state *State, r *message.DelRequest) (int, error) {
	dn, err := server.checkDN(string(*r), false)
	if err != nil {
		return ldap.LDAPResultInvalidDNSyntax, err
	}

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

	// Check that this LDAP entry exists and has no children
	path, err := dnToConsul(dn)
	if err != nil {
		return ldap.LDAPResultInvalidDNSyntax, err
	}

	items, _, err := server.kv.List(path+"/", &server.readOpts)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}

	if len(items) == 0 {
		return ldap.LDAPResultNoSuchObject, fmt.Errorf("Not found: %s", dn)
	}
	for _, item := range items {
		itemDN, _, err := consulToDN(item.Key)
		if err != nil {
			continue
		}
		if itemDN != dn {
			return ldap.LDAPResultNotAllowedOnNonLeaf, fmt.Errorf(
				"Cannot delete %s as it has children", dn)
		}
	}

	// Retrieve group membership before we delete everything
	memberOf, err := server.getAttribute(dn, ATTR_MEMBEROF)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}
	memberList, err := server.getAttribute(dn, ATTR_MEMBER)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}

	// Delete the LDAP entry
	_, err = server.kv.DeleteTree(path+"/", nil)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}
	// ~~ from this point on, our operation succeeded ~~
	// ~~ future errors cause inconsistencies in the DB and are logged ~~

	// Delete it from the member list of all the groups it was a member of
	if memberOf != nil {
		for _, group := range memberOf {
			groupMembers, err := server.getAttribute(group, ATTR_MEMBER)
			if err != nil {
				server.logger.Warnf("Could not remove %s from members of %s: %s", dn, group, err)
				continue
			}

			newMembers := []string{}
			for _, memb := range groupMembers {
				if memb != dn {
					newMembers = append(newMembers, memb)
				}
			}

			err = server.putAttributes(group, Entry{
				ATTR_MEMBER: newMembers,
			})
			if err != nil {
				server.logger.Warnf("Could not remove %s from members of %s: %s", dn, group, err)
			}
		}
	}

	// Delete it from all of its member's memberOf info
	for _, member := range memberList {
		server.memberOfRemove(member, dn)
	}

	return ldap.LDAPResultSuccess, nil
}

// Modify request ------------------------

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

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

	res := ldap.NewResponse(code)
	if err != nil {
		res.SetDiagnosticMessage(err.Error())
	}
	if code == ldap.LDAPResultSuccess {
		server.logger.Printf("Successfully modified %s", string(r.Object()))
	} else {
		server.logger.Printf("Failed to modifiy %s (%s)", string(r.Object()), err)
	}
	w.Write(message.ModifyResponse(res))
}

func (server *Server) handleModifyInternal(state *State, r *message.ModifyRequest) (int, error) {
	dn, err := server.checkDN(string(r.Object()), false)
	if err != nil {
		return ldap.LDAPResultInvalidDNSyntax, err
	}

	dnSplit, err := parseDN(dn)
	if err != nil {
		return ldap.LDAPResultInvalidDNSyntax, err
	}

	// First permission check with no particular attributes
	if !server.config.Acl.Check(&state.login, "modify", dn, []string{}) &&
		!server.config.Acl.Check(&state.login, "modifyAdd", dn, []string{}) {
		return ldap.LDAPResultInsufficientAccessRights, nil
	}

	// Retrieve previous values (by the way, check object exists)
	path, err := dnToConsul(dn)
	if err != nil {
		return ldap.LDAPResultInvalidDNSyntax, err
	}

	items, _, err := server.kv.List(path+"/attribute=", &server.readOpts)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}

	if len(items) == 0 {
		return ldap.LDAPResultNoSuchObject, fmt.Errorf("Not found: %s", dn)
	}

	prevEntry := Entry{}
	for _, item := range items {
		itemDN, attr, err := consulToDN(item.Key)
		if err != nil {
			continue
		}
		if itemDN != dn {
			server.logger.Fatal("itemDN != dn in handleModifyInternal")
		}
		vals, err := parseValue(item.Value)
		if err != nil {
			return ldap.LDAPResultOperationsError, err
		}
		prevEntry[attr] = vals
	}

	// Keep track of group members added/deleted
	addMembers, delMembers := []string{}, []string{}

	// Produce new entry values to be saved
	entry := Entry{}

	for _, change := range r.Changes() {
		attr := string(change.Modification().Type_())
		changeValues := []string{}
		for _, v := range change.Modification().Vals() {
			changeValues = append(changeValues, string(v))
		}

		// If we already had an attribute with this name before,
		// make sure we are using the same lowercase/uppercase
		for prevAttr := range prevEntry {
			if strings.EqualFold(attr, prevAttr) {
				attr = prevAttr
				break
			}
		}

		// Check that this attribute is not system-managed thus restricted
		err = checkRestrictedAttr(attr)
		if err != nil {
			return ldap.LDAPResultObjectClassViolation, err
		}
		if strings.EqualFold(attr, dnSplit[0].Type) {
			return ldap.LDAPResultObjectClassViolation, fmt.Errorf("%s may not be changed as it is part of object path", attr)
		}

		// Check for permission to modify this attribute
		if !(server.config.Acl.Check(&state.login, "modify", dn, []string{attr}) ||
			(change.Operation() == ldap.ModifyRequestChangeOperationAdd &&
				server.config.Acl.Check(&state.login, "modifyAdd", dn, []string{attr}))) {
			return ldap.LDAPResultInsufficientAccessRights, nil
		}

		// If we are changing ATTR_MEMBER, rewrite all values to canonical form
		if strings.EqualFold(attr, ATTR_MEMBER) {
			for i := range changeValues {
				canonical_val, err := server.checkDN(changeValues[i], false)
				if err != nil {
					return ldap.LDAPResultInvalidDNSyntax, err
				}
				changeValues[i] = canonical_val
			}
		}

		// If we don't yet have a new value for this attr,
		// but one existed before, initialize entry[attr] to the old value
		// so that later on what we do is simply modify entry[attr] in place
		// (this allows to handle sequences of several changes on the same attr)
		if _, ok := entry[attr]; !ok {
			if _, ok := prevEntry[attr]; ok {
				entry[attr] = prevEntry[attr]
			}
		}

		// Apply effective modification on entry[attr]
		if change.Operation() == ldap.ModifyRequestChangeOperationAdd {
			for _, val := range changeValues {
				if !listContains(entry[attr], val) {
					entry[attr] = append(entry[attr], val)
					if strings.EqualFold(attr, ATTR_MEMBER) {
						addMembers = append(addMembers, val)
					}
				}
			}
		} else if change.Operation() == ldap.ModifyRequestChangeOperationDelete {
			if len(changeValues) == 0 {
				// Delete everything
				if strings.EqualFold(attr, ATTR_MEMBER) {
					delMembers = append(delMembers, entry[attr]...)
				}
				entry[attr] = []string{}
			} else {
				// Delete only those specified
				newList := []string{}
				for _, prevVal := range entry[attr] {
					if !listContains(changeValues, prevVal) {
						newList = append(newList, prevVal)
					} else {
						if strings.EqualFold(attr, ATTR_MEMBER) {
							delMembers = append(delMembers, prevVal)
						}
					}
				}
				entry[attr] = newList
			}
		} else if change.Operation() == ldap.ModifyRequestChangeOperationReplace {
			if strings.EqualFold(attr, ATTR_MEMBER) {
				for _, newMem := range changeValues {
					if !listContains(entry[attr], newMem) {
						addMembers = append(addMembers, newMem)
					}
				}
				for _, prevMem := range entry[attr] {
					if !listContains(changeValues, prevMem) {
						delMembers = append(delMembers, prevMem)
					}
				}
			}
			entry[attr] = changeValues
		}
	}

	// Check that added members actually exist
	for i := range addMembers {
		exists, err := server.objectExists(addMembers[i])
		if err != nil {
			return ldap.LDAPResultOperationsError, err
		}
		if !exists {
			return ldap.LDAPResultNoSuchObject, fmt.Errorf(
				"Cannot add member %s, it does not exist", addMembers[i])
		}
	}

	for k, v := range entry {
		if strings.EqualFold(k, ATTR_OBJECTCLASS) && len(v) == 0 {
			return ldap.LDAPResultInsufficientAccessRights, fmt.Errorf(
				"Cannot remove all objectclass values")
		}
	}

	// Now, the modification has been processed and accepted and we want to commit it
	entry[ATTR_MODIFIERSNAME] = []string{state.login.user}
	entry[ATTR_MODIFYTIMESTAMP] = []string{genTimestamp()}

	// Save the edited values
	err = server.putAttributes(dn, entry)
	if err != nil {
		return ldap.LDAPResultOperationsError, err
	}
	// ~~ from this point on, our operation succeeded ~~
	// ~~ future errors cause inconsistencies in the DB and are logged ~~

	// Update memberOf for added members and deleted members
	for _, addMem := range addMembers {
		server.memberOfAdd(addMem, dn)
	}

	for _, delMem := range delMembers {
		server.memberOfRemove(delMem, dn)
	}

	return ldap.LDAPResultSuccess, nil
}
