package label

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"math/rand"
	"net/http"
	"strings"
	"time"

	"github.com/MakeNowJust/heredoc"
	"github.com/cli/cli/v2/api"
	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/spf13/cobra"
)

var randomColors = []string{
	"B60205",
	"D93F0B",
	"FBCA04",
	"0E8A16",
	"006B75",
	"1D76DB",
	"0052CC",
	"5319E7",
	"E99695",
	"F9D0C4",
	"FEF2C0",
	"C2E0C6",
	"BFDADC",
	"C5DEF5",
	"BFD4F2",
	"D4C5F9",
}

type createOptions struct {
	BaseRepo   func() (ghrepo.Interface, error)
	HttpClient func() (*http.Client, error)
	IO         *iostreams.IOStreams

	Color       string
	Description string
	Name        string
	Force       bool
}

func newCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Command {
	opts := createOptions{
		HttpClient: f.HttpClient,
		IO:         f.IOStreams,
	}

	cmd := &cobra.Command{
		Use:   "create <name>",
		Short: "Create a new label",
		Long: heredoc.Docf(`
			Create a new label on GitHub, or update an existing one with %[1]s--force%[1]s.

			Must specify name for the label. The description and color are optional.
			If a color isn't provided, a random one will be chosen.

			The label color needs to be 6 character hex value.
		`, "`"),
		Example: heredoc.Doc(`
			# Create new bug label
			$ gh label create bug --description "Something isn't working" --color E99695
		`),
		Args: cmdutil.ExactArgs(1, "cannot create label: name argument required"),
		RunE: func(c *cobra.Command, args []string) error {
			opts.BaseRepo = f.BaseRepo
			opts.Name = args[0]
			opts.Color = strings.TrimPrefix(opts.Color, "#")
			if runF != nil {
				return runF(&opts)
			}
			return createRun(&opts)
		},
	}

	cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Description of the label")
	cmd.Flags().StringVarP(&opts.Color, "color", "c", "", "Color of the label")
	cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Update the label color and description if label already exists")

	return cmd
}

var errLabelAlreadyExists = errors.New("label already exists")

func createRun(opts *createOptions) error {
	httpClient, err := opts.HttpClient()
	if err != nil {
		return err
	}

	baseRepo, err := opts.BaseRepo()
	if err != nil {
		return err
	}

	if opts.Color == "" {
		r := rand.New(rand.NewSource(time.Now().UnixNano()))
		opts.Color = randomColors[r.Intn(len(randomColors)-1)]
	}

	opts.IO.StartProgressIndicator()
	err = createLabel(httpClient, baseRepo, opts)
	opts.IO.StopProgressIndicator()
	if err != nil {
		if errors.Is(err, errLabelAlreadyExists) {
			return fmt.Errorf("label with name %q already exists; use `--force` to update its color and description", opts.Name)
		}
		return err
	}

	if opts.IO.IsStdoutTTY() {
		cs := opts.IO.ColorScheme()
		successMsg := fmt.Sprintf("%s Label %q created in %s\n", cs.SuccessIcon(), opts.Name, ghrepo.FullName(baseRepo))
		fmt.Fprint(opts.IO.Out, successMsg)
	}

	return nil
}

func createLabel(client *http.Client, repo ghrepo.Interface, opts *createOptions) error {
	apiClient := api.NewClientFromHTTP(client)
	path := fmt.Sprintf("repos/%s/%s/labels", repo.RepoOwner(), repo.RepoName())
	requestByte, err := json.Marshal(map[string]string{
		"name":        opts.Name,
		"description": opts.Description,
		"color":       opts.Color,
	})
	if err != nil {
		return err
	}
	requestBody := bytes.NewReader(requestByte)
	err = apiClient.REST(repo.RepoHost(), "POST", path, requestBody, nil)

	if httpError, ok := err.(api.HTTPError); ok && isLabelAlreadyExistsError(httpError) {
		err = errLabelAlreadyExists
	}

	if opts.Force && errors.Is(err, errLabelAlreadyExists) {
		editOpts := editOptions{
			Description: opts.Description,
			Color:       opts.Color,
			Name:        opts.Name,
		}
		return updateLabel(apiClient, repo, &editOpts)
	}

	return err
}

func updateLabel(apiClient *api.Client, repo ghrepo.Interface, opts *editOptions) error {
	path := fmt.Sprintf("repos/%s/%s/labels/%s", repo.RepoOwner(), repo.RepoName(), opts.Name)
	properties := map[string]string{}
	if opts.Description != "" {
		properties["description"] = opts.Description
	}
	if opts.Color != "" {
		properties["color"] = opts.Color
	}
	if opts.NewName != "" {
		properties["new_name"] = opts.NewName
	}
	requestByte, err := json.Marshal(properties)
	if err != nil {
		return err
	}
	requestBody := bytes.NewReader(requestByte)
	err = apiClient.REST(repo.RepoHost(), "PATCH", path, requestBody, nil)

	if httpError, ok := err.(api.HTTPError); ok && isLabelAlreadyExistsError(httpError) {
		err = errLabelAlreadyExists
	}

	return err
}

func isLabelAlreadyExistsError(err api.HTTPError) bool {
	return err.StatusCode == 422 && len(err.Errors) == 1 && err.Errors[0].Field == "name" && err.Errors[0].Code == "already_exists"
}
