package main

import (
	"fmt"
	"strings"
	"sync"

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

//Mux value, this value permits do not have two identicals values in the parallel instances
type StoreAllCN struct {
	mu sync.Mutex
	cn map[string]struct{}
}

var allNames = StoreAllCN{cn: make(map[string]struct{})}

//Type used for the tests
type attributes struct {
	Name string
	Data []string
}

type data_DN struct {
	DN         string
	Attributes []attributes
}

type instance struct {
	numberUsers, numberGroups int
	dataGroups, dataUsers     []data_DN
	logging                   *ldap.Conn
}

//Create a new object instance
//With this instance, we can obtain an isolated container where
//we have our users and groups. It allows to run tests in parallel.
func NewInstance(numberUsers, numberGroups int) (*instance, error) {
	l, err := Connect()
	if err != nil {
		return nil, err
	}

	logging.Level = logrus.InfoLevel

	inst := instance{
		numberUsers:  numberUsers,
		numberGroups: numberGroups,
		dataGroups:   []data_DN{},
		dataUsers:    []data_DN{},
		logging:      l,
	}

	err = inst.createOrganizationnalUnit()
	if ldap.IsErrorWithCode(err, uint16(68)) {
		logging.Warn("OrganizationnalUnit already created")
		err = nil
	}
	if err != nil {
		return nil, err
	}

	err = inst.CreateGroups()
	if err != nil {
		return nil, err
	}
	err = inst.CreateUsers()
	if err != nil {
		return nil, err
	}

	return &inst, nil
}

//Part: Created users or groups or OU

func (inst *instance) createOrganizationnalUnit() error {
	dn := []string{"ou=groups,dc=deuxfleurs,dc=fr", "ou=users,dc=deuxfleurs,dc=fr"}
	attributes := []map[string][]string{{
		"description":           []string{"OrganizationalUnit qui regroupe tous les groupes"},
		"objectclass":           []string{"organizationalUnit", "top"},
		"ou":                    []string{"groups"},
		"structuralobjectclass": []string{"organizationalUnit"},
	},
		{
			"description":           []string{"OrganizationalUnit qui regroupe tous les users"},
			"objectclass":           []string{"organizationalUnit", "top"},
			"ou":                    []string{"users"},
			"structuralobjectclass": []string{"organizationalUnit"},
		},
	}

	for index := range dn {
		err := inst.Add_Request(dn[index], attributes[index])
		if err != nil {
			return err
		}
	}
	return nil

}

//Part: Create User or group

func (inst *instance) CreateUsers() (err error) {

	dn := "cn=%s,ou=users,dc=deuxfleurs,dc=fr"
	attributes := map[string][]string{
		"displayname":           {},
		"objectclass":           {"inetOrgPerson", "organizationalPerson", "person", "top"},
		"structuralobjectclass": {"inetOrgPerson"},
	}

	du, err := inst.create(dn, []string{"displayname"}, inst.numberUsers, attributes, inst.dataUsers)
	if err == nil {
		inst.dataUsers = du
	}
	return err
}

func (inst *instance) CreateGroups() error {
	dn := "cn=%s,ou=groups,dc=deuxfleurs,dc=fr"
	attributes := map[string][]string{
		"description":           {},
		"objectclass":           {"groupOfNames", "top"},
		"structuralobjectclass": {"groupOfNames"},
	}

	dg, err := inst.create(dn, []string{"description"}, inst.numberGroups, attributes, inst.dataGroups)
	if err == nil {
		inst.dataGroups = dg
	}

	return err
}

//Hard Function: She does:
//- generate an unique name
//- store the Data of each AddRequest in instance struct
//- send AddRequest to Bottin
func (inst *instance) create(dn string, unique_attr []string, number int, attributes map[string][]string, data []data_DN) ([]data_DN, error) {
	for i := 0; i < number; i++ {
		name := inst.GenerateName()

		datDn := data_DN{DN: fmt.Sprintf(dn, name)}

		for _, value := range unique_attr {
			attributes[value] = []string{name}
		}

		datDn.Attributes = MapAttToStruct(attributes)
		data = append(data, datDn)

		err := inst.Add_Request(fmt.Sprintf(dn, name), attributes)
		if err != nil {
			return nil, err
		}
	}
	return data, nil
}

//Part: clean

func (inst *instance) Clean() error {
	err := inst.CleanGroups()
	if err != nil {
		return err
	}
	err = inst.CleanUsers()
	return err
}

func (inst *instance) CleanUsers() error {
	err := inst.clean(inst.dataUsers)
	if err != nil {
		return err
	}
	inst.dataUsers = []data_DN{}
	return err
}

func (inst *instance) CleanGroups() error {
	err := inst.clean(inst.dataGroups)
	if err != nil {
		return err
	}
	inst.dataGroups = []data_DN{}
	return err
}

func (inst *instance) clean(stock []data_DN) error {
	logging.Debugf("Delete %d elements.", len(stock))
	for _, value := range stock {
		err := inst.Delete_Request(value.DN)
		if err != nil {
			return err
		}
	}
	return nil
}

//Part: Verify if a data_Dn is a group or an user
func (inst *instance) VerifyUser(user data_DN) (bool, error) {
	dn := "ou=users,dc=deuxfleurs,dc=fr"
	cn := strings.Split(user.DN, ",")[0]
	filter := fmt.Sprintf("(%s)", cn)

	res, err := inst.Search_Request(dn, filter, []string{"cn"})

	return len(res.Entries) == 1, err
}

func (inst *instance) VerifyGroup(group data_DN) (bool, error) {
	dn := "ou=groups,dc=deuxfleurs,dc=fr"
	cn := strings.Split(group.DN, ",")[0]
	filter := fmt.Sprintf("(%s)", cn)

	res, err := inst.Search_Request(dn, filter, []string{"cn"})

	return len(res.Entries) == 1, err
}

//Part: Add user in a group
func (inst *instance) AddUserInGroup(user, group data_DN) error {

	err := inst.Modify_Request(group.DN, nil, nil, map[string][]string{
		"member": {user.DN},
	})
	return err
}

func (inst *instance) AddUserSliceInGroup(users_cn []string, group_dn string) error {

	err := inst.Modify_Request(group_dn, nil, nil, map[string][]string{
		"member": users_cn,
	})
	return err
}

//Part: modify, add, delete data_DN struct

func AddAtt(name string, data []string, dat data_DN) data_DN {
	dat.Attributes = append(dat.Attributes, attributes{
		Name: name,
		Data: data,
	})

	logging.Debug(fmt.Sprintf("Attributes %s add from %s.", name, dat.DN))
	return dat
}

func DelAtt(name string, dat data_DN) data_DN {
	for index, value := range dat.Attributes {
		if value.Name == name {
			dat.Attributes[index] = dat.Attributes[len(dat.Attributes)-1]
			//tmp := dat.Attributes[:len(dat.Attributes)-1]
			dat.Attributes = []attributes{}
			logging.Debugf("Attributes %s delete from %s.", name, dat.DN)
			return dat
		}
	}
	logging.Debugf("Can't delete attribute %s from %s.", name, dat.DN)
	return dat
}

func ReplaceAtt(name string, data []string, dat data_DN) data_DN {
	for index, value := range dat.Attributes {
		if value.Name == name {
			dat.Attributes[index] = attributes{
				Name: name,
				Data: data,
			}
			logging.Debugf("Replace attributes %s from %s succesful..", name, dat.DN)
			return dat
		}
	}
	logging.Debugf("Can't replace attributes %s from %s.", name, dat.DN)
	return dat
}
