package storage

import (
	"git.mstar.dev/mstar/goutils/sliceutils"
	"github.com/rs/zerolog/log"
	"gorm.io/gorm"

	"git.mstar.dev/mstar/linstrom/shared"
)

// Could I collapse all these go:generate command into more condensed ones?
// Yes
// Will I do that?
// No
// This is explicit in what is being done. And easier to understand

//go:generate go build -o RolesGenerator ../cmd/RolesGenerator/main.go
//go:generate ./RolesGenerator -input=roles.go -output=rolesshared_generated.go
//go:generate rm RolesGenerator

//go:generate go build -o ApiGenerator ../cmd/RolesApiTypeGenerator/main.go
//go:generate ./ApiGenerator -input=roles.go -output=../server/apiLinstromTypes_generated.go
//go:generate rm ApiGenerator

//go:generate go build -o HelperGenerator ../cmd/RolesApiConverter/main.go
//go:generate ./HelperGenerator -input=roles.go -output=../server/apiLinstromTypeHelpers_generated.go
//go:generate rm HelperGenerator

//go:generate go build -o FrontendGenerator ../cmd/RolesFrontendGenerator/main.go
//go:generate ./FrontendGenerator -input=roles.go -output=../frontend-reactive/app/models/role.ts
//go:generate rm FrontendGenerator

// A role is, in concept, similar to how Discord handles roles
// Some permission can be either disallowed (&false), don't care (nil) or allowed (&true)
// Don't care just says to use the value from the next lower role where it is set
type Role struct {
	// TODO: More control options
	// Extend upon whatever Masto, Akkoma and Misskey have
	// Lots of details please

	// --- Role metadata ---

	// Include full db model stuff
	gorm.Model

	// Name of the role
	Name string `gorm:"primaryKey;unique"`

	// Priority of the role
	// Lower priority gets applied first and thus overwritten by higher priority ones
	// If two roles have the same priority, the order is undetermined and may be random
	// Default priority for new roles is 1 to always overwrite default user
	// And full admin has max priority possible
	Priority uint32
	// Whether this role is for a for a single user only (like custom, per user permissions in Discord)
	// If yes, Name will be the id of the user in question
	IsUserRole bool

	// Whether this role is one built into Linstrom from the start or not
	// Note: Built-in roles can't be modified
	IsBuiltIn bool

	// --- User permissions ---
	CanSendMedia             *bool // Local & remote
	CanSendCustomEmotes      *bool // Local & remote
	CanSendCustomReactions   *bool // Local & remote
	CanSendPublicNotes       *bool // Local & remote
	CanSendLocalNotes        *bool // Local & remote
	CanSendFollowerOnlyNotes *bool // Local & remote
	CanSendPrivateNotes      *bool // Local & remote
	CanSendReplies           *bool // Local & remote
	CanQuote                 *bool // Local only
	CanBoost                 *bool // Local only
	CanIncludeLinks          *bool // Local & remote
	CanIncludeSurvey         *bool // Local
	CanFederateFedi          *bool // Local & remote
	CanFederateBsky          *bool // Local

	CanChangeDisplayName *bool // Local

	// Internal ids of accounts blocked by this role
	BlockedUsers     []string `gorm:"type:bytes;serializer:gob"` // Local
	CanSubmitReports *bool    // Local & remote
	// If disabled, an account can no longer be interacted with. The owner can no longer change anything about it
	// And the UI will show a notice (and maybe include that info in the AP data too)
	// Only moderators and admins will be able to edit the account's roles
	CanLogin *bool // Local

	CanMentionOthers     *bool   // Local & remote
	HasMentionCountLimit *bool   // Local & remote
	MentionLimit         *uint32 // Local & remote

	// CanViewBoosts *bool
	// CanViewQuotes *bool
	// CanViewMedia *bool
	// CanViewCustomEmotes *bool

	// --- Automod ---
	AutoNsfwMedia                *bool   // Local & remote
	AutoCwPosts                  *bool   // Local & remote
	AutoCwPostsText              *string // Local & remote
	ScanCreatedPublicNotes       *bool   // Local & remote
	ScanCreatedLocalNotes        *bool   // Local & remote
	ScanCreatedFollowerOnlyNotes *bool   // Local & remote
	ScanCreatedPrivateNotes      *bool   // Local & remote
	// Blocks all interactions and federation between users with the role and all included ids/handles
	// TODO: Decide whether this is a list of handles or of account ids
	// Handles would increase the load due to having to search for them first
	// while ids would require to store every single account mentioned
	// which could cause escalating storage costs
	DisallowInteractionsWith []string `gorm:"type:bytes;serializer:gob"` // Local & remote

	WithholdNotesForManualApproval *bool    // Local & remote
	WithholdNotesBasedOnRegex      *bool    // Local & remote
	WithholdNotesRegexes           []string `gorm:"type:bytes;serializer:gob"` // Local & remote

	// --- Admin perms ---
	// If set, counts as all permissions being set as given and all restrictions being disabled
	FullAdmin                          *bool // Local
	CanAffectOtherAdmins               *bool // Local
	CanDeleteNotes                     *bool // Local
	CanConfirmWithheldNotes            *bool // Local
	CanAssignRoles                     *bool // Local
	CanSupressInteractionsBetweenUsers *bool // Local
	CanOverwriteDisplayNames           *bool // Local
	CanManageCustomEmotes              *bool // Local
	CanViewDeletedNotes                *bool // Local
	CanRecoverDeletedNotes             *bool // Local
	CanManageAvatarDecorations         *bool // Local
	CanManageAds                       *bool // Local
	CanSendAnnouncements               *bool // Local
	CanDeleteAccounts                  *bool // Local
}

/*
Mastodon permissions (highest permission to lowest):
- Admin
- Devops
- View Audit log
- View Dashboard
- Manage Reports
- Manage Federation
- Manage Settings
- Manage Blocks
- Manage Taxonomies
- Manage Appeals
- Manage Users
- Manage Invites
- Manage Rules
- Manage Announcements
- Manage Custom Emojis
- Manage Webhooks
- Invite Users
- Manage Roles
- Manage User Access
- Delete User Data
*/

/*
Misskey "permissions" (no order):
- Global timeline available (interact with global timeline I think)
- Local timeline available (same as global, but for local)
- b-something timeline available
- Can send public notes
- How many mentions a note can have
- Can invite others
- How many invites can be sent
- InviteLimitCycle (whatever that means)
- Invite Expiration time (duration of how long invites stay valid I think)
- Manage custom emojis
- Manage custom avatar decorations
- Seach for notes
- Use translator
- Hide ads from self
- How much storage space the user has
- Whether to mark all posts from account as nsfw
- max number of pinned messages
- max number of antennas
- max number of muted words
- max number of webhooks
- max number of clips
- max number of notes contained in a clip (? I think. Don't know enough about clips)
- Max number of lists of users
- max number of users in a user list
- rate limit multiplier
- max number of applied avatar decorations
*/

func (s *Storage) NewEmptyRole(name string) (*Role, error) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	// Check if a role with the given name already exists
	_, err := s.FindRoleByName(name)
	switch err {
	case nil:
		return nil, ErrEntryAlreadyExists
	case ErrEntryNotFound: // Empty case, since this is what we want
	default:
		return nil, err
	}

	// New roles have a priority of 1 by default
	newRole := Role{Name: name, Priority: 1}
	err = s.db.Create(&newRole).Error
	if err != nil {
		return nil, err
	}
	return &newRole, nil
}

func (s *Storage) FindRoleByName(name string) (*Role, error) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	role := Role{}
	err := s.db.Where("name = ?", name).First(&role).Error
	switch err {
	case nil:
		return &role, nil
	case gorm.ErrRecordNotFound:
		return nil, ErrEntryNotFound
	default:
		return nil, err
	}
}

func (s *Storage) FindRolesByNames(names []string) ([]Role, error) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	roles := []Role{}
	err := s.db.Where("name IN ?", names).Find(&roles).Error
	switch err {
	case nil:
		return roles, nil
	case gorm.ErrRecordNotFound:
		return nil, ErrEntryNotFound
	default:
		return nil, err
	}
}

func (s *Storage) UpdateRole(role *Role) error {
	defer shared.Untrace(shared.Trace(&log.Logger))
	return s.db.Save(role).Error
}

func (s *Storage) DeleteRoleByName(name string) error {
	// Prevent deletion of built-in roles
	if sliceutils.Contains(
		sliceutils.Map(allDefaultRoles, func(t *Role) string { return t.Name }),
		name,
	) {
		return ErrNotAllowed
	}
	defer shared.Untrace(shared.Trace(&log.Logger))
	return s.db.Where(&Role{Name: name, IsBuiltIn: false}).Delete(&Role{}).Error
}
