package acl

import (
	authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
	config "github.com/OliveTin/OliveTin/internal/config"
	log "github.com/sirupsen/logrus"

	"slices"
)

type PermissionBits int

const (
	View PermissionBits = 1 << iota
	Exec
	Logs
	Kill
)

func (p PermissionBits) Has(permission PermissionBits) bool {
	return p&permission != 0
}

func logAclNotMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, acl *config.AccessControlList) {
	if cfg.LogDebugOptions.AclNotMatched {
		log.WithFields(log.Fields{
			"User":     user.Username,
			"Resource": resourceTitle,
			"ACL":      acl.Name,
		}).Debugf("%v - ACL Not Matched", aclFunction)
	}
}

func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, acl *config.AccessControlList) {
	if resourceTitle == "" {
		resourceTitle = "N/A"
	}

	if cfg.LogDebugOptions.AclMatched {
		log.WithFields(log.Fields{
			"User":     user.Username,
			"Resource": resourceTitle,
			"ACL":      acl.Name,
		}).Debugf("%v - Matched ACL", aclFunction)
	}
}

func logAclNoneMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, defaultPermission bool) {
	if cfg.LogDebugOptions.AclNoneMatched {
		log.WithFields(log.Fields{
			"User":     user.Username,
			"Resource": resourceTitle,
			"Default":  defaultPermission,
		}).Debugf("%v - No ACLs Matched, returning default permission", aclFunction)
	}
}

func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits {
	type permPair struct {
		enabled bool
		bit     PermissionBits
	}

	permMap := []permPair{
		{permissions.View, View},
		{permissions.Exec, Exec},
		{permissions.Logs, Logs},
		{permissions.Kill, Kill},
	}

	var ret PermissionBits

	for _, perm := range permMap {
		if perm.enabled {
			ret |= perm.bit
		}
	}

	return ret
}

func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, resourceAcls []string, includeAddToEvery bool) bool {
	relevantAcls := getRelevantAcls(cfg, resourceAcls, user, includeAddToEvery)

	if cfg.LogDebugOptions.AclCheckStarted {
		log.WithFields(log.Fields{
			"resourceTitle":      resourceTitle,
			"username":           user.Username,
			"usergroupLine":      user.UsergroupLine,
			"relevantAcls":       len(relevantAcls),
			"requiredPermission": requiredPermission,
		}).Debugf("ACL check - %v", aclFunction)
	}

	for _, acl := range relevantAcls {
		permissionBits := permissionsConfigToBits(acl.Permissions)

		if permissionBits.Has(requiredPermission) {
			logAclMatched(cfg, aclFunction, user, resourceTitle, acl)

			return true
		}

		logAclNotMatched(cfg, aclFunction, user, resourceTitle, acl)
	}

	logAclNoneMatched(cfg, aclFunction, user, resourceTitle, defaultValue)

	return defaultValue
}

// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
func IsAllowedLogs(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
	return aclCheck(Logs, cfg.DefaultPermissions.Logs, cfg, "isAllowedLogs", user, action.Title, action.Acls, true)
}

// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
func IsAllowedExec(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
	return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action.Title, action.Acls, true)
}

// IsAllowedView checks if a User is allowed to view an Action.
// Action.Hidden is not a security control — it only affects dashboard listing.
func IsAllowedView(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
	return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action.Title, action.Acls, true)
}

func IsAllowedKill(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
	return aclCheck(Kill, cfg.DefaultPermissions.Kill, cfg, "isAllowedKill", user, action.Title, action.Acls, true)
}

// IsAllowedViewDashboard checks if a user may see a root dashboard.
// Dashboards with no acls are unrestricted. AddToEveryAction does not apply.
func IsAllowedViewDashboard(cfg *config.Config, user *authpublic.AuthenticatedUser, dashboard *config.DashboardComponent) bool {
	if dashboard == nil || len(dashboard.Acls) == 0 {
		return true
	}

	return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedViewDashboard", user, dashboard.Title, dashboard.Acls, false)
}

// IsAllowedViewEntityType checks if a user may see an entity type (list, details, search).
// Entity types with no acls are unrestricted. AddToEveryAction does not apply.
func IsAllowedViewEntityType(cfg *config.Config, user *authpublic.AuthenticatedUser, entityFile *config.EntityFile) bool {
	if entityFile == nil || len(entityFile.Acls) == 0 {
		return true
	}

	return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedViewEntityType", user, entityFile.Name, entityFile.Acls, false)
}

func isACLRelevant(resourceAcls []string, acl *config.AccessControlList, user *authpublic.AuthenticatedUser, includeAddToEvery bool) bool {
	if !slices.Contains(user.Acls, acl.Name) {
		return false
	}

	if includeAddToEvery && acl.AddToEveryAction {
		return true
	}

	return slices.Contains(resourceAcls, acl.Name)
}

func getRelevantAcls(cfg *config.Config, resourceAcls []string, user *authpublic.AuthenticatedUser, includeAddToEvery bool) []*config.AccessControlList {
	var ret []*config.AccessControlList

	for _, acl := range cfg.AccessControlLists {
		if isACLRelevant(resourceAcls, acl, user, includeAddToEvery) {
			ret = append(ret, acl)
		}
	}

	return ret
}
