package main

import (
	"context"
	"fmt"

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

type secretSVCsFn func() (influxdb.SecretService, influxdb.OrganizationService, func(*input.UI) string, error)

func cmdSecret(f *globalFlags, opt genericCLIOpts) *cobra.Command {
	builder := newCmdSecretBuilder(newSecretSVCs, opt)
	builder.globalFlags = f
	return builder.cmd()
}

type cmdSecretBuilder struct {
	genericCLIOpts
	*globalFlags

	svcFn secretSVCsFn

	json        bool
	hideHeaders bool
	key         string
	value       string
	org         organization
}

func newCmdSecretBuilder(svcsFn secretSVCsFn, opt genericCLIOpts) *cmdSecretBuilder {
	return &cmdSecretBuilder{
		genericCLIOpts: opt,
		svcFn:          svcsFn,
	}
}

func (b *cmdSecretBuilder) cmd() *cobra.Command {
	cmd := b.newCmd("secret", nil, false)
	cmd.Short = "Secret management commands"
	cmd.Run = seeHelp
	cmd.AddCommand(
		b.cmdDelete(),
		b.cmdFind(),
		b.cmdUpdate(),
	)
	return cmd
}

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

	cmd.Flags().StringVarP(&b.key, "key", "k", "", "The secret key (required)")
	cmd.Flags().StringVarP(&b.value, "value", "v", "", "Optional secret value for scripting convenience, using this might expose the secret to your local history")
	cmd.MarkFlagRequired("key")
	b.org.register(cmd, false)
	b.registerPrintFlags(cmd)

	return cmd
}

func (b *cmdSecretBuilder) cmdUpdateRunEFn(cmd *cobra.Command, args []string) error {
	scrSVC, orgSVC, getSecretFn, err := b.svcFn()
	if err != nil {
		return err
	}
	orgID, err := b.org.getID(orgSVC)
	if err != nil {
		return err
	}

	ctx := context.Background()

	ui := &input.UI{
		Writer: b.genericCLIOpts.w,
		Reader: b.genericCLIOpts.in,
	}
	var secretVal string
	if b.value != "" {
		secretVal = b.value
	} else {
		secretVal = getSecretFn(ui)
	}

	err = scrSVC.PatchSecrets(ctx, orgID, map[string]string{
		b.key: secretVal,
	})
	if err != nil {
		return fmt.Errorf("failed to update secret with key %q: %v", b.key, err)
	}

	return b.printSecrets(secretPrintOpt{
		secret: secret{
			key:   b.key,
			orgID: orgID,
		},
	})
}

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

	cmd.Flags().StringVarP(&b.key, "key", "k", "", "The secret key (required)")
	cmd.MarkFlagRequired("key")
	b.org.register(cmd, false)
	b.registerPrintFlags(cmd)

	return cmd
}

func (b *cmdSecretBuilder) cmdDeleteRunEFn(cmd *cobra.Command, args []string) error {
	scrSVC, orgSVC, _, err := b.svcFn()
	if err != nil {
		return err
	}
	orgID, err := b.org.getID(orgSVC)
	if err != nil {
		return err
	}

	ctx := context.Background()
	if err := scrSVC.DeleteSecret(ctx, orgID, b.key); err != nil {
		return fmt.Errorf("failed to delete secret with key %q: %v", b.key, err)
	}

	return b.printSecrets(secretPrintOpt{
		deleted: true,
		secret: secret{
			key:   b.key,
			orgID: orgID,
		},
	})
}

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

	b.org.register(cmd, false)
	b.registerPrintFlags(cmd)

	return cmd
}

func (b *cmdSecretBuilder) cmdFindRunEFn(cmd *cobra.Command, args []string) error {
	scrSVC, orgSVC, _, err := b.svcFn()
	if err != nil {
		return err
	}

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

	platformSecrets, err := scrSVC.GetSecretKeys(context.Background(), orgID)
	if err != nil {
		return fmt.Errorf("failed to retrieve secret keys: %s", err)
	}

	secrets := make([]secret, 0, len(platformSecrets))
	for _, key := range platformSecrets {
		secrets = append(secrets, secret{
			key:   key,
			orgID: orgID,
		})
	}

	return b.printSecrets(secretPrintOpt{
		secrets: secrets,
	})
}

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

func (b *cmdSecretBuilder) printSecrets(opt secretPrintOpt) error {
	if b.json {
		var v interface{} = opt.secrets
		if opt.secrets == nil {
			v = opt.secret
		}
		return b.writeJSON(v)
	}

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

	w.HideHeaders(b.hideHeaders)

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

	if opt.secrets == nil {
		opt.secrets = append(opt.secrets, opt.secret)
	}

	for _, s := range opt.secrets {
		m := map[string]interface{}{
			"Key":             s.key,
			"Organization ID": s.orgID.String(),
		}
		if opt.deleted {
			m["Deleted"] = true
		}
		w.Write(m)
	}

	return nil
}

type (
	secretPrintOpt struct {
		deleted bool
		secret  secret
		secrets []secret
	}

	secret struct {
		key   string
		orgID influxdb.ID
	}
)

func newSecretSVCs() (influxdb.SecretService, influxdb.OrganizationService, func(*input.UI) string, error) {
	httpClient, err := newHTTPClient()
	if err != nil {
		return nil, nil, nil, err
	}
	orgSvc := &http.OrganizationService{Client: httpClient}

	return &http.SecretService{Client: httpClient}, orgSvc, getSecret, nil
}
