// Copyright (c) 2015-2023 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package policy

import (
	"encoding/json"
	"fmt"
	"sort"

	"github.com/minio/minio-go/v7/pkg/set"
)

// ActionSet - set of actions.
type ActionSet map[Action]struct{}

// Clone clones ActionSet structure
func (actionSet ActionSet) Clone() ActionSet {
	return NewActionSet(actionSet.ToSlice()...)
}

// Add - add action to the set.
func (actionSet ActionSet) Add(action Action) {
	actionSet[action] = struct{}{}
}

// Contains - checks given action exists in the action set.
func (actionSet ActionSet) Contains(action Action) bool {
	_, found := actionSet[action]
	return found
}

// IsEmpty - returns if the current action set is empty
func (actionSet ActionSet) IsEmpty() bool {
	return len(actionSet) == 0
}

var implicitActions = map[Action]ActionSet{
	GetObjectVersionAction: NewActionSet(GetObjectAction),

	// S3Tables actions implicitly allow their data actions
	S3TablesGetTableDataAction: NewActionSet(GetObjectAction, ListMultipartUploadPartsAction),
	S3TablesPutTableDataAction: NewActionSet(PutObjectAction, AbortMultipartUploadAction, ListBucketAction),
	// S3TablesDeleteTableAction implicitly allows DeleteObjectAction to support table purging.
	// This is needed because Spark's DROP TABLE ... PURGE performs client-side deletes rather than
	// using purgeRequested=true to let the catalog handle deletion. This workaround grants the
	// necessary privilege until the issue is fixed in Spark/Iceberg upstream.
	// See: https://github.com/apache/iceberg/issues/14743
	//      https://github.com/apache/iceberg/issues/11023
	S3TablesDeleteTableAction: NewActionSet(DeleteObjectAction),
	AllS3TablesActions:        NewActionSet(GetObjectAction, PutObjectAction, DeleteObjectAction, ListBucketAction, ListMultipartUploadPartsAction, AbortMultipartUploadAction),

	// TableBucket actions implicitly allow their Warehouse counterparts
	S3TablesCreateTableBucketAction:                      NewActionSet(S3TablesCreateWarehouseAction),
	S3TablesDeleteTableBucketAction:                      NewActionSet(S3TablesDeleteWarehouseAction),
	S3TablesDeleteTableBucketEncryptionAction:            NewActionSet(S3TablesDeleteWarehouseEncryptionAction),
	S3TablesDeleteTableBucketPolicyAction:                NewActionSet(S3TablesDeleteWarehousePolicyAction),
	S3TablesGetTableBucketAction:                         NewActionSet(S3TablesGetWarehouseAction),
	S3TablesGetTableBucketEncryptionAction:               NewActionSet(S3TablesGetWarehouseEncryptionAction),
	S3TablesGetTableBucketMaintenanceConfigurationAction: NewActionSet(S3TablesGetWarehouseMaintenanceConfigurationAction),
	S3TablesGetTableBucketPolicyAction:                   NewActionSet(S3TablesGetWarehousePolicyAction),
	S3TablesListTableBucketsAction:                       NewActionSet(S3TablesListWarehousesAction),
	S3TablesPutTableBucketEncryptionAction:               NewActionSet(S3TablesPutWarehouseEncryptionAction),
	S3TablesPutTableBucketMaintenanceConfigurationAction: NewActionSet(S3TablesPutWarehouseMaintenanceConfigurationAction),
	S3TablesPutTableBucketPolicyAction:                   NewActionSet(S3TablesPutWarehousePolicyAction),

	// Warehouse actions implicitly allow their TableBucket counterparts
	S3TablesCreateWarehouseAction:                      NewActionSet(S3TablesCreateTableBucketAction),
	S3TablesDeleteWarehouseAction:                      NewActionSet(S3TablesDeleteTableBucketAction),
	S3TablesDeleteWarehouseEncryptionAction:            NewActionSet(S3TablesDeleteTableBucketEncryptionAction),
	S3TablesDeleteWarehousePolicyAction:                NewActionSet(S3TablesDeleteTableBucketPolicyAction),
	S3TablesGetWarehouseAction:                         NewActionSet(S3TablesGetTableBucketAction),
	S3TablesGetWarehouseEncryptionAction:               NewActionSet(S3TablesGetTableBucketEncryptionAction),
	S3TablesGetWarehouseMaintenanceConfigurationAction: NewActionSet(S3TablesGetTableBucketMaintenanceConfigurationAction),
	S3TablesGetWarehousePolicyAction:                   NewActionSet(S3TablesGetTableBucketPolicyAction),
	S3TablesListWarehousesAction:                       NewActionSet(S3TablesListTableBucketsAction),
	S3TablesPutWarehouseEncryptionAction:               NewActionSet(S3TablesPutTableBucketEncryptionAction),
	S3TablesPutWarehouseMaintenanceConfigurationAction: NewActionSet(S3TablesPutTableBucketMaintenanceConfigurationAction),
	S3TablesPutWarehousePolicyAction:                   NewActionSet(S3TablesPutTableBucketPolicyAction),
}

// Match - matches object name with anyone of action pattern in action set.
func (actionSet ActionSet) Match(action Action) bool {
	for r := range actionSet {
		if r.Match(action) {
			return true
		}

		if implicits, ok := implicitActions[r]; ok {
			if implicits.Contains(action) {
				return true
			}
		}
	}

	return false
}

// Equals - checks whether given action set is equal to current action set or not.
func (actionSet ActionSet) Equals(sactionSet ActionSet) bool {
	// If length of set is not equal to length of given set, the
	// set is not equal to given set.
	if len(actionSet) != len(sactionSet) {
		return false
	}

	// As both sets are equal in length, check each elements are equal.
	for k := range actionSet {
		if _, ok := sactionSet[k]; !ok {
			return false
		}
	}

	return true
}

// Intersection - returns actions available in both ActionSet.
func (actionSet ActionSet) Intersection(sset ActionSet) ActionSet {
	nset := NewActionSet()
	for k := range actionSet {
		if _, ok := sset[k]; ok {
			nset.Add(k)
		}
	}

	return nset
}

// MarshalJSON - encodes ActionSet to JSON data.
func (actionSet ActionSet) MarshalJSON() ([]byte, error) {
	if len(actionSet) == 0 {
		return nil, Errorf("empty actions not allowed")
	}
	return json.Marshal(actionSet.ToSlice())
}

func (actionSet ActionSet) String() string {
	actions := []string{}
	for action := range actionSet {
		actions = append(actions, string(action))
	}
	sort.Strings(actions)

	return fmt.Sprintf("%v", actions)
}

// ToSlice - returns slice of actions from the action set.
func (actionSet ActionSet) ToSlice() []Action {
	if len(actionSet) == 0 {
		return nil
	}
	actions := make([]Action, 0, len(actionSet))
	for action := range actionSet {
		actions = append(actions, action)
	}

	return actions
}

// ToAdminSlice - returns slice of admin actions from the action set.
func (actionSet ActionSet) ToAdminSlice() []AdminAction {
	if len(actionSet) == 0 {
		return nil
	}
	actions := make([]AdminAction, 0, len(actionSet))
	for action := range actionSet {
		actions = append(actions, AdminAction(action))
	}

	return actions
}

// ToSTSSlice - returns slice of STS actions from the action set.
func (actionSet ActionSet) ToSTSSlice() []STSAction {
	actions := []STSAction{}
	for action := range actionSet {
		actions = append(actions, STSAction(action))
	}

	return actions
}

// ToKMSSlice - returns slice of kms actions from the action set.
func (actionSet ActionSet) ToKMSSlice() (actions []KMSAction) {
	for action := range actionSet {
		actions = append(actions, KMSAction(action))
	}
	return actions
}

// ToTableSlice - returns slice of table actions from the action set.
func (actionSet ActionSet) ToTableSlice() []TableAction {
	if len(actionSet) == 0 {
		return nil
	}
	actions := make([]TableAction, 0, len(actionSet))
	for action := range actionSet {
		actions = append(actions, TableAction(action))
	}

	return actions
}

// ToVectorsSlice - returns slice of vectors actions from the action set.
func (actionSet ActionSet) ToVectorsSlice() []VectorsAction {
	if len(actionSet) == 0 {
		return nil
	}
	actions := make([]VectorsAction, 0, len(actionSet))
	for action := range actionSet {
		actions = append(actions, VectorsAction(action))
	}

	return actions
}

// UnmarshalJSON - decodes JSON data to ActionSet.
func (actionSet *ActionSet) UnmarshalJSON(data []byte) error {
	var sset set.StringSet
	if err := json.Unmarshal(data, &sset); err != nil {
		return err
	}

	*actionSet = make(ActionSet)
	for _, s := range sset.ToSlice() {
		actionSet.Add(Action(s))
	}

	return nil
}

// ValidateAdmin checks if all actions are valid Admin actions
func (actionSet ActionSet) ValidateAdmin() error {
	for _, action := range actionSet.ToAdminSlice() {
		if !action.IsValid() {
			return Errorf("unsupported admin action '%v'", action)
		}
	}
	return nil
}

// ValidateSTS checks if all actions are valid STS actions
func (actionSet ActionSet) ValidateSTS() error {
	for _, action := range actionSet.ToSTSSlice() {
		if !action.IsValid() {
			return Errorf("unsupported STS action '%v'", action)
		}
	}
	return nil
}

// ValidateKMS checks if all actions are valid KMS actions
func (actionSet ActionSet) ValidateKMS() error {
	for _, action := range actionSet.ToKMSSlice() {
		if !action.IsValid() {
			return Errorf("unsupported KMS action '%v'", action)
		}
	}
	return nil
}

// ValidateTable checks if all actions are valid Table actions
func (actionSet ActionSet) ValidateTable() error {
	for _, action := range actionSet.ToTableSlice() {
		if !action.IsValid() {
			return Errorf("unsupported table action '%v'", action)
		}
	}
	return nil
}

// ValidateVectors checks if all actions are valid Vectors actions
func (actionSet ActionSet) ValidateVectors() error {
	for _, action := range actionSet.ToVectorsSlice() {
		if !action.IsValid() {
			return Errorf("unsupported vectors action '%v'", action)
		}
	}
	return nil
}

// Validate checks if all actions are valid
func (actionSet ActionSet) Validate() error {
	for _, action := range actionSet.ToSlice() {
		if !action.IsValid() {
			return Errorf("unsupported action '%v'", action)
		}
	}
	return nil
}

// NewActionSet - creates new action set.
func NewActionSet(actions ...Action) ActionSet {
	actionSet := make(ActionSet, len(actions))
	for _, action := range actions {
		actionSet.Add(action)
	}

	return actionSet
}

// NewActionStrings - creates new action set from strings.
func NewActionStrings(actions ...string) ActionSet {
	actionSet := make(ActionSet, len(actions))
	for _, action := range actions {
		actionSet.Add(Action(action))
	}

	return actionSet
}
