// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package role

import (
	"slices"
	"strings"

	"github.com/siderolabs/gen/maps"
)

// Role represents Talos user role.
// Its string value is used everywhere: as the Organization value of Talos client certificate,
// as the value of talosctl flag, etc.
type Role string

const (
	// Prefix for all built-in roles.
	Prefix = string("os:")

	// Admin defines Talos role for admins (every API is available).
	Admin = Role(Prefix + "admin")

	// Operator defines Talos role for operators (Reader + management APIs which do not allow secret access, e.g. rebooting a node).
	Operator = Role(Prefix + "operator")

	// Reader defines Talos role for readers who can access read-only APIs that do not expose secrets.
	Reader = Role(Prefix + "reader")

	// EtcdBackup defines Talos role that allows making etcd backups.
	EtcdBackup = Role(Prefix + "etcd:backup")

	// ImageVerifier defines Talos role that allows verifying images.
	ImageVerifier = Role(Prefix + "image:verifier")

	// MetaWriter defines Talos role that allows mutating META values (write and delete).
	MetaWriter = Role(Prefix + "meta:writer")

	// Impersonator defines Talos role for impersonating another user (and their role).
	// Used internally, but may also be granted to the user.
	Impersonator = Role(Prefix + "impersonator")
)

// Set represents a set of roles.
type Set struct {
	roles map[Role]struct{}
}

var (
	// All roles that can be granted to users.
	All = MakeSet(Admin, Operator, Reader, EtcdBackup, ImageVerifier, MetaWriter, Impersonator)

	// Zero is an empty set of roles.
	Zero = MakeSet()
)

// MakeSet makes a set of roles from constants.
// Use Parse in other cases.
func MakeSet(roles ...Role) Set {
	res := Set{
		roles: make(map[Role]struct{}, len(roles)),
	}
	for _, r := range roles {
		res.roles[r] = struct{}{}
	}

	return res
}

// Parse parses a set of roles.
// The returned set is always non-nil and contains all roles, including unknown (for compatibility with future versions).
// The returned slice contains roles unknown to the current version.
func Parse(str []string) (Set, []string) {
	res := MakeSet()

	var unknownRoles []string

	for _, r := range str {
		r = strings.TrimSpace(r)

		// Client certificates generated by previous Talos versions contained one empty organization.
		if r == "" {
			continue
		}

		role := Role(r)
		if _, ok := All.roles[role]; !ok {
			unknownRoles = append(unknownRoles, r)
		}

		res.roles[role] = struct{}{}
	}

	return res, unknownRoles
}

// Strings returns a set as a slice of strings.
func (s Set) Strings() []string {
	res := maps.KeysFunc(s.roles, func(r Role) string { return string(r) })
	slices.Sort(res)

	return res
}

// IncludesAny returns true if there is a non-empty intersection between sets.
//
// Returns false if any set is empty.
func (s Set) IncludesAny(other Set) bool {
	for r := range other.roles {
		if _, ok := s.roles[r]; ok {
			return true
		}
	}

	return false
}

// Includes returns true if given role is present in the set.
func (s Set) Includes(role Role) bool {
	_, ok := s.roles[role]

	return ok
}

// Intersect returns a new set which is an intersection of two sets.
func (s Set) Intersect(other Set) Set {
	res := MakeSet()

	for r := range s.roles {
		if _, ok := other.roles[r]; ok {
			res.roles[r] = struct{}{}
		}
	}

	return res
}
