package trace

import (
	"context"
	"errors"
	"fmt"
	"regexp"

	"gitlab.com/gitlab-org/cli/pkg/iostreams"

	"gitlab.com/gitlab-org/cli/internal/glrepo"
	"gitlab.com/gitlab-org/cli/pkg/prompt"

	"gitlab.com/gitlab-org/cli/api"
	"gitlab.com/gitlab-org/cli/commands/ci/ciutils"
	"gitlab.com/gitlab-org/cli/commands/cmdutils"
	"gitlab.com/gitlab-org/cli/pkg/git"
	"gitlab.com/gitlab-org/cli/pkg/utils"

	"github.com/AlecAivazis/survey/v2"
	"github.com/AlecAivazis/survey/v2/terminal"
	"github.com/MakeNowJust/heredoc"
	"github.com/spf13/cobra"
	"github.com/xanzy/go-gitlab"
)

type TraceOpts struct {
	Branch string
	JobID  int

	BaseRepo   func() (glrepo.Interface, error)
	HTTPClient func() (*gitlab.Client, error)
	IO         *iostreams.IOStreams
}

func NewCmdTrace(f *cmdutils.Factory, runE func(traceOpts *TraceOpts) error) *cobra.Command {
	opts := &TraceOpts{
		IO: f.IO,
	}
	pipelineCITraceCmd := &cobra.Command{
		Use:   "trace [<job-id>] [flags]",
		Short: `Trace a CI/CD job log in real time`,
		Example: heredoc.Doc(`
	$ glab ci trace
	#=> interactively select a job to trace

	$ glab ci trace 224356863
	#=> trace job with id 224356863
	`),
		RunE: func(cmd *cobra.Command, args []string) error {
			var err error

			// support `-R, --repo` override
			//
			// NOTE: it is important to assign the BaseRepo and HTTPClient in RunE because
			// they are overridden in a PersistentRun hook (when `-R, --repo` is specified)
			// which runs before RunE is executed
			opts.BaseRepo = f.BaseRepo
			opts.HTTPClient = f.HttpClient

			if len(args) != 0 {
				opts.JobID = utils.StringToInt(args[0])
			}
			if opts.Branch == "" {
				opts.Branch, err = git.CurrentBranch()
				if err != nil {
					return err
				}
			}
			if runE != nil {
				return runE(opts)
			}
			return TraceRun(opts)
		},
	}

	pipelineCITraceCmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "Check pipeline status for a branch. (Default is the current branch)")
	return pipelineCITraceCmd
}

func TraceRun(opts *TraceOpts) error {
	apiClient, err := opts.HTTPClient()
	if err != nil {
		return err
	}

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

	if opts.JobID < 1 {
		fmt.Fprintf(opts.IO.StdOut, "\nSearching for latest pipeline on %s...\n", opts.Branch)

		pipeline, err := api.GetLastPipeline(apiClient, repo.FullName(), opts.Branch)
		if err != nil {
			return err
		}

		fmt.Fprintf(opts.IO.StdOut, "Getting jobs for pipeline %d...\n\n", pipeline.ID)

		jobs, err := api.GetPipelineJobs(apiClient, pipeline.ID, repo.FullName())
		if err != nil {
			return err
		}

		var jobOptions []string
		var selectedJob string

		for _, job := range jobs {
			jobOptions = append(jobOptions, fmt.Sprintf("%s (%d) - %s", job.Name, job.ID, job.Status))
		}

		promptOpts := &survey.Select{
			Message: "Select pipeline job to trace:",
			Options: jobOptions,
		}

		err = prompt.AskOne(promptOpts, &selectedJob)
		if err != nil {
			if errors.Is(err, terminal.InterruptErr) {
				return nil
			}

			return err
		}

		if selectedJob != "" {
			re := regexp.MustCompile(`(?s)\((.*)\)`)
			m := re.FindAllStringSubmatch(selectedJob, -1)
			opts.JobID = utils.StringToInt(m[0][1])
		} else if len(jobs) > 0 {
			opts.JobID = jobs[0].ID
		} else {
			// use commit statuses to show external jobs
			cs, err := api.GetCommitStatuses(apiClient, repo.FullName(), pipeline.SHA)
			if err != nil {
				return nil
			}

			c := opts.IO.Color()

			fmt.Fprint(opts.IO.StdOut, "Getting external jobs...")
			for _, status := range cs {
				var s string

				switch status.Status {
				case "success":
					s = c.Green(status.Status)
				case "error":
					s = c.Red(status.Status)
				default:
					s = c.Gray(status.Status)
				}
				fmt.Fprintf(opts.IO.StdOut, "(%s) %s\nURL: %s\n\n", s, c.Bold(status.Name), c.Gray(status.TargetURL))
			}

			return nil
		}
	}

	job, err := api.GetPipelineJob(apiClient, opts.JobID, repo.FullName())
	if err != nil {
		return err
	}
	fmt.Fprintln(opts.IO.StdOut)

	err = ciutils.RunTrace(context.Background(), apiClient, opts.IO.StdOut, repo.FullName(), job, job.Name)
	if err != nil {
		return err
	}

	return nil
}
