package checks

import (
	"fmt"
	"sort"
	"time"

	"github.com/MakeNowJust/heredoc"
	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/pkg/cmd/pr/shared"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/cli/cli/v2/utils"
	"github.com/spf13/cobra"
)

type browser interface {
	Browse(string) error
}

type ChecksOptions struct {
	IO      *iostreams.IOStreams
	Browser browser

	Finder shared.PRFinder

	SelectorArg string
	WebMode     bool
}

func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Command {
	opts := &ChecksOptions{
		IO:      f.IOStreams,
		Browser: f.Browser,
	}

	cmd := &cobra.Command{
		Use:   "checks [<number> | <url> | <branch>]",
		Short: "Show CI status for a single pull request",
		Long: heredoc.Doc(`
			Show CI status for a single pull request.

			Without an argument, the pull request that belongs to the current branch
			is selected.			
		`),
		Args: cobra.MaximumNArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			opts.Finder = shared.NewFinder(f)

			if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
				return cmdutil.FlagErrorf("argument required when using the --repo flag")
			}

			if len(args) > 0 {
				opts.SelectorArg = args[0]
			}

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

			return checksRun(opts)
		},
	}

	cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the web browser to show details about checks")

	return cmd
}

func checksRun(opts *ChecksOptions) error {
	findOptions := shared.FindOptions{
		Selector: opts.SelectorArg,
		Fields:   []string{"number", "baseRefName", "statusCheckRollup"},
	}
	if opts.WebMode {
		findOptions.Fields = []string{"number"}
	}
	pr, baseRepo, err := opts.Finder.Find(findOptions)
	if err != nil {
		return err
	}

	isTerminal := opts.IO.IsStdoutTTY()

	if opts.WebMode {
		openURL := ghrepo.GenerateRepoURL(baseRepo, "pull/%d/checks", pr.Number)
		if isTerminal {
			fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
		}
		return opts.Browser.Browse(openURL)
	}

	if len(pr.StatusCheckRollup.Nodes) == 0 {
		return fmt.Errorf("no commit found on the pull request")
	}

	rollup := pr.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes
	if len(rollup) == 0 {
		return fmt.Errorf("no checks reported on the '%s' branch", pr.BaseRefName)
	}

	passing := 0
	failing := 0
	skipping := 0
	pending := 0

	type output struct {
		mark      string
		bucket    string
		name      string
		elapsed   string
		link      string
		markColor func(string) string
	}

	cs := opts.IO.ColorScheme()

	outputs := []output{}

	for _, c := range pr.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes {
		mark := "✓"
		bucket := "pass"
		state := c.State
		markColor := cs.Green
		if state == "" {
			if c.Status == "COMPLETED" {
				state = c.Conclusion
			} else {
				state = c.Status
			}
		}
		switch state {
		case "SUCCESS":
			passing++
		case "SKIPPED", "NEUTRAL":
			mark = "-"
			markColor = cs.Gray
			skipping++
			bucket = "skipping"
		case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
			mark = "X"
			markColor = cs.Red
			failing++
			bucket = "fail"
		default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE"
			mark = "*"
			markColor = cs.Yellow
			pending++
			bucket = "pending"
		}

		elapsed := ""
		zeroTime := time.Time{}

		if c.StartedAt != zeroTime && c.CompletedAt != zeroTime {
			e := c.CompletedAt.Sub(c.StartedAt)
			if e > 0 {
				elapsed = e.String()
			}
		}

		link := c.DetailsURL
		if link == "" {
			link = c.TargetURL
		}

		name := c.Name
		if name == "" {
			name = c.Context
		}

		outputs = append(outputs, output{mark, bucket, name, elapsed, link, markColor})
	}

	sort.Slice(outputs, func(i, j int) bool {
		b0 := outputs[i].bucket
		n0 := outputs[i].name
		l0 := outputs[i].link
		b1 := outputs[j].bucket
		n1 := outputs[j].name
		l1 := outputs[j].link

		if b0 == b1 {
			if n0 == n1 {
				return l0 < l1
			}
			return n0 < n1
		}

		return (b0 == "fail") || (b0 == "pending" && b1 == "success")
	})

	tp := utils.NewTablePrinter(opts.IO)

	for _, o := range outputs {
		if isTerminal {
			tp.AddField(o.mark, nil, o.markColor)
			tp.AddField(o.name, nil, nil)
			tp.AddField(o.elapsed, nil, nil)
			tp.AddField(o.link, nil, nil)
		} else {
			tp.AddField(o.name, nil, nil)
			tp.AddField(o.bucket, nil, nil)
			if o.elapsed == "" {
				tp.AddField("0", nil, nil)
			} else {
				tp.AddField(o.elapsed, nil, nil)
			}
			tp.AddField(o.link, nil, nil)
		}

		tp.EndRow()
	}

	summary := ""
	if failing+passing+skipping+pending > 0 {
		if failing > 0 {
			summary = "Some checks were not successful"
		} else if pending > 0 {
			summary = "Some checks are still pending"
		} else {
			summary = "All checks were successful"
		}

		tallies := fmt.Sprintf("%d failing, %d successful, %d skipped, and %d pending checks",
			failing, passing, skipping, pending)

		summary = fmt.Sprintf("%s\n%s", cs.Bold(summary), tallies)
	}

	if isTerminal {
		fmt.Fprintln(opts.IO.Out, summary)
		fmt.Fprintln(opts.IO.Out)
	}

	err = tp.Render()
	if err != nil {
		return err
	}

	if failing+pending > 0 {
		return cmdutil.SilentError
	}

	return nil
}
