package acl

import (
	"context"
	"net/http"
	"strings"

	"connectrpc.com/connect"
	"github.com/OliveTin/OliveTin/internal/auth"
	config "github.com/OliveTin/OliveTin/internal/config"
	log "github.com/sirupsen/logrus"

	"golang.org/x/exp/slices"
)

type PermissionBits int

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

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

// User respresents a person.
type AuthenticatedUser struct {
	Username      string
	UsergroupLine string

	Provider string
	SID      string

	Acls []string

	EffectivePolicy *config.ConfigurationPolicy
}

func (u *AuthenticatedUser) IsGuest() bool {
	return u.Username == "guest" && u.Provider == "system"
}

func (u *AuthenticatedUser) parseUsergroupLine(sep string) []string {
	ret := []string{}

	if sep != "" {
		for _, v := range strings.Split(u.UsergroupLine, sep) {
			trimmed := strings.TrimSpace(v)

			if trimmed != "" {
				ret = append(ret, trimmed)
			}
		}
	} else {
		ret = strings.Fields(u.UsergroupLine)
	}

	log.Debugf("parseUsergroupLine: %v, %v, sep:%v", u.UsergroupLine, ret, sep)

	return ret
}

func (u *AuthenticatedUser) matchesUsergroupAcl(matchUsergroups []string, sep string) bool {
	groupList := u.parseUsergroupLine(sep)

	for _, group := range groupList {
		if slices.Contains(matchUsergroups, group) {
			log.Debugf("Usergroup %v found in %+v (len: %v)", group, groupList, len(groupList))
			return true
		}
	}

	return false
}

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

func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
	actionTitle := "N/A"

	if action != nil {
		actionTitle = action.Title
	}

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

func logAclNoneMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, defaultPermission bool) {
	if cfg.LogDebugOptions.AclNoneMatched {
		log.WithFields(log.Fields{
			"User":    user.Username,
			"Action":  action.Title,
			"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 *AuthenticatedUser, action *config.Action) bool {
	relevantAcls := getRelevantAcls(cfg, action.Acls, user)

	if cfg.LogDebugOptions.AclCheckStarted {
		log.WithFields(log.Fields{
			"actionTitle":        action.Title,
			"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, action, acl)

			return true
		} else {
			logAclNotMatched(cfg, aclFunction, user, action, acl)
		}
	}

	logAclNoneMatched(cfg, aclFunction, user, action, cfg.DefaultPermissions.Logs)

	return defaultValue
}

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

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

// IsAllowedView checks if a User is allowed to view an Action
func IsAllowedView(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
	if action.Hidden {
		return false
	}

	return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action)
}

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

func getHeaderKeyOrEmpty(headers http.Header, key string) string {
	values := headers.Values(key)
	if len(values) > 0 {
		return values[0]
	}
	return ""
}

// UserFromContext tries to find a user from a Connect RPC context
func UserFromContext[T any](ctx context.Context, req *connect.Request[T], cfg *config.Config) *AuthenticatedUser {
	user := userFromHeaders(req, cfg)
	if user.Username == "" {
		user = userFromLocalSession(req, cfg, user)
	}
	if user.Username == "" {
		user = *UserGuest(cfg)
	} else {
		buildUserAcls(cfg, &user)
	}
	log.WithFields(log.Fields{
		"username":      user.Username,
		"usergroupLine": user.UsergroupLine,
		"provider":      user.Provider,
		"acls":          user.Acls,
	}).Debugf("UserFromContext")
	return &user
}

//gocyclo:ignore
func userFromHeaders[T any](req *connect.Request[T], cfg *config.Config) AuthenticatedUser {
	var u AuthenticatedUser
	if req == nil {
		return u
	}
	if cfg.AuthHttpHeaderUsername != "" {
		u.Username = getHeaderKeyOrEmpty(req.Header(), cfg.AuthHttpHeaderUsername)
	}
	if cfg.AuthHttpHeaderUserGroup != "" {
		u.UsergroupLine = getHeaderKeyOrEmpty(req.Header(), cfg.AuthHttpHeaderUserGroup)
	}
	if prov := getHeaderKeyOrEmpty(req.Header(), "provider"); prov != "" {
		u.Provider = prov
	}
	return u
}

//gocyclo:ignore
func userFromLocalSession[T any](req *connect.Request[T], cfg *config.Config, u AuthenticatedUser) AuthenticatedUser {
	if req == nil || u.Username != "" {
		return u
	}
	dummy := &http.Request{Header: req.Header()}
	c, err := dummy.Cookie("olivetin-sid-local")
	if err != nil || c == nil || c.Value == "" {
		return u
	}
	sess := auth.GetUserSession("local", c.Value)
	if sess == nil {
		log.WithFields(log.Fields{"sid": c.Value, "provider": "local"}).Warn("UserFromContext: stale local session")
		return u
	}
	if cfgUser := cfg.FindUserByUsername(sess.Username); cfgUser != nil {
		u.Username = cfgUser.Username
		u.UsergroupLine = cfgUser.Usergroup
		u.Provider = "local"
		u.SID = c.Value
		return u
	}
	log.WithFields(log.Fields{"username": sess.Username}).Warn("UserFromContext: local session user not in config")
	return u
}

func UserGuest(cfg *config.Config) *AuthenticatedUser {
	ret := &AuthenticatedUser{}
	ret.Username = "guest"
	ret.UsergroupLine = "guest"
	ret.Provider = "system"

	buildUserAcls(cfg, ret)

	return ret
}

func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser {
	ret := &AuthenticatedUser{
		Username:      username,
		UsergroupLine: "system",
		Provider:      "system",
	}

	buildUserAcls(cfg, ret)

	return ret
}

func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
	for _, acl := range cfg.AccessControlLists {
		if slices.Contains(acl.MatchUsernames, user.Username) {
			user.Acls = append(user.Acls, acl.Name)
			continue
		}

		if user.matchesUsergroupAcl(acl.MatchUsergroups, cfg.AuthHttpHeaderUserGroupSep) {
			user.Acls = append(user.Acls, acl.Name)
			continue
		}
	}

	user.EffectivePolicy = getEffectivePolicy(cfg, user)
}

func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool {
	if !slices.Contains(user.Acls, acl.Name) {
		// If the user does not have this ACL, then it is not relevant

		return false
	}

	if acl.AddToEveryAction {
		return true
	}

	if slices.Contains(actionAcls, acl.Name) {
		return true
	}

	return false
}

func getRelevantAcls(cfg *config.Config, actionAcls []string, user *AuthenticatedUser) []*config.AccessControlList {
	var ret []*config.AccessControlList

	for _, acl := range cfg.AccessControlLists {
		if isACLRelevantToAction(cfg, actionAcls, acl, user) {
			ret = append(ret, acl)
		}
	}

	return ret
}

func getEffectivePolicy(cfg *config.Config, user *AuthenticatedUser) *config.ConfigurationPolicy {
	ret := &config.ConfigurationPolicy{
		ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics,
		ShowLogList:     cfg.DefaultPolicy.ShowLogList,
	}

	for _, acl := range cfg.AccessControlLists {
		if slices.Contains(user.Acls, acl.Name) {
			logAclMatched(cfg, "GetEffectivePolicy", user, nil, acl)

			ret = buildConfigurationPolicy(ret, acl.Policy)
		}
	}

	return ret
}

func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.ConfigurationPolicy) *config.ConfigurationPolicy {
	if policy.ShowDiagnostics {
		ret.ShowDiagnostics = policy.ShowDiagnostics
	}

	if policy.ShowLogList {
		ret.ShowLogList = policy.ShowLogList
	}

	return ret
}
