package plugin

import (
	"encoding/base64"
	"encoding/json"
	"net/http"
	"strconv"
	"strings"

	distreference "github.com/docker/distribution/reference"
	"github.com/docker/docker/api/server/httputils"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/pkg/ioutils"
	"github.com/docker/docker/pkg/streamformatter"
	"github.com/docker/docker/reference"
	"github.com/pkg/errors"
	"golang.org/x/net/context"
)

func parseHeaders(headers http.Header) (map[string][]string, *types.AuthConfig) {

	metaHeaders := map[string][]string{}
	for k, v := range headers {
		if strings.HasPrefix(k, "X-Meta-") {
			metaHeaders[k] = v
		}
	}

	// Get X-Registry-Auth
	authEncoded := headers.Get("X-Registry-Auth")
	authConfig := &types.AuthConfig{}
	if authEncoded != "" {
		authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
		if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
			authConfig = &types.AuthConfig{}
		}
	}

	return metaHeaders, authConfig
}

// parseRemoteRef parses the remote reference into a reference.Named
// returning the tag associated with the reference. In the case the
// given reference string includes both digest and tag, the returned
// reference will have the digest without the tag, but the tag will
// be returned.
func parseRemoteRef(remote string) (reference.Named, string, error) {
	// Parse remote reference, supporting remotes with name and tag
	// NOTE: Using distribution reference to handle references
	// containing both a name and digest
	remoteRef, err := distreference.ParseNamed(remote)
	if err != nil {
		return nil, "", err
	}

	var tag string
	if t, ok := remoteRef.(distreference.Tagged); ok {
		tag = t.Tag()
	}

	// Convert distribution reference to docker reference
	// TODO: remove when docker reference changes reconciled upstream
	ref, err := reference.WithName(remoteRef.Name())
	if err != nil {
		return nil, "", err
	}
	if d, ok := remoteRef.(distreference.Digested); ok {
		ref, err = reference.WithDigest(ref, d.Digest())
		if err != nil {
			return nil, "", err
		}
	} else if tag != "" {
		ref, err = reference.WithTag(ref, tag)
		if err != nil {
			return nil, "", err
		}
	} else {
		ref = reference.WithDefaultTag(ref)
	}

	return ref, tag, nil
}

func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	metaHeaders, authConfig := parseHeaders(r.Header)

	ref, _, err := parseRemoteRef(r.FormValue("remote"))
	if err != nil {
		return err
	}

	privileges, err := pr.backend.Privileges(ctx, ref, metaHeaders, authConfig)
	if err != nil {
		return err
	}
	return httputils.WriteJSON(w, http.StatusOK, privileges)
}

func (pr *pluginRouter) upgradePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return errors.Wrap(err, "failed to parse form")
	}

	var privileges types.PluginPrivileges
	dec := json.NewDecoder(r.Body)
	if err := dec.Decode(&privileges); err != nil {
		return errors.Wrap(err, "failed to parse privileges")
	}
	if dec.More() {
		return errors.New("invalid privileges")
	}

	metaHeaders, authConfig := parseHeaders(r.Header)
	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
	if err != nil {
		return err
	}

	name, err := getName(ref, tag, vars["name"])
	if err != nil {
		return err
	}
	w.Header().Set("Docker-Plugin-Name", name)

	w.Header().Set("Content-Type", "application/json")
	output := ioutils.NewWriteFlusher(w)

	if err := pr.backend.Upgrade(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
		if !output.Flushed() {
			return err
		}
		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
	}

	return nil
}

func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return errors.Wrap(err, "failed to parse form")
	}

	var privileges types.PluginPrivileges
	dec := json.NewDecoder(r.Body)
	if err := dec.Decode(&privileges); err != nil {
		return errors.Wrap(err, "failed to parse privileges")
	}
	if dec.More() {
		return errors.New("invalid privileges")
	}

	metaHeaders, authConfig := parseHeaders(r.Header)
	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
	if err != nil {
		return err
	}

	name, err := getName(ref, tag, r.FormValue("name"))
	if err != nil {
		return err
	}
	w.Header().Set("Docker-Plugin-Name", name)

	w.Header().Set("Content-Type", "application/json")
	output := ioutils.NewWriteFlusher(w)

	if err := pr.backend.Pull(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
		if !output.Flushed() {
			return err
		}
		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
	}

	return nil
}

func getName(ref reference.Named, tag, name string) (string, error) {
	if name == "" {
		if _, ok := ref.(reference.Canonical); ok {
			trimmed := reference.TrimNamed(ref)
			if tag != "" {
				nt, err := reference.WithTag(trimmed, tag)
				if err != nil {
					return "", err
				}
				name = nt.String()
			} else {
				name = reference.WithDefaultTag(trimmed).String()
			}
		} else {
			name = ref.String()
		}
	} else {
		localRef, err := reference.ParseNamed(name)
		if err != nil {
			return "", err
		}
		if _, ok := localRef.(reference.Canonical); ok {
			return "", errors.New("cannot use digest in plugin tag")
		}
		if distreference.IsNameOnly(localRef) {
			// TODO: log change in name to out stream
			name = reference.WithDefaultTag(localRef).String()
		}
	}
	return name, nil
}

func (pr *pluginRouter) createPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	options := &types.PluginCreateOptions{
		RepoName: r.FormValue("name")}

	if err := pr.backend.CreateFromContext(ctx, r.Body, options); err != nil {
		return err
	}
	//TODO: send progress bar
	w.WriteHeader(http.StatusNoContent)
	return nil
}

func (pr *pluginRouter) enablePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	name := vars["name"]
	timeout, err := strconv.Atoi(r.Form.Get("timeout"))
	if err != nil {
		return err
	}
	config := &types.PluginEnableConfig{Timeout: timeout}

	return pr.backend.Enable(name, config)
}

func (pr *pluginRouter) disablePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	name := vars["name"]
	config := &types.PluginDisableConfig{
		ForceDisable: httputils.BoolValue(r, "force"),
	}

	return pr.backend.Disable(name, config)
}

func (pr *pluginRouter) removePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	name := vars["name"]
	config := &types.PluginRmConfig{
		ForceRemove: httputils.BoolValue(r, "force"),
	}
	return pr.backend.Remove(name, config)
}

func (pr *pluginRouter) pushPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return errors.Wrap(err, "failed to parse form")
	}

	metaHeaders, authConfig := parseHeaders(r.Header)

	w.Header().Set("Content-Type", "application/json")
	output := ioutils.NewWriteFlusher(w)

	if err := pr.backend.Push(ctx, vars["name"], metaHeaders, authConfig, output); err != nil {
		if !output.Flushed() {
			return err
		}
		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
	}
	return nil
}

func (pr *pluginRouter) setPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	var args []string
	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		return err
	}
	if err := pr.backend.Set(vars["name"], args); err != nil {
		return err
	}
	w.WriteHeader(http.StatusNoContent)
	return nil
}

func (pr *pluginRouter) listPlugins(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	l, err := pr.backend.List()
	if err != nil {
		return err
	}
	return httputils.WriteJSON(w, http.StatusOK, l)
}

func (pr *pluginRouter) inspectPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	result, err := pr.backend.Inspect(vars["name"])
	if err != nil {
		return err
	}
	return httputils.WriteJSON(w, http.StatusOK, result)
}
