package ldaputil

import (
	"crypto/tls"
	"fmt"

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

type Config struct {
	Host       string
	Port       int
	UseTLS     bool
	BindDN     string
	Password   string
	BaseDN     string
	UserFilter string
	UserAttr   string
	FlagField  string
	TruthyVals []string
	Invert     bool
}

// FetchVlessFlags returns map[email]enabled
func FetchVlessFlags(cfg Config) (map[string]bool, error) {
	addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)

	scheme := "ldap"
	if cfg.UseTLS {
		scheme = "ldaps"
	}

	ldapURL := fmt.Sprintf("%s://%s", scheme, addr)

	var opts []ldap.DialOpt
	if cfg.UseTLS {
		opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{
			InsecureSkipVerify: false,
		}))
	}

	conn, err := ldap.DialURL(ldapURL, opts...)
	if err != nil {
		return nil, err
	}
	defer conn.Close()

	if cfg.BindDN != "" {
		if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
			return nil, err
		}
	}

	if cfg.UserFilter == "" {
		cfg.UserFilter = "(objectClass=person)"
	}
	if cfg.UserAttr == "" {
		cfg.UserAttr = "mail"
	}
	// if field not set we fallback to legacy vless_enabled
	if cfg.FlagField == "" {
		cfg.FlagField = "vless_enabled"
	}

	req := ldap.NewSearchRequest(
		cfg.BaseDN,
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
		cfg.UserFilter,
		[]string{cfg.UserAttr, cfg.FlagField},
		nil,
	)

	res, err := conn.Search(req)
	if err != nil {
		return nil, err
	}

	result := make(map[string]bool, len(res.Entries))
	for _, e := range res.Entries {
		user := e.GetAttributeValue(cfg.UserAttr)
		if user == "" {
			continue
		}
		val := e.GetAttributeValue(cfg.FlagField)
		enabled := false
		for _, t := range cfg.TruthyVals {
			if val == t {
				enabled = true
				break
			}
		}
		if cfg.Invert {
			enabled = !enabled
		}
		result[user] = enabled
	}
	return result, nil
}

// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
func AuthenticateUser(cfg Config, username, password string) (bool, error) {
	addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)

	scheme := "ldap"
	if cfg.UseTLS {
		scheme = "ldaps"
	}

	ldapURL := fmt.Sprintf("%s://%s", scheme, addr)

	var opts []ldap.DialOpt
	if cfg.UseTLS {
		opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{
			InsecureSkipVerify: false,
		}))
	}

	conn, err := ldap.DialURL(ldapURL, opts...)
	if err != nil {
		return false, err
	}
	defer conn.Close()

	// Optional initial bind for search
	if cfg.BindDN != "" {
		if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
			return false, err
		}
	}

	if cfg.UserFilter == "" {
		cfg.UserFilter = "(objectClass=person)"
	}
	if cfg.UserAttr == "" {
		cfg.UserAttr = "uid"
	}

	// Build filter to find specific user
	filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username))
	req := ldap.NewSearchRequest(
		cfg.BaseDN,
		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
		filter,
		[]string{"dn"},
		nil,
	)
	res, err := conn.Search(req)
	if err != nil {
		return false, err
	}
	if len(res.Entries) == 0 {
		return false, nil
	}
	userDN := res.Entries[0].DN
	// Try to bind as the user
	if err := conn.Bind(userDN, password); err != nil {
		return false, nil
	}
	return true, nil
}
