package ldap

import (
	"errors"
	"fmt"
	"sort"
	"strconv"

	"gogs.carducci-dante.gov.it/karmen/core/config"
	"gogs.carducci-dante.gov.it/karmen/core/orm"
	ldap "gopkg.in/ldap.v2"
)

type CompleteNameI interface {
	CompleteName() string
}

type Client struct {
	Conn   *ldap.Conn
	Config *config.ConfigT
}

func (c *Client) Close() {
	c.Conn.Close()
}

func NewClient(host string, config *config.ConfigT) (*Client, error) {
	var err error

	client := new(Client)
	client.Config = config

	client.Conn, err = ldap.Dial("tcp", host)
	if err != nil {
		return nil, err
	}

	err = client.Conn.Bind(config.AdminCN(), config.Ldap.AdminPassword)
	if err != nil {
		return nil, err
	}
	return client, nil
}

func (c *Client) AddUser(user orm.User) error {
	mailDir := fmt.Sprintf("%s/%s/", c.Config.Ldap.MailDirBasePath, user.Username())
	uidNumber, err := c.NextMailUIDNumber()
	if err != nil {
		return err
	}
	addRequest := ldap.NewAddRequest(user.DN())
	addRequest.Attribute("objectClass", []string{
		"inetOrgPerson",
		"posixAccount",
		"PostfixBookMailAccount",
		"organizationalPerson",
		"extensibleObject",
	})
	addRequest.Attribute("sn", []string{user.GetSurname()})
	addRequest.Attribute("uid", []string{user.Username()})
	addRequest.Attribute("homeDirectory", []string{fmt.Sprintf("/home/users/%s", user.Username())})
	addRequest.Attribute("givenName", []string{user.GetSurname()})
	addRequest.Attribute("mail", []string{fmt.Sprintf("%s@%s", user.Username(), c.Config.Domain)})
	addRequest.Attribute("mailEnabled", []string{"TRUE"})
	addRequest.Attribute("mailGidNumber", []string{c.Config.Ldap.MailGIDNumber})
	addRequest.Attribute("mailUidNumber", []string{uidNumber})
	addRequest.Attribute("uidNumber", []string{uidNumber})
	addRequest.Attribute("gidNumber", []string{uidNumber})
	addRequest.Attribute("uniqueIdentifier", []string{user.Username()})
	addRequest.Attribute("mailHomeDirectory", []string{mailDir})
	addRequest.Attribute("mailStorageDirectory", []string{"maildir:" + mailDir})
	addRequest.Attribute("mailQuota", []string{c.Config.Ldap.MailQuota})
	addRequest.Attribute("userPassword", []string{fmt.Sprintf("{SSHA}%s", orm.SaltPassword(user.GetPlainPassword()))})

	err = c.Conn.Add(addRequest)
	if err != nil {
		return err
	}

	return nil
}

func (c *Client) UpdateUser(user orm.User) error {
	mailDir := fmt.Sprintf("%s/%s/", c.Config.Ldap.MailDirBasePath, user.Username())
	modRequest := ldap.NewModifyRequest(user.DN())
	modRequest.Replace("mail", []string{fmt.Sprintf("%s@%s", user.Username(), c.Config.Domain)})
	modRequest.Replace("mailHomeDirectory", []string{mailDir})
	modRequest.Replace("mailStorageDirectory", []string{"maildir:" + mailDir})
	modRequest.Replace("mailEnabled", []string{"TRUE"})
	modRequest.Replace("mailGidNumber", []string{c.Config.Ldap.MailGIDNumber})
	modRequest.Replace("mailQuota", []string{c.Config.Ldap.MailQuota})

	err := c.Conn.Modify(modRequest)
	if err != nil {
		return err
	}

	return nil
}

func (c *Client) UpdateUserPassword(user orm.User) error {
	modRequest := ldap.NewModifyRequest(user.DN())
	modRequest.Replace("userPassword", []string{fmt.Sprintf("{SSHA}%s", orm.SaltPassword(user.GetPlainPassword()))})
	err := c.Conn.Modify(modRequest)
	if err != nil {
		return err
	}
	return nil
}

func (c *Client) DeleteUser(user orm.User) error {
	delRequest := ldap.NewDelRequest(user.DN(), nil)
	err := c.Conn.Del(delRequest)
	if err != nil {
		return err
	}

	return nil
}

func (c *Client) DeleteByDN(dn string) error {
	delRequest := ldap.NewDelRequest(dn, nil)
	err := c.Conn.Del(delRequest)
	if err != nil {
		return err
	}

	return nil
}

func (c *Client) AddUserToGroup(user orm.User, groupDN string) error {
	memberAttr, err := c.memberAttribute(groupDN)
	if err != nil {
		return err
	}
	modRequest := ldap.NewModifyRequest(fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()))
	switch memberAttr {
	case "member":
		modRequest.Add(memberAttr, []string{user.DN()})
	case "memberuid":
		modRequest.Add(memberAttr, []string{user.Username()})
	default:
		return errors.New(fmt.Sprintf("Attribute %s is not supported!", memberAttr))
	}
	err = c.Conn.Modify(modRequest)
	if err != nil {
		return err
	}

	return nil
}

func (c *Client) RemoveUserFromGroup(user *orm.Credential, groupDN string) error {
	memberAttr, err := c.memberAttribute(groupDN)
	if err != nil {
		return err
	}
	modRequest := ldap.NewModifyRequest(fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()))
	switch memberAttr {
	case "member":
		modRequest.Delete(memberAttr, []string{user.DN()})
	case "memberuid":
		modRequest.Delete(memberAttr, []string{user.Username()})
	default:
		return errors.New(fmt.Sprintf("Attribute %s is not supported!", memberAttr))
	}
	err = c.Conn.Modify(modRequest)
	if err != nil {
		return err
	}

	return nil
}

func (c *Client) RemoveUserFromGroupByMemberValue(memberValue string, groupDN string) error {
	memberAttr, err := c.memberAttribute(groupDN)
	if err != nil {
		return err
	}
	modRequest := ldap.NewModifyRequest(fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()))
	switch memberAttr {
	case "member":
		modRequest.Delete(memberAttr, []string{memberValue})
	case "memberuid":
		modRequest.Delete(memberAttr, []string{memberValue})
	default:
		return errors.New(fmt.Sprintf("Attribute %s is not supported!", memberAttr))
	}
	err = c.Conn.Modify(modRequest)
	if err != nil {
		return err
	}

	return nil
}

func (c *Client) IsUserInGroup(user *orm.Credential, groupDN string) (bool, error) {
	memberAttr, err := c.memberAttribute(groupDN)
	if err != nil {
		return false, err
	}
	var memberValue string
	switch memberAttr {
	case "member":
		memberValue = user.DN()
	case "memberuid":
		memberValue = user.Username()
	default:
		return false, errors.New(fmt.Sprintf("Attribute %s is not supported!", memberAttr))
	}

	searchRequest := ldap.NewSearchRequest(
		fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()),
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
		fmt.Sprintf("(&(%s=%s))", memberAttr, memberValue),
		[]string{memberAttr},
		nil,
	)

	sr, err := c.Conn.Search(searchRequest)
	if err != nil {
		return false, err
	}

	return len(sr.Entries) > 0, nil
}

func (c *Client) GroupMembers(groupDN string) ([]*ldap.Entry, error) {
	searchRequest := ldap.NewSearchRequest(
		fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()),
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
		"(|(member=*)(memberuid=*))",
		[]string{"member", "memberuid"},
		nil,
	)
	sr, err := c.Conn.Search(searchRequest)
	if err != nil {
		return nil, err
	}

	return sr.Entries, nil
}

func (c *Client) Users(searchDN string) ([]*ldap.Entry, error) {
	result, err := c.Search(
		searchDN,
		"(&(objectClass=organizationalPerson))",
		[]string{"dn", "cn"},
	)
	if err != nil {
		return nil, err
	}

	return result.Entries, nil
}

func (c *Client) Search(base string, filter string, attributes []string) (*ldap.SearchResult, error) {
	searchRequest := ldap.NewSearchRequest(
		base,
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
		filter,
		attributes,
		nil,
	)

	sr, err := c.Conn.Search(searchRequest)
	if err != nil {
		return nil, err
	}

	return sr, nil
}

func (c *Client) UserExists(user *orm.Credential, searchDN string) (bool, error) {
	searchRequest := ldap.NewSearchRequest(
		searchDN,
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
		fmt.Sprintf("(&(objectClass=organizationalPerson)(cn=%s))", user.CompleteName()),

		[]string{"dn", "cn"},
		nil,
	)

	sr, err := c.Conn.Search(searchRequest)
	if err != nil {
		return false, err
	}

	return len(sr.Entries) > 0, nil
}

func (c *Client) DomainDN() string {
	return c.Config.DomainDN()
}

func (c *Client) GroupsDN() string {
	return fmt.Sprintf("%s,%s", c.Config.Ldap.GroupsDN, c.DomainDN())
}

func (c *Client) PeopleDN() string {
	return fmt.Sprintf("%s,%s", c.Config.Ldap.PeopleDN, c.DomainDN())
}

func (c *Client) NextMailUIDNumber() (string, error) {
	searchRequest := ldap.NewSearchRequest(
		c.PeopleDN(),
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
		"(mailUidNumber=*)",
		[]string{"mailUidNumber"},
		nil,
	)
	sr, err := c.Conn.Search(searchRequest)
	if err != nil {
		return "0", err
	}

	if len(sr.Entries) == 0 {
		return c.Config.Ldap.FirstUIDNumber, nil
	}

	var uids []int

	for _, e := range sr.Entries {
		n, err := strconv.Atoi(e.Attributes[0].Values[0])
		uids = append(uids, n)
		if err != nil {
			return "0", err
		}
	}

	sort.Ints(uids)

	nextUid := uids[len(uids)-1] + 1

	return strconv.Itoa(nextUid), nil
}

func (c *Client) memberAttribute(groupDN string) (string, error) {
	searchRequest := ldap.NewSearchRequest(
		fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()),
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
		"(member=*)",
		[]string{"member"},
		nil,
	)

	sr, err := c.Conn.Search(searchRequest)
	if err != nil {
		return "", err
	}

	if len(sr.Entries) > 0 {
		return "member", nil
	}

	return "memberuid", nil
}

func (c *Client) personDN(person CompleteNameI, baseDN string) string {
	dn := fmt.Sprintf("cn=%s,%s", person.CompleteName(), baseDN)
	return dn
}
