package delete

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/MakeNowJust/heredoc/v2"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"

	gitlab "gitlab.com/gitlab-org/api/client-go/v2"

	"gitlab.com/gitlab-org/cli/internal/cmdutils"
	"gitlab.com/gitlab-org/cli/internal/glrepo"
	"gitlab.com/gitlab-org/cli/internal/iostreams"
	"gitlab.com/gitlab-org/cli/internal/mcpannotations"
)

const (
	FlagDryRun    = "dry-run"
	FlagOlderThan = "older-than"
	FlagPage      = "page"
	FlagPaginate  = "paginate"
	FlagPerPage   = "per-page"
	FlagSource    = "source"
	FlagStatus    = "status"
)

var (
	pipelineStatuses = []string{
		string(gitlab.Running), string(gitlab.Pending), string(gitlab.Success), string(gitlab.Failed),
		string(gitlab.Canceled), string(gitlab.Skipped), string(gitlab.Created), string(gitlab.Manual),
	}
	pipelineSources = []string{
		"api", "chat", "external", "external_pull_request_event", "merge_request_event",
		"ondemand_dast_scan", "ondemand_dast_validation", "parent_pipeline", "pipeline",
		"push", "schedule", "security_orchestration_policy", "trigger", "web", "webide",
	}
)

func NewCmdDelete(f cmdutils.Factory) *cobra.Command {
	pipelineDeleteCmd := &cobra.Command{
		Use:   "delete <id> [flags]",
		Short: `Delete CI/CD pipelines.`,
		Long: heredoc.Docf(`
			You can delete by ID, or filter by status, source, or age. Use %[1]s--dry-run%[1]s to preview
			the pipelines that would be deleted.
		`, "`"),
		Example: heredoc.Doc(`
			# Delete a pipeline by ID
			glab ci delete 34

			# Delete multiple pipelines by ID
			glab ci delete 12,34,2

			# Delete all pipelines triggered through the API
			glab ci delete --source=api

			# Delete all failed pipelines
			glab ci delete --status=failed

			# Delete pipelines older than 24 hours
			glab ci delete --older-than 24h

			# Delete failed pipelines older than 24 hours
			glab ci delete --older-than 24h --status=failed`),
		Args: func(cmd *cobra.Command, args []string) error {
			olderThanDuration, _ := cmd.Flags().GetDuration(FlagOlderThan)
			status, _ := cmd.Flags().GetString(FlagStatus)
			source, _ := cmd.Flags().GetString(FlagSource)

			if olderThanDuration == 0 && status == "" && source == "" {
				return cobra.ExactArgs(1)(cmd, args)
			}

			if len(args) > 0 {
				return errors.New("either a status filter or a pipeline ID must be passed, but not both")
			}

			return nil
		},
		Annotations: map[string]string{
			mcpannotations.Destructive: "true",
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			var err error
			c := f.IO().Color()
			client, err := f.GitLabClient()
			if err != nil {
				return err
			}

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

			dryRunMode, _ := cmd.Flags().GetBool(FlagDryRun)

			var pipelineIDs []int
			if len(args) == 1 {
				pipelineIDs, err = parseRawPipelineIDs(args[0])
				if err != nil {
					return err
				}

				return runDeletion(f.IO(), pipelineIDs, dryRunMode, c, client, repo)
			}

			paginate, _ := cmd.Flags().GetBool(FlagPaginate)

			pipelineIDs, totalAvailable, truncated, err := listPipelineIDs(client, repo.FullName(), paginate, optsFromFlags(cmd.Flags()))
			if err != nil {
				return err
			}

			if err := runDeletion(f.IO(), pipelineIDs, dryRunMode, c, client, repo); err != nil {
				return err
			}

			if truncated {
				verb := "Deleted"
				if dryRunMode {
					verb = "Matched"
				}
				// GitLab omits X-Total for result sets larger than 10,000, so totalAvailable may be 0 even when truncated.
				if totalAvailable > int64(len(pipelineIDs)) {
					f.IO().LogErrorf(
						"%s %d of %d matching pipelines. Pass --paginate to act on all matches, or --per-page to fetch more per request.\n",
						verb, len(pipelineIDs), totalAvailable)
				} else {
					f.IO().LogErrorf(
						"%s %d matching pipelines; more matches exist. Pass --paginate to act on all matches, or --per-page to fetch more per request.\n",
						verb, len(pipelineIDs))
				}
			}

			return nil
		},
	}

	SetupCommandFlags(pipelineDeleteCmd.Flags())

	return pipelineDeleteCmd
}

func SetupCommandFlags(flags *pflag.FlagSet) {
	flags.BoolP(FlagDryRun, "", false, "Simulate process, but do not delete anything.")
	flags.StringP(FlagStatus, "s", "", fmt.Sprintf("Delete pipelines by status: %s.", strings.Join(pipelineStatuses, ", ")))
	flags.String(FlagSource, "", fmt.Sprintf("Filter pipelines by source: %s.", strings.Join(pipelineSources, ", ")))
	flags.Duration(FlagOlderThan, 0, "Filter pipelines older than the given duration. Valid units: h, m, s, ms, us, ns.")
	flags.BoolP(FlagPaginate, "", false, "Make additional HTTP requests to fetch all pages of pipelines. Respects '--per-page'.")
	flags.IntP(FlagPage, "", 0, "Page number.")
	flags.IntP(FlagPerPage, "", 0, "Number of items to list per page.")
}

func optsFromFlags(flags *pflag.FlagSet) *gitlab.ListProjectPipelinesOptions {
	opts := &gitlab.ListProjectPipelinesOptions{}
	page, _ := flags.GetInt(FlagPage)
	perPage, _ := flags.GetInt(FlagPerPage)

	if perPage != 0 {
		opts.PerPage = int64(perPage)
	}
	if page != 0 {
		opts.Page = int64(page)
	}

	source, _ := flags.GetString(FlagSource)
	status, _ := flags.GetString(FlagStatus)
	olderThanDuration, _ := flags.GetDuration(FlagOlderThan)

	if source != "" {
		opts.Source = new(source)
	}

	if status != "" {
		opts.Status = new(gitlab.BuildStateValue(status))
	}

	if olderThanDuration != 0 {
		opts.UpdatedBefore = new(time.Now().Add(-olderThanDuration))
	}

	return opts
}

func parseRawPipelineIDs(rawPipelineIDs string) ([]int, error) {
	var inputPipelineIDs []int
	for stringID := range strings.SplitSeq(rawPipelineIDs, ",") {
		id, err := strconv.Atoi(stringID)
		if err != nil {
			return nil, err
		}
		inputPipelineIDs = append(inputPipelineIDs, id)
	}

	return inputPipelineIDs, nil
}

func runDeletion(io *iostreams.IOStreams, pipelineIDs []int, dryRunMode bool, c *iostreams.ColorPalette, apiClient *gitlab.Client, repo glrepo.Interface) error {
	for _, id := range pipelineIDs {
		if dryRunMode {
			io.LogInfof("%s Pipeline #%d will be deleted.\n", c.DotWarnIcon(), id)
			continue
		}

		_, err := apiClient.Pipelines.DeletePipeline(repo.FullName(), int64(id))
		if err != nil {
			return err
		}

		io.LogInfof("%s Pipeline #%d deleted successfully.\n", c.RedCheck(), id)
	}
	fmt.Println()

	return nil
}

func listPipelineIDs(apiClient *gitlab.Client, repoName string, paginate bool, opts *gitlab.ListProjectPipelinesOptions) ([]int, int64, bool, error) {
	var pipelineIDs []int
	var totalAvailable int64
	var truncated bool

	hasRemaining := true
	first := true
	for hasRemaining {
		pipes, resp, err := apiClient.Pipelines.ListProjectPipelines(repoName, opts)
		if err != nil {
			return pipelineIDs, totalAvailable, truncated, err
		}

		if first && resp != nil {
			totalAvailable = resp.TotalItems
			first = false
		}

		for _, item := range pipes {
			pipelineIDs = append(pipelineIDs, int(item.ID))
		}

		opts.Page = resp.NextPage
		// NextPage is reliable even when X-Total-Pages is absent (large result sets);
		// CurrentPage != TotalPages would loop forever in that case.
		hasRemaining = paginate && resp.NextPage > 0
		if !hasRemaining && resp.NextPage > 0 {
			truncated = true
		}
	}

	return pipelineIDs, totalAvailable, truncated, nil
}
