package credentials

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/Sirupsen/logrus"
	"github.com/docker/docker/cliconfig/configfile"
	"github.com/docker/engine-api/types"
)

const (
	remoteCredentialsPrefix = "docker-credential-"
	tokenUsername           = "<token>"
)

// Standarize the not found error, so every helper returns
// the same message and docker can handle it properly.
var errCredentialsNotFound = errors.New("credentials not found in native keychain")

// command is an interface that remote executed commands implement.
type command interface {
	Output() ([]byte, error)
	Input(in io.Reader)
}

// credentialsRequest holds information shared between docker and a remote credential store.
type credentialsRequest struct {
	ServerURL string
	Username  string
	Secret    string
}

// credentialsGetResponse is the information serialized from a remote store
// when the plugin sends requests to get the user credentials.
type credentialsGetResponse struct {
	Username string
	Secret   string
}

// nativeStore implements a credentials store
// using native keychain to keep credentials secure.
// It piggybacks into a file store to keep users' emails.
type nativeStore struct {
	commandFn func(args ...string) command
	fileStore Store
}

// NewNativeStore creates a new native store that
// uses a remote helper program to manage credentials.
func NewNativeStore(file *configfile.ConfigFile) Store {
	return &nativeStore{
		commandFn: shellCommandFn(file.CredentialsStore),
		fileStore: NewFileStore(file),
	}
}

// Erase removes the given credentials from the native store.
func (c *nativeStore) Erase(serverAddress string) error {
	if err := c.eraseCredentialsFromStore(serverAddress); err != nil {
		return err
	}

	// Fallback to plain text store to remove email
	return c.fileStore.Erase(serverAddress)
}

// Get retrieves credentials for a specific server from the native store.
func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) {
	// load user email if it exist or an empty auth config.
	auth, _ := c.fileStore.Get(serverAddress)

	creds, err := c.getCredentialsFromStore(serverAddress)
	if err != nil {
		return auth, err
	}
	auth.Username = creds.Username
	auth.IdentityToken = creds.IdentityToken
	auth.Password = creds.Password

	return auth, nil
}

// GetAll retrieves all the credentials from the native store.
func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) {
	auths, _ := c.fileStore.GetAll()

	for s, ac := range auths {
		creds, _ := c.getCredentialsFromStore(s)
		ac.Username = creds.Username
		ac.Password = creds.Password
		ac.IdentityToken = creds.IdentityToken
		auths[s] = ac
	}

	return auths, nil
}

// Store saves the given credentials in the file store.
func (c *nativeStore) Store(authConfig types.AuthConfig) error {
	if err := c.storeCredentialsInStore(authConfig); err != nil {
		return err
	}
	authConfig.Username = ""
	authConfig.Password = ""
	authConfig.IdentityToken = ""

	// Fallback to old credential in plain text to save only the email
	return c.fileStore.Store(authConfig)
}

// storeCredentialsInStore executes the command to store the credentials in the native store.
func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
	cmd := c.commandFn("store")
	creds := &credentialsRequest{
		ServerURL: config.ServerAddress,
		Username:  config.Username,
		Secret:    config.Password,
	}

	if config.IdentityToken != "" {
		creds.Username = tokenUsername
		creds.Secret = config.IdentityToken
	}

	buffer := new(bytes.Buffer)
	if err := json.NewEncoder(buffer).Encode(creds); err != nil {
		return err
	}
	cmd.Input(buffer)

	out, err := cmd.Output()
	if err != nil {
		t := strings.TrimSpace(string(out))
		logrus.Debugf("error adding credentials - err: %v, out: `%s`", err, t)
		return fmt.Errorf(t)
	}

	return nil
}

// getCredentialsFromStore executes the command to get the credentials from the native store.
func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthConfig, error) {
	var ret types.AuthConfig

	cmd := c.commandFn("get")
	cmd.Input(strings.NewReader(serverAddress))

	out, err := cmd.Output()
	if err != nil {
		t := strings.TrimSpace(string(out))

		// do not return an error if the credentials are not
		// in the keyckain. Let docker ask for new credentials.
		if t == errCredentialsNotFound.Error() {
			return ret, nil
		}

		logrus.Debugf("error getting credentials - err: %v, out: `%s`", err, t)
		return ret, fmt.Errorf(t)
	}

	var resp credentialsGetResponse
	if err := json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil {
		return ret, err
	}

	if resp.Username == tokenUsername {
		ret.IdentityToken = resp.Secret
	} else {
		ret.Password = resp.Secret
		ret.Username = resp.Username
	}

	ret.ServerAddress = serverAddress
	return ret, nil
}

// eraseCredentialsFromStore executes the command to remove the server credentails from the native store.
func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error {
	cmd := c.commandFn("erase")
	cmd.Input(strings.NewReader(serverURL))

	out, err := cmd.Output()
	if err != nil {
		t := strings.TrimSpace(string(out))
		logrus.Debugf("error erasing credentials - err: %v, out: `%s`", err, t)
		return fmt.Errorf(t)
	}

	return nil
}
