package models

import (
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strings"
	"time"

	"code.nonshy.com/nonshy/website/pkg/log"
	"gorm.io/gorm"
)

// AdminGroup table gives a name to a set of admin permission scopes.
type AdminGroup struct {
	ID        uint64 `gorm:"primaryKey"`
	Name      string `gorm:"index"`
	CreatedAt time.Time
	UpdatedAt time.Time
	Scopes    []AdminScope `gorm:"foreignKey:GroupID"`
	Users     []*User      `gorm:"many2many:admin_group_users;"`
}

// AdminScope table maps admin group IDs to named scopes (which may contain wildcards).
type AdminScope struct {
	ID      uint64 `gorm:"primaryKey"`
	GroupID uint64 `gorm:"index"`
	Scope   string
}

// Preload related tables for the AdminGroup.
func (g *AdminGroup) Preload() *gorm.DB {
	return DB.Preload("Scopes").Preload("Users")
}

// HasAdminScope returns whether the user is an admin and has the requested scope.
func (u *User) HasAdminScope(scope string) bool {
	if !u.IsAdmin {
		return false
	}

	for _, group := range u.AdminGroups {
		for _, compare := range group.Scopes {
			// Scope has a wildcard?
			if strings.ContainsRune(compare.Scope, '*') {
				re := regexp.MustCompile(
					fmt.Sprintf(`^%s$`, strings.ReplaceAll(compare.Scope, "*", ".+?")),
				)
				if res := re.FindStringSubmatch(scope); len(res) > 0 {
					log.Debug("Regexp scope '%s' matched requested '%s'", compare.Scope, scope)
					return true
				}
			} else if compare.Scope == scope {
				return true
			}
		}
	}

	return false
}

// ListAdminGroups returns the list of admin group (names) the user belongs to.
func (u *User) ListAdminGroups() []string {
	var names = []string{}
	for _, group := range u.AdminGroups {
		names = append(names, group.Name)
	}
	sort.Strings(names)
	return names
}

// ListAdminScopes returns the list of admin group (names) the user belongs to.
func (u *User) ListAdminScopes() []string {
	var (
		names    = []string{}
		distinct = map[string]interface{}{}
	)
	for _, group := range u.AdminGroups {
		for _, scope := range group.Scopes {
			if _, ok := distinct[scope.Scope]; ok {
				continue
			}
			distinct[scope.Scope] = nil
			names = append(names, scope.Scope)
		}
	}
	sort.Strings(names)
	return names
}

// CreateAdminGroup inserts a new admin group.
func CreateAdminGroup(name string, scopes []string) (*AdminGroup, error) {
	// Verify the name is unique.
	if _, err := FindAdminGroup(name); err == nil {
		return nil, fmt.Errorf("that group name already exists: %s", err)
	}

	g := &AdminGroup{
		Name:   name,
		Scopes: []AdminScope{},
	}
	for _, scope := range scopes {
		scope = strings.TrimSpace(scope)
		if scope != "" {
			g.Scopes = append(g.Scopes, AdminScope{
				Scope: scope,
			})
		}
	}

	result := DB.Create(g)
	return g, result.Error
}

// FindAdminGroup by name.
func FindAdminGroup(name string) (*AdminGroup, error) {
	if name == "" {
		return nil, errors.New("username is required")
	}

	g := &AdminGroup{}
	result := g.Preload().Where("name = ?", name).Limit(1).First(g)
	return g, result.Error
}

// ListAdminGroups returns all admin groups.
func ListAdminGroups() ([]*AdminGroup, error) {
	var groups = []*AdminGroup{}

	result := (&AdminGroup{}).Preload().Order("name asc").Find(&groups)
	return groups, result.Error
}

// ListAdminUsers returns all admin user accounts.
func ListAdminUsers() ([]*User, error) {
	var users = []*User{}

	result := (&User{}).Preload().Where("is_admin is true").Order("id asc").Find(&users)
	return users, result.Error
}

// ScopesString returns the scopes as a newline separated string.
func (g *AdminGroup) ScopesString() string {
	var scopes = []string{}
	for _, scope := range g.Scopes {
		scopes = append(scopes, scope.Scope)
	}
	return strings.Join(scopes, "\n")
}

// ReplaceScopes sets new scopes for the admin group.
func (g *AdminGroup) ReplaceScopes(scopes []string) error {
	// Delete the original scopes.
	var replace = []AdminScope{}
	for _, scope := range scopes {
		scope = strings.TrimSpace(scope)
		if scope != "" {
			replace = append(replace, AdminScope{
				Scope: scope,
			})
		}
	}

	err := DB.Model(g).Association("Scopes").Replace(replace)

	// Cleanup orphaned scopes.
	if result := DB.Where("group_id IS NULL").Delete(&AdminScope{}); result.Error != nil {
		log.Error("AdminGroup.ReplaceScopes: cleanup orphaned scopes: %s", result.Error)
	}

	return err
}

// HasAdmin returns whether the given username is a member of the group.
func (g *AdminGroup) HasAdmin(username string) bool {
	for _, user := range g.Users {
		if user.Username == username {
			return true
		}
	}
	return false
}

// ReplaceUsers easily adds or removes admin users from a group.
//
// Post the full list of usernames who should be in the group, and it will add or
// remove users as needed and return which users were changed.
func (g *AdminGroup) ReplaceUsers(usernames []string) (added, removed []string, err error) {
	added = []string{}
	removed = []string{}

	// Map who is currently in the group.
	var currentUsernames = map[string]interface{}{}
	var allUsernames = []string{}
	for _, user := range g.Users {
		currentUsernames[user.Username] = nil
		allUsernames = append(allUsernames, user.Username)
	}

	// Map the incoming username list.
	var incomingUsernames = map[string]interface{}{}

	// Who needs added?
	for _, username := range usernames {
		incomingUsernames[username] = nil
		allUsernames = append(allUsernames, username)
		if _, ok := currentUsernames[username]; !ok {
			added = append(added, username)
		}
	}

	// Who needs removed?
	for username := range currentUsernames {
		if _, ok := incomingUsernames[username]; !ok {
			removed = append(removed, username)
		}
	}

	log.Info("AdminGroup(%s).ReplaceUsers: complete list %s (adding: %s) (removing: %s)",
		g.Name,
		usernames,
		added,
		removed,
	)

	// Select all affected users from the DB.
	usermap, err := MapUsersByUsername(allUsernames)
	if err != nil {
		return
	}

	// Do the needful.
	for _, username := range added {
		log.Info("AdminGroup(%s).ReplaceUsers: ADD user %s", g.Name, username)
		if err := g.AddUser(usermap[username]); err != nil {
			return added, removed, fmt.Errorf("adding user %s: %s", username, err)
		}
	}
	for _, username := range removed {
		log.Info("AdminGroup(%s).ReplaceUsers: REMOVE user %s", g.Name, username)
		if err := g.RemoveUser(usermap[username]); err != nil {
			return added, removed, fmt.Errorf("removing user %s: %s", username, err)
		}
	}

	return
}

// AddUser puts the given user into the admin group.
func (g *AdminGroup) AddUser(user *User) error {
	if user.AdminGroups == nil {
		user.AdminGroups = []*AdminGroup{}
	}

	// Already exists?
	for _, group := range user.AdminGroups {
		if group.ID == g.ID {
			return nil
		}
	}

	// Insert them.
	user.AdminGroups = append(user.AdminGroups, g)
	return user.Save()
}

// RemoveUser removes a user from the admin group.
func (g *AdminGroup) RemoveUser(user *User) error {
	if user.AdminGroups == nil {
		return errors.New("user had no admin groups")
	}

	return DB.Model(user).Association("AdminGroups").Delete(g)
}

// Delete the AdminGroup.
func (g *AdminGroup) Delete() error {
	// Remove scopes.
	if result := DB.Where("group_id = ?", g.ID).Delete(&AdminScope{}); result.Error != nil {
		return fmt.Errorf("can't remove scopes for group ID %d: %s", g.ID, result.Error)
	}

	// Remove users.
	if _, _, err := g.ReplaceUsers([]string{}); err != nil {
		return fmt.Errorf("can't remove users for group ID %d: %s", g.ID, err)
	}

	return DB.Delete(g).Error
}
