package service

import (
	"crypto/tls"
	"fmt"
	"log"
	"strings"

	"github.com/go-ldap/ldap/v3"
	"github.com/stonith404/pocket-id/backend/internal/dto"
	"github.com/stonith404/pocket-id/backend/internal/model"
	"gorm.io/gorm"
)

type LdapService struct {
	db               *gorm.DB
	appConfigService *AppConfigService
	userService      *UserService
	groupService     *UserGroupService
}

func NewLdapService(db *gorm.DB, appConfigService *AppConfigService, userService *UserService, groupService *UserGroupService) *LdapService {
	return &LdapService{db: db, appConfigService: appConfigService, userService: userService, groupService: groupService}
}

func (s *LdapService) createClient() (*ldap.Conn, error) {
	if s.appConfigService.DbConfig.LdapEnabled.Value != "true" {
		return nil, fmt.Errorf("LDAP is not enabled")
	}
	// Setup LDAP connection
	ldapURL := s.appConfigService.DbConfig.LdapUrl.Value
	skipTLSVerify := s.appConfigService.DbConfig.LdapSkipCertVerify.Value == "true"
	client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: skipTLSVerify}))
	if err != nil {
		return nil, fmt.Errorf("failed to connect to LDAP: %w", err)
	}

	// Bind as service account
	bindDn := s.appConfigService.DbConfig.LdapBindDn.Value
	bindPassword := s.appConfigService.DbConfig.LdapBindPassword.Value
	err = client.Bind(bindDn, bindPassword)
	if err != nil {
		return nil, fmt.Errorf("failed to bind to LDAP: %w", err)
	}
	return client, nil
}

func (s *LdapService) SyncAll() error {
	err := s.SyncUsers()
	if err != nil {
		return fmt.Errorf("failed to sync users: %w", err)
	}

	err = s.SyncGroups()
	if err != nil {
		return fmt.Errorf("failed to sync groups: %w", err)
	}

	return nil
}

func (s *LdapService) SyncGroups() error {
	// Setup LDAP connection
	client, err := s.createClient()
	if err != nil {
		return fmt.Errorf("failed to create LDAP client: %w", err)
	}
	defer client.Close()

	baseDN := s.appConfigService.DbConfig.LdapBase.Value
	nameAttribute := s.appConfigService.DbConfig.LdapAttributeGroupName.Value
	uniqueIdentifierAttribute := s.appConfigService.DbConfig.LdapAttributeGroupUniqueIdentifier.Value
	filter := "(objectClass=groupOfUniqueNames)"

	searchAttrs := []string{
		nameAttribute,
		uniqueIdentifierAttribute,
		"member",
	}

	searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{})
	result, err := client.Search(searchReq)
	if err != nil {
		return fmt.Errorf("failed to query LDAP: %w", err)
	}

	// Create a mapping for groups that exist
	ldapGroupIDs := make(map[string]bool)

	for _, value := range result.Entries {
		var usersToAddDto dto.UserGroupUpdateUsersDto
		var membersUserId []string

		ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
		ldapGroupIDs[ldapId] = true

		// Try to find the group in the database
		var databaseGroup model.UserGroup
		s.db.Where("ldap_id = ?", ldapId).First(&databaseGroup)

		// Get group members and add to the correct Group
		groupMembers := value.GetAttributeValues("member")
		for _, member := range groupMembers {
			// Normal output of this would be CN=username,ou=people,dc=example,dc=com
			// Splitting at the "=" and "," then just grabbing the username for that string
			singleMember := strings.Split(strings.Split(member, "=")[1], ",")[0]

			var databaseUser model.User
			s.db.Where("username = ?", singleMember).First(&databaseUser)
			membersUserId = append(membersUserId, databaseUser.ID)
		}

		syncGroup := dto.UserGroupCreateDto{
			Name:         value.GetAttributeValue(nameAttribute),
			FriendlyName: value.GetAttributeValue(nameAttribute),
			LdapID:       value.GetAttributeValue(uniqueIdentifierAttribute),
		}

		usersToAddDto = dto.UserGroupUpdateUsersDto{
			UserIDs: membersUserId,
		}

		if databaseGroup.ID == "" {
			newGroup, err := s.groupService.Create(syncGroup)
			if err != nil {
				log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
			} else {
				if _, err = s.groupService.UpdateUsers(newGroup.ID, usersToAddDto); err != nil {
					log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
				}
			}
		} else {
			_, err = s.groupService.Update(databaseGroup.ID, syncGroup, true)
			_, err = s.groupService.UpdateUsers(databaseGroup.ID, usersToAddDto)
			if err != nil {
				log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
				return err
			}

		}

	}

	// Get all LDAP groups from the database
	var ldapGroupsInDb []model.UserGroup
	if err := s.db.Find(&ldapGroupsInDb, "ldap_id IS NOT NULL").Select("ldap_id").Error; err != nil {
		fmt.Println(fmt.Errorf("failed to fetch groups from database: %v", err))
	}

	// Delete groups that no longer exist in LDAP
	for _, group := range ldapGroupsInDb {
		if _, exists := ldapGroupIDs[*group.LdapID]; !exists {
			if err := s.db.Delete(&model.UserGroup{}, "ldap_id = ?", group.LdapID).Error; err != nil {
				log.Printf("Failed to delete group %s with: %v", group.Name, err)
			} else {
				log.Printf("Deleted group %s", group.Name)
			}
		}
	}

	return nil
}

func (s *LdapService) SyncUsers() error {
	// Setup LDAP connection
	client, err := s.createClient()
	if err != nil {
		return fmt.Errorf("failed to create LDAP client: %w", err)
	}
	defer client.Close()

	baseDN := s.appConfigService.DbConfig.LdapBase.Value
	uniqueIdentifierAttribute := s.appConfigService.DbConfig.LdapAttributeUserUniqueIdentifier.Value
	usernameAttribute := s.appConfigService.DbConfig.LdapAttributeUserUsername.Value
	emailAttribute := s.appConfigService.DbConfig.LdapAttributeUserEmail.Value
	firstNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserFirstName.Value
	lastNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserLastName.Value
	adminGroupAttribute := s.appConfigService.DbConfig.LdapAttributeAdminGroup.Value

	filter := "(objectClass=person)"

	searchAttrs := []string{
		"memberOf",
		"sn",
		"cn",
		uniqueIdentifierAttribute,
		usernameAttribute,
		emailAttribute,
		firstNameAttribute,
		lastNameAttribute,
	}

	// Filters must start and finish with ()!
	searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{})

	result, err := client.Search(searchReq)
	if err != nil {
		fmt.Println(fmt.Errorf("failed to query LDAP: %w", err))
	}

	// Create a mapping for users that exist
	ldapUserIDs := make(map[string]bool)

	for _, value := range result.Entries {
		ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
		ldapUserIDs[ldapId] = true

		// Get the user from the database
		var databaseUser model.User
		s.db.Where("ldap_id = ?", ldapId).First(&databaseUser)

		// Check if user is admin by checking if they are in the admin group
		isAdmin := false
		for _, group := range value.GetAttributeValues("memberOf") {
			if strings.Contains(group, adminGroupAttribute) {
				isAdmin = true
			}
		}

		newUser := dto.UserCreateDto{
			Username:  value.GetAttributeValue(usernameAttribute),
			Email:     value.GetAttributeValue(emailAttribute),
			FirstName: value.GetAttributeValue(firstNameAttribute),
			LastName:  value.GetAttributeValue(lastNameAttribute),
			IsAdmin:   isAdmin,
			LdapID:    ldapId,
		}

		if databaseUser.ID == "" {
			_, err = s.userService.CreateUser(newUser)
			if err != nil {
				log.Printf("Error syncing user %s: %s", newUser.Username, err)
			}
		} else {
			_, err = s.userService.UpdateUser(databaseUser.ID, newUser, false, true)
			if err != nil {
				log.Printf("Error syncing user %s: %s", newUser.Username, err)
			}

		}

	}

	// Get all LDAP users from the database
	var ldapUsersInDb []model.User
	if err := s.db.Find(&ldapUsersInDb, "ldap_id IS NOT NULL").Select("ldap_id").Error; err != nil {
		fmt.Println(fmt.Errorf("failed to fetch users from database: %v", err))
	}

	// Delete users that no longer exist in LDAP
	for _, user := range ldapUsersInDb {
		if _, exists := ldapUserIDs[*user.LdapID]; !exists {
			if err := s.db.Delete(&model.User{}, "ldap_id = ?", user.LdapID).Error; err != nil {
				log.Printf("Failed to delete user %s with: %v", user.Username, err)
			} else {
				log.Printf("Deleted user %s", user.Username)
			}
		}
	}
	return nil
}
