package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/http"
	"github.com/spf13/cobra"
	input "github.com/tcnksm/go-input"
)

type userSVCsFn func() (
	cmdUserDeps,
	error,
)

type cmdUserDeps struct {
	userSVC   influxdb.UserService
	orgSvc    influxdb.OrganizationService
	passSVC   influxdb.PasswordsService
	urmSVC    influxdb.UserResourceMappingService
	getPassFn func(*input.UI, bool) string
}

func cmdUser(f *globalFlags, opt genericCLIOpts) *cobra.Command {
	builder := newCmdUserBuilder(newUserSVC, opt)
	builder.globalFlags = f
	return builder.cmd()
}

type cmdUserBuilder struct {
	genericCLIOpts
	*globalFlags

	svcFn userSVCsFn

	hideHeaders bool
	id          string
	json        bool
	name        string
	password    string
	org         organization
}

func newCmdUserBuilder(svcsFn userSVCsFn, opt genericCLIOpts) *cmdUserBuilder {
	return &cmdUserBuilder{
		genericCLIOpts: opt,
		svcFn:          svcsFn,
	}
}

func (b *cmdUserBuilder) cmd() *cobra.Command {
	cmd := b.newCmd("user", nil, false)
	cmd.Short = "User management commands"
	cmd.Run = seeHelp
	cmd.AddCommand(
		b.cmdCreate(),
		b.cmdDelete(),
		b.cmdFind(),
		b.cmdUpdate(),
		b.cmdPassword(),
	)

	return cmd
}

func (b *cmdUserBuilder) cmdPassword() *cobra.Command {
	cmd := b.newCmd("password", b.cmdPasswordRunEFn, true)
	cmd.Short = "Update user password"

	cmd.Flags().StringVarP(&b.id, "id", "i", "", "The user ID")
	cmd.Flags().StringVarP(&b.name, "name", "n", "", "The user name")

	return cmd
}

func (b *cmdUserBuilder) cmdPasswordRunEFn(cmd *cobra.Command, args []string) error {
	ctx := context.Background()
	dep, err := b.svcFn()
	if err != nil {
		return err
	}

	filter := influxdb.UserFilter{}
	if b.name != "" {
		filter.Name = &b.name
	}
	if b.id != "" {
		id, err := influxdb.IDFromString(b.id)
		if err != nil {
			return err
		}
		filter.ID = id
	}
	u, err := dep.userSVC.FindUser(ctx, filter)
	if err != nil {
		return err
	}
	ui := &input.UI{
		Writer: b.genericCLIOpts.w,
		Reader: b.genericCLIOpts.in,
	}
	password := dep.getPassFn(ui, true)

	if err = dep.passSVC.SetPassword(ctx, u.ID, password); err != nil {
		return err
	}
	fmt.Fprintln(b.w, "Your password has been successfully updated.")
	return nil
}

func (b *cmdUserBuilder) cmdUpdate() *cobra.Command {
	cmd := b.newCmd("update", b.cmdUpdateRunEFn, true)
	cmd.Short = "Update user"

	b.registerPrintFlags(cmd)
	cmd.Flags().StringVarP(&b.id, "id", "i", "", "The user ID (required)")
	cmd.Flags().StringVarP(&b.name, "name", "n", "", "The user name")
	cmd.MarkFlagRequired("id")

	return cmd
}

func (b *cmdUserBuilder) cmdUpdateRunEFn(cmd *cobra.Command, args []string) error {
	dep, err := b.svcFn()
	if err != nil {
		return err
	}

	var id influxdb.ID
	if err := id.DecodeFromString(b.id); err != nil {
		return err
	}

	update := influxdb.UserUpdate{}
	if b.name != "" {
		update.Name = &b.name
	}

	user, err := dep.userSVC.UpdateUser(context.Background(), id, update)
	if err != nil {
		return err
	}

	return b.printUser(userPrintOpts{user: user})
}

func (b *cmdUserBuilder) cmdCreate() *cobra.Command {
	cmd := b.newCmd("create", b.cmdCreateRunEFn, true)
	cmd.Short = "Create user"

	opts := flagOpts{
		{
			DestP:    &b.name,
			Flag:     "name",
			Short:    'n',
			Desc:     "The user name (required)",
			Required: true,
		},
	}
	opts.mustRegister(cmd)

	cmd.Flags().StringVarP(&b.password, "password", "p", "", "The user password")
	b.org.register(cmd, false)
	b.registerPrintFlags(cmd)

	return cmd
}

func (b *cmdUserBuilder) cmdCreateRunEFn(*cobra.Command, []string) error {
	ctx := context.Background()
	if err := b.org.validOrgFlags(b.globalFlags); err != nil {
		return err
	}

	dep, err := b.svcFn()
	if err != nil {
		return err
	}

	user := &influxdb.User{
		Name: b.name,
	}

	if err := dep.userSVC.CreateUser(ctx, user); err != nil {
		return err
	}

	orgID, err := b.org.getID(dep.orgSvc)
	if err != nil {
		return err
	}

	pass := b.password
	if orgID == 0 && pass == "" {
		return b.printUser(userPrintOpts{user: user})
	}

	if pass != "" && orgID == 0 {
		return errors.New("an org id is required when providing a user password")
	}

	err = dep.urmSVC.CreateUserResourceMapping(context.Background(), &influxdb.UserResourceMapping{
		UserID:       user.ID,
		UserType:     influxdb.Member,
		ResourceType: influxdb.OrgsResourceType,
		ResourceID:   orgID,
	})
	if err != nil {
		return err
	}

	if err := dep.passSVC.SetPassword(ctx, user.ID, pass); err != nil {
		return err
	}

	return b.printUser(userPrintOpts{user: user})
}

func (b *cmdUserBuilder) cmdFind() *cobra.Command {
	cmd := b.newCmd("list", b.cmdFindRunEFn, true)
	cmd.Short = "List users"
	cmd.Aliases = []string{"find", "ls"}

	b.registerPrintFlags(cmd)
	cmd.Flags().StringVarP(&b.id, "id", "i", "", "The user ID")
	cmd.Flags().StringVarP(&b.name, "name", "n", "", "The user name")

	return cmd
}

func (b *cmdUserBuilder) cmdFindRunEFn(*cobra.Command, []string) error {
	dep, err := b.svcFn()
	if err != nil {
		return err
	}

	filter := influxdb.UserFilter{}
	if b.name != "" {
		filter.Name = &b.name
	}
	if b.id != "" {
		id, err := influxdb.IDFromString(b.id)
		if err != nil {
			return err
		}
		filter.ID = id
	}

	users, _, err := dep.userSVC.FindUsers(context.Background(), filter)
	if err != nil {
		return err
	}

	return b.printUser(userPrintOpts{users: users})
}

func (b *cmdUserBuilder) cmdDelete() *cobra.Command {
	cmd := b.newCmd("delete", b.cmdDeleteRunEFn, true)
	cmd.Short = "Delete user"

	b.registerPrintFlags(cmd)
	cmd.Flags().StringVarP(&b.id, "id", "i", "", "The user ID (required)")
	cmd.MarkFlagRequired("id")

	return cmd
}

func (b *cmdUserBuilder) cmdDeleteRunEFn(cmd *cobra.Command, args []string) error {
	dep, err := b.svcFn()
	if err != nil {
		return err
	}

	var id influxdb.ID
	if err := id.DecodeFromString(b.id); err != nil {
		return err
	}

	ctx := context.Background()
	u, err := dep.userSVC.FindUserByID(ctx, id)
	if err != nil {
		return err
	}

	if err := dep.userSVC.DeleteUser(ctx, id); err != nil {
		return err
	}

	return b.printUser(userPrintOpts{
		deleted: true,
		user:    u,
	})
}

func (b *cmdUserBuilder) registerPrintFlags(cmd *cobra.Command) {
	registerPrintOptions(cmd, &b.hideHeaders, &b.json)
}

func (b *cmdUserBuilder) printUser(opt userPrintOpts) error {
	if b.json {
		var v interface{} = opt.users
		if opt.user != nil {
			v = opt.user
		}
		return b.writeJSON(v)
	}

	w := b.newTabWriter()
	defer w.Flush()

	w.HideHeaders(b.hideHeaders)

	headers := []string{"ID", "Name"}
	if opt.deleted {
		headers = append(headers, "Deleted")
	}
	w.WriteHeaders(headers...)

	if opt.user != nil {
		opt.users = append(opt.users, opt.user)
	}

	for _, u := range opt.users {
		m := map[string]interface{}{
			"ID":   u.ID.String(),
			"Name": u.Name,
		}
		if opt.deleted {
			m["Deleted"] = true
		}
		w.Write(m)
	}

	return nil
}

type userPrintOpts struct {
	deleted bool
	user    *influxdb.User
	users   []*influxdb.User
}

func newUserService() (influxdb.UserService, error) {
	if flags.local {
		return newLocalKVService()
	}

	client, err := newHTTPClient()
	if err != nil {
		return nil, err
	}
	return &http.UserService{
		Client: client,
	}, nil
}

func newUserSVC() (cmdUserDeps, error) {
	httpClient, err := newHTTPClient()
	if err != nil {
		return cmdUserDeps{}, err
	}
	userSvc := &http.UserService{Client: httpClient}
	orgSvc := &http.OrganizationService{Client: httpClient}
	passSvc := &http.PasswordService{Client: httpClient}
	urmSvc := &http.UserResourceMappingService{Client: httpClient}
	getPassFn := getPassword

	return cmdUserDeps{
		userSVC:   userSvc,
		orgSvc:    orgSvc,
		passSVC:   passSvc,
		urmSVC:    urmSvc,
		getPassFn: getPassFn,
	}, nil
}
