package cancel

import (
	"errors"
	"fmt"
	"net/http"
	"strconv"

	"github.com/cli/cli/v2/api"
	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/pkg/cmd/run/shared"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/spf13/cobra"
)

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

	Prompt bool

	RunID string
	Force bool
}

func NewCmdCancel(f *cmdutil.Factory, runF func(*CancelOptions) error) *cobra.Command {
	opts := &CancelOptions{
		IO:         f.IOStreams,
		HttpClient: f.HttpClient,
		Prompter:   f.Prompter,
	}

	cmd := &cobra.Command{
		Use:   "cancel [<run-id>]",
		Short: "Cancel a workflow run",
		Args:  cobra.MaximumNArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			// support `-R, --repo` override
			opts.BaseRepo = f.BaseRepo

			if len(args) > 0 {
				opts.RunID = args[0]
			} else if !opts.IO.CanPrompt() {
				return cmdutil.FlagErrorf("run ID required when not running interactively")
			} else {
				opts.Prompt = true
			}

			if runF != nil {
				return runF(opts)
			}

			return runCancel(opts)
		},
	}

	cmd.Flags().BoolVar(&opts.Force, "force", false, "Force cancel a workflow run")

	return cmd
}

func runCancel(opts *CancelOptions) error {
	if opts.RunID != "" {
		_, err := strconv.Atoi(opts.RunID)
		if err != nil {
			return fmt.Errorf("invalid run-id %#v", opts.RunID)
		}
	}
	httpClient, err := opts.HttpClient()
	if err != nil {
		return fmt.Errorf("failed to create http client: %w", err)
	}
	client := api.NewClientFromHTTP(httpClient)

	cs := opts.IO.ColorScheme()

	repo, err := opts.BaseRepo()
	if err != nil {
		return fmt.Errorf("failed to determine base repo: %w", err)
	}

	runID := opts.RunID
	var run *shared.Run

	if opts.Prompt {
		runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool {
			return run.Status != shared.Completed
		})
		if err != nil {
			return fmt.Errorf("failed to get runs: %w", err)
		}
		if len(runs) == 0 {
			return fmt.Errorf("found no in progress runs to cancel")
		}
		if runID, err = shared.SelectRun(opts.Prompter, cs, runs); err != nil {
			return err
		}
		// TODO silly stopgap until dust settles and SelectRun can just return a run
		for _, r := range runs {
			if fmt.Sprintf("%d", r.ID) == runID {
				run = &r
				break
			}
		}
	} else {
		run, err = shared.GetRun(client, repo, runID, 0)
		if err != nil {
			var httpErr api.HTTPError
			if errors.As(err, &httpErr) {
				if httpErr.StatusCode == http.StatusNotFound {
					err = fmt.Errorf("Could not find any workflow run with ID %s", opts.RunID)
				}
			}
			return err
		}
	}

	force := opts.Force

	err = cancelWorkflowRun(client, repo, fmt.Sprintf("%d", run.ID), force)
	if err != nil {
		var httpErr api.HTTPError
		if errors.As(err, &httpErr) {
			if httpErr.StatusCode == http.StatusConflict {
				err = fmt.Errorf("Cannot cancel a workflow run that is completed")
			}
		}

		return err
	}

	if force {
		fmt.Fprintf(opts.IO.Out, "%s Request to force cancel workflow %s submitted.\n", cs.SuccessIcon(), runID)
	} else {
		fmt.Fprintf(opts.IO.Out, "%s Request to cancel workflow %s submitted.\n", cs.SuccessIcon(), runID)
	}

	return nil
}

func cancelWorkflowRun(client *api.Client, repo ghrepo.Interface, runID string, force bool) error {
	var path string
	if force {
		path = fmt.Sprintf("repos/%s/actions/runs/%s/force-cancel", ghrepo.FullName(repo), runID)
	} else {
		path = fmt.Sprintf("repos/%s/actions/runs/%s/cancel", ghrepo.FullName(repo), runID)
	}

	err := client.REST(repo.RepoHost(), "POST", path, nil, nil)
	if err != nil {
		return err
	}

	return nil
}
