package base64

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"os"
	"strings"

	"github.com/pkg/errors"
	"github.com/smallstep/cli/utils"
	"github.com/urfave/cli"
	"go.step.sm/cli-utils/command"
)

func init() {
	cmd := cli.Command{
		Name:   "base64",
		Action: command.ActionFunc(base64Action),
		Usage:  "encodes and decodes using base64 representation",
		UsageText: `**step base64**
[**-d**|**--decode**] [**-r**|**--raw**] [**-u**|**--url**]`,
		Description: `**step base64** implements base64 encoding as specified by RFC 4648.

## Examples

Encode to base64 using the standard encoding:
'''
$ echo -n This is the string to encode | step base64
VGhpcyBpcyB0aGUgc3RyaW5nIHRvIGVuY29kZQ==
$ step base64 This is the string to encode
VGhpcyBpcyB0aGUgc3RyaW5nIHRvIGVuY29kZQ==
'''

Decode a base64 encoded string:
'''
$ echo VGhpcyBpcyB0aGUgc3RyaW5nIHRvIGVuY29kZQ== | step base64 -d
This is the string to encode
'''

Encode to base64 without padding:
'''
$ echo -n This is the string to encode | step base64 -r
VGhpcyBpcyB0aGUgc3RyaW5nIHRvIGVuY29kZQ
$ step base64 -r This is the string to encode
VGhpcyBpcyB0aGUgc3RyaW5nIHRvIGVuY29kZQ
'''

Encode to base64 using the url encoding:
'''
$ echo 'abc123$%^&*()_+-=~' | step base64 -u
YWJjMTIzJCVeJiooKV8rLT1-Cg==
'''

Decode an url encoded base64 string. The encoding type can be enforced
using the '-u' or '-r' flags, but it will be autodetected if they are not
passed:
'''
$ echo YWJjMTIzJCVeJiooKV8rLT1-Cg== | step base64 -d
abc123$%^&*()_+-=~
$ echo YWJjMTIzJCVeJiooKV8rLT1-Cg== | step base64 -d -u
abc123$%^&*()_+-=~
'''`,
		Flags: []cli.Flag{
			cli.BoolFlag{
				Name:  "d,decode",
				Usage: "decode base64 input",
			},
			cli.BoolFlag{
				Name:  "r,raw",
				Usage: "use the unpadded base64 encoding",
			},
			cli.BoolFlag{
				Name:  "u,url",
				Usage: "use the encoding format typically used in URLs and file names",
			},
		},
	}

	command.Register(cmd)
}

func base64Action(ctx *cli.Context) error {
	var err error
	var data []byte
	isDecode := ctx.Bool("decode")

	if ctx.NArg() > 0 {
		data = []byte(strings.Join(ctx.Args(), " "))
	} else {
		var prompt string
		if isDecode {
			prompt = "Please enter text to decode"
		} else {
			prompt = "Please enter text to encode"
		}

		if data, err = utils.ReadInput(prompt); err != nil {
			return err
		}
	}

	enc := getEncoder(ctx, data)
	if isDecode {
		b, err := enc.DecodeString(string(data))
		if err != nil {
			return errors.Wrap(err, "error decoding input")
		}
		os.Stdout.Write(b)
	} else {
		fmt.Println(enc.EncodeToString(data))
	}

	return nil
}

func getEncoder(ctx *cli.Context, data []byte) *base64.Encoding {
	raw := ctx.Bool("raw")
	url := ctx.Bool("url")
	isDecode := ctx.Bool("decode")

	// Detect encoding
	if isDecode && !ctx.IsSet("raw") && !ctx.IsSet("url") {
		raw = !bytes.HasSuffix(bytes.TrimSpace(data), []byte("="))
		url = bytes.Contains(data, []byte("-")) || bytes.Contains(data, []byte("_"))
	}

	if raw {
		if url {
			return base64.RawURLEncoding
		}
		return base64.RawStdEncoding
	}
	if url {
		return base64.URLEncoding
	}

	return base64.StdEncoding
}
