package shell

import (
	"bytes"
	"context"
	"flag"
	"fmt"
	"io"
	"strings"

	"github.com/seaweedfs/seaweedfs/weed/filer"
	"github.com/seaweedfs/seaweedfs/weed/iam"
	"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func init() {
	Commands = append(Commands, &commandS3Configure{})
}

type commandS3Configure struct {
}

func (c *commandS3Configure) Name() string {
	return "s3.configure"
}

func (c *commandS3Configure) Help() string {
	return `configure and apply s3 options for each bucket

	# see the current configuration file content
	s3.configure

	# create a new identity with account information
	s3.configure -user=username -actions=Read,Write,List,Tagging -buckets=bucket-name -policies=policy1,policy2 -access_key=key -secret_key=secret -account_id=id -account_display_name=name -account_email=email@example.com -apply
	`
}

func (c *commandS3Configure) HasTag(tag CommandTag) bool {
	return tag == Hidden
}

func (c *commandS3Configure) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {

	s3ConfigureCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
	actions := s3ConfigureCommand.String("actions", "", "comma separated actions names: Read,Write,List,Tagging,Admin")
	user := s3ConfigureCommand.String("user", "", "user name")
	buckets := s3ConfigureCommand.String("buckets", "", "bucket name")
	accessKey := s3ConfigureCommand.String("access_key", "", "specify the access key")
	secretKey := s3ConfigureCommand.String("secret_key", "", "specify the secret key")
	accountId := s3ConfigureCommand.String("account_id", "", "specify the account id")
	accountDisplayName := s3ConfigureCommand.String("account_display_name", "", "specify the account display name")
	accountEmail := s3ConfigureCommand.String("account_email", "", "specify the account email address")
	policies := s3ConfigureCommand.String("policies", "", "comma separated policy names")
	isDelete := s3ConfigureCommand.Bool("delete", false, "delete users, actions, access keys or policies")
	apply := s3ConfigureCommand.Bool("apply", false, "update and apply s3 configuration")
	if err = s3ConfigureCommand.Parse(args); err != nil {
		return nil
	}

	// Case 1: List configuration (no user specified)
	if *user == "" {
		return c.listConfiguration(commandEnv, writer)
	}

	// Case 2: Modify specific user
	var identity *iam_pb.Identity
	var isNewUser bool

	err = commandEnv.withIamClient(func(ctx context.Context, client iam_pb.SeaweedIdentityAccessManagementClient) error {

		// Try to get existing user
		resp, getErr := client.GetUser(ctx, &iam_pb.GetUserRequest{
			Username: *user,
		})

		if getErr == nil {
			identity = resp.Identity
			if identity == nil {
				// Should not happen if err is nil, but handle defensively
				isNewUser = true
				identity = &iam_pb.Identity{Name: *user}
			}
		} else {
			st, ok := status.FromError(getErr)
			if ok && st.Code() == codes.NotFound {
				isNewUser = true
				identity = &iam_pb.Identity{
					Name:        *user,
					Credentials: []*iam_pb.Credential{},
					Actions:     []string{},
					PolicyNames: []string{},
				}
			} else {
				return fmt.Errorf("failed to get user %s: %v", *user, getErr)
			}
		}

		// Apply changes to identity object
		if err := c.applyChanges(identity, isNewUser, actions, buckets, accessKey, secretKey, policies, isDelete, accountId, accountDisplayName, accountEmail); err != nil {
			return err
		}

		// Print changes (Simulation)
		var buf bytes.Buffer
		filer.ProtoToText(&buf, identity)
		fmt.Fprint(writer, buf.String())
		fmt.Fprintln(writer)

		if !*apply {
			infoAboutSimulationMode(writer, *apply, "-apply")
			return nil
		}

		// Apply changes
		if *isDelete && *actions == "" && *accessKey == "" && *buckets == "" && *policies == "" {
			// Delete User
			_, err := client.DeleteUser(ctx, &iam_pb.DeleteUserRequest{Username: *user})
			return err
		} else {
			// Create or Update User
			if isNewUser {
				_, err := client.CreateUser(ctx, &iam_pb.CreateUserRequest{Identity: identity})
				return err
			} else {
				_, err := client.UpdateUser(ctx, &iam_pb.UpdateUserRequest{Username: *user, Identity: identity})
				return err
			}
		}
	})

	return err
}

func (c *commandS3Configure) listConfiguration(commandEnv *CommandEnv, writer io.Writer) error {
	return commandEnv.withIamClient(func(ctx context.Context, client iam_pb.SeaweedIdentityAccessManagementClient) error {
		resp, err := client.GetConfiguration(ctx, &iam_pb.GetConfigurationRequest{})
		if err != nil {
			return err
		}
		var buf bytes.Buffer
		filer.ProtoToText(&buf, resp.Configuration)
		fmt.Fprint(writer, buf.String())
		fmt.Fprintln(writer)
		return nil
	})
}

func (c *commandS3Configure) applyChanges(identity *iam_pb.Identity, isNewUser bool, actions, buckets, accessKey, secretKey, policies *string, isDelete *bool, accountId, accountDisplayName, accountEmail *string) error {

	// Helper to update account info
	if *accountId != "" || *accountDisplayName != "" || *accountEmail != "" {
		if identity.Account == nil {
			identity.Account = &iam_pb.Account{}
		}
		if *accountId != "" {
			identity.Account.Id = *accountId
		}
		if *accountDisplayName != "" {
			identity.Account.DisplayName = *accountDisplayName
		}
		if *accountEmail != "" {
			identity.Account.EmailAddress = *accountEmail
		}
	}

	// Prepare lists
	var cmdActions []string
	if *actions != "" {
		for _, action := range strings.Split(*actions, ",") {
			if *buckets == "" {
				cmdActions = append(cmdActions, action)
			} else {
				for _, bucket := range strings.Split(*buckets, ",") {
					cmdActions = append(cmdActions, fmt.Sprintf("%s:%s", action, bucket))
				}
			}
		}
	}

	var cmdPolicies []string
	if *policies != "" {
		for _, policy := range strings.Split(*policies, ",") {
			if policy != "" {
				cmdPolicies = append(cmdPolicies, policy)
			}
		}
	}

	if *isDelete {
		// DELETE LOGIC

		// Remove Actions
		if len(cmdActions) > 0 {
			identity.Actions = removeFromSlice(identity.Actions, cmdActions)
		}

		// Remove Credentials
		if *accessKey != "" {
			var keepCredentials []*iam_pb.Credential
			for _, cred := range identity.Credentials {
				if cred.AccessKey != *accessKey {
					keepCredentials = append(keepCredentials, cred)
				}
			}
			identity.Credentials = keepCredentials
		}

		// Remove Policies
		if len(cmdPolicies) > 0 {
			identity.PolicyNames = removeFromSlice(identity.PolicyNames, cmdPolicies)
		}

	} else {
		// ADD/UPDATE LOGIC

		// Add Actions
		identity.Actions = addUniqueToSlice(identity.Actions, cmdActions)

		// Add/Update Credentials
		if *accessKey != "" && identity.Name != "anonymous" {
			found := false
			for _, cred := range identity.Credentials {
				if cred.AccessKey == *accessKey {
					found = true
					if *secretKey != "" {
						cred.SecretKey = *secretKey
					}
					break
				}
			}
			if !found {
				if *secretKey == "" {
					return fmt.Errorf("secret_key is required when adding a new access_key")
				}
				identity.Credentials = append(identity.Credentials, &iam_pb.Credential{
					AccessKey: *accessKey,
					SecretKey: *secretKey,
					Status:    iam.AccessKeyStatusActive,
				})
			}
		}

		// Add Policies
		identity.PolicyNames = addUniqueToSlice(identity.PolicyNames, cmdPolicies)
	}

	return nil
}

// Helper to remove items from a slice
func removeFromSlice(current []string, toRemove []string) []string {
	removeSet := make(map[string]struct{}, len(toRemove))
	for _, item := range toRemove {
		removeSet[item] = struct{}{}
	}
	var result []string
	for _, item := range current {
		if _, found := removeSet[item]; !found {
			result = append(result, item)
		}
	}
	return result
}

// Helper to add unique items to a slice
func addUniqueToSlice(current []string, toAdd []string) []string {
	existingSet := make(map[string]struct{}, len(current))
	for _, item := range current {
		existingSet[item] = struct{}{}
	}
	for _, item := range toAdd {
		if _, found := existingSet[item]; !found {
			current = append(current, item)
		}
	}
	return current
}
