package activeDirectory

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"

	"github.com/go-ldap/ldap"
)

const (
	ldapHost      = "ad.auth.rackspace.com"
	ldapTLSPort   = uint16(636)
	ldapCACertPEM = `-----BEGIN CERTIFICATE-----
MIIFTDCCAzSgAwIBAgIQNJwfKVTMrbBDWXAjkS7/UzANBgkqhkiG9w0BAQsFADA3
MRcwFQYDVQQKEw5SYWNrc3BhY2UgSW5jLjEcMBoGA1UEAxMTUmFja3NwYWNlIFJv
b3QgQ0EgMTAeFw0xNzA1MTAxNjMyMTdaFw00MjA1MTAxNjMyMTdaMDcxFzAVBgNV
BAoTDlJhY2tzcGFjZSBJbmMuMRwwGgYDVQQDExNSYWNrc3BhY2UgUm9vdCBDQSAx
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwWmZdFgUAmeHqijMGVc3
WyGSPZDelBfB88Cd91jRJCP2YhNcXG0RMwBYTYsJwI2BI5tSdwDIfDb32XqfbPKd
or6oLITDNzLoOQN1klSnzV7eVuMBQfo5We+EcYWaqYUqgFbDRx/yLugogADhFDT9
0M63aaK1Plixeyw+oajjYxbrRiMq+p2O9m4XijEqOK+vHxLTBLDfZ0S+DMXThAFD
XKGhNnKM2j8XEIEnLjuMyiZNuQEfSdhJigr7iDizaWWdrBYfZLWgDCjF8CX3xZxg
Ft51Po0sq6bDsGayOn7POrvK8m2PrZCXvUAXc3OXv14JiGa/IIM+f+sBNBUXvV3F
uwWMzmPqKHmP8nZN8VYQzNhQ0NrtXzgClbHYFjCoOdGHzoQzOcD//vUUvcL3Pd1a
CzpKSivxVLr8EdZhCjW8QzGXr0lMK9ajeCEZghMfV7e7XlKSj+zL7NFYkKYKf50v
ChjERQ0VM6Ty87l2EMHagwrHL6hI0L7O/Bquoc3RcqEOqBT5wjjuyQZOe3Q7X2Fw
rcsVnRmgFv3fE/MARwL5uKdc8JJopuU8EILxxSBv4CWMVMtAaeoFmlLskoeGMHHP
fp1qHNIEopt/8i0BsN7VOyyWcqbuy0vxgNv6+5BK9YlxitxR9pC0+EhbsP2yzKOW
/wGsphP2m9X91zbKr7Q4MEUCAwEAAaNUMFIwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
EwEB/wQFMAMBAf8wHQYDVR0OBBYEFIMvOaa5mQFs2X4W279J8Oyw1CvGMBAGCSsG
AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBCwUAA4ICAQBbEUAHbBLGdeLYNCJSfwM2
Jhs2Y+ZPxn70slHST2fr7jorg++/1GwPKb/QnyLzYd7d4Xv0umtoMsb1WwuIdzvY
vSgQLCk+ueoJYQv6B0yodByLBNDusWkdfGyOt1PK6HDfGJrG2L61ZwAvBNvBceox
W0fNuj28PiCAiiyIv4rsjFtzIZlUUmYoJ5RIY1WfW1UhpT6SNW9EgOBzpaXlpGX3
ZBzd4SIWOhGUZH00/ZAhqVwYYyHnpituTj/Gz2c5if3VDOb7Gzo1rFnnjh0mv8I6
9a8HvFUZguQnobxwCEIPzo5z6v+l1/Wp/Kl0obztZw5Z/eK75vIF40BjpS7/uSkN
md8mmsm4fRoWNj9VxOSIaPcPsfhTFTdVdeVYGMVtCfkpfvAxFog8Vsuzw13KeqNJ
nlbkALs+AJ70BQcLSaHEvc+B+g5mDWrQWtCYwrcO6wVPEuK8pDV9U7UvO89Wh5P+
cq42OAJ0HxOZmLECuuBw+ynjShPcrmGx0GodNVptm81BrFjO04xWWCvyXKrC7L2o
7we0xbpp3Iv0WlwdikOI2rZ40L2sNoiGUhFLqRA3dHIGmVsl0CltqdTB0x2i0TVt
UUdQhV6wj8J6BZ/kBdGnCPLsnhKOMWgsTogpQv/FwBDneSO8WlBfLREMsIGB1wXx
6kexs/oEWYwL7j4KtYqxWg==
-----END CERTIFICATE-----
`
)

var ldapCACertPool = x509.NewCertPool()

func init() {
	ldapCACertPool.AppendCertsFromPEM([]byte(ldapCACertPEM))
}

type (
	// BindCreds are used for authenticating to the LDAP server. Leave BindDN blank if you don't know it.
	BindCreds struct {
		BindDN string
		User   string
		Pass   string
	}
	// UserAttribs provides access to LDAP results parsed by the ldap library.
	// For normal users, sAMAccountName = SSO username
	UserAttribs struct {
		sAMAccountName string
		attribs        []*ldap.EntryAttribute
	}
	// UserGroup provides a simple listing of username and group memberships. Some of the nuance of the actual LDAP
	// entry is lost.
	UserGroup struct {
		SSO    string
		Groups []string
	}

	// UserAccount represents an easily decipherable way to see details about a user from the FindAccount
	// AD search function.
	UserAccount struct {
		SSO       string
		CN        string
		Title     string
		Thumbnail string
		Mobile    string
		EMail     string
		Status    string
	}
)

func (b *BindCreds) complete() error {
	if len(b.BindDN) < 1 {
		if len(b.User) < 1 {
			return errors.New("no User or BindDN provided for LDAP binding")
		}
		b.BindDN = fmt.Sprintf("rackspace\\%s", b.User)
	}
	return nil
}

// Bind connects to the LDAP server and sets the credentials to be used for subsequent queries.
// Returns an open connection. Don't forget to close it when you're done with it.
func Bind(b BindCreds) (*ldap.Conn, error) {
	err := b.complete()
	if err != nil {
		return nil, err
	}
	c, err := ldap.DialTLS(
		"tcp",
		fmt.Sprintf("%s:%d", ldapHost, ldapTLSPort),
		&tls.Config{RootCAs: ldapCACertPool, ServerName: ldapHost},
	)
	if err != nil {
		return nil, err
	}
	err = c.Bind(b.BindDN, b.Pass)
	if err != nil {
		c.Close()
		return nil, err
	}
	return c, nil
}

// GetGroupMemberships returns detailed LDAP data on the memberships of a given user
func GetGroupMemberships(l *ldap.Conn, user string) (UserAttribs, error) {
	var ret UserAttribs

	searchRequest := ldap.NewSearchRequest(
		"dc=rackspace,dc=corp",
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
		fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(samAccountName=%v))", user),
		[]string{"sAMAccountName", "memberOf"},
		nil,
	)

	sr, err := l.Search(searchRequest)
	if err != nil {
		return ret, err
	}
	if len(sr.Entries) == 0 {
		return ret, fmt.Errorf("no LDAP results for user %v", user)
	} else if len(sr.Entries) > 1 {
		return ret, fmt.Errorf("multiple LDAP results for user %v", user)
	}
	entry := sr.Entries[0]
	saNames := entry.GetAttributeValues("sAMAccountName")
	if len(saNames) != 1 {
		return ret, errors.New("LDAP entry has no sAMAccountName attribute")
	}
	ret.sAMAccountName = saNames[0]
	ret.attribs = entry.Attributes
	return ret, nil
}

// FindAccount returns detailed LDAP data on the memberships of a given user
func FindAccount(l *ldap.Conn, user string) ([]UserAccount, error) {
	ret := make([]UserAccount, 0)
	searchRequest := ldap.NewSearchRequest(
		"dc=rackspace,dc=corp",
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
		fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(anr=%s))", user),
		[]string{"sAMAccountName", "cn", "title", "thumbnailPhoto", "mobile"},
		nil,
	)

	sr, err := l.Search(searchRequest)
	if err != nil {
		return ret, err
	}
	if len(sr.Entries) == 0 {
		return ret, fmt.Errorf("no LDAP results for user %v", user)
	}
	for _, entry := range sr.Entries {
		ua := UserAccount{SSO: entry.GetAttributeValue("sAMAccountName")}
		for _, attr := range entry.Attributes {
			if attr.Name == "cn" {
				ua.CN = attr.Values[0]
				continue
			}
			if attr.Name == "title" {
				ua.Title = attr.Values[0]
			}
		}
		ret = append(ret, ua)
	}
	return ret, nil
}

// UserInfo returns detailed LDAP data on the memberships of a given user
func UserInfo(l *ldap.Conn, uid string) (UserAccount, error) {
	searchRequest := ldap.NewSearchRequest(
		"dc=rackspace,dc=corp",
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
		fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(uid=%s))", uid),
		[]string{"sAMAccountName", "cn", "title", "thumbnailPhoto", "mobile", "mail", "employeeStatus"},
		nil,
	)

	sr, err := l.Search(searchRequest)
	if err != nil {
		return UserAccount{}, err
	}
	if len(sr.Entries) == 0 {
		return UserAccount{}, fmt.Errorf("no LDAP results for user %v", uid)
	}
	for _, entry := range sr.Entries {
		ua := UserAccount{SSO: entry.GetAttributeValue("sAMAccountName")}
		for _, attr := range entry.Attributes {
			switch attr.Name {
			case "cn":
				ua.CN = attr.Values[0]
			case "title":
				ua.Title = attr.Values[0]
			case "thumbnailPhoto":
				ua.Thumbnail = attr.Values[0]
			case "mobile":
				ua.Mobile = attr.Values[0]
			case "mail":
				ua.EMail = attr.Values[0]
			case "employeeStatus":
				ua.Status = attr.Values[0]
			}
		}
		return ua, nil
	}
	return UserAccount{}, fmt.Errorf("no LDAP results for user %v", uid)
}

// GetSimpleGroupMemberships uses GetGroupMemberships and then simplifies its output
func GetSimpleGroupMemberships(l *ldap.Conn, user string) (UserGroup, error) {
	var ug UserGroup
	ua, err := GetGroupMemberships(l, user)
	if err != nil {
		return ug, err
	}
	ug.SSO = ua.sAMAccountName
	for _, a := range ua.attribs {
		if a.Name == "memberOf" {
			for _, v := range a.Values {
				DN, err := ldap.ParseDN(v)
				if err != nil {
					return ug, fmt.Errorf("failed to parse DN string: %v", DN)
				}
				for _, dn := range DN.RDNs {
					for _, dna := range dn.Attributes {
						if dna.Type == "CN" {
							ug.Groups = append(ug.Groups, dna.Value)
						}
					}
				}
			}
		}
	}
	return ug, nil
}
