package service

import (
	"context"
	"errors"
	"strings"

	"github.com/docker/cli/cli"
	"github.com/docker/cli/cli/command"
	"github.com/docker/cli/cli/command/idresolver"
	"github.com/docker/cli/cli/command/node"
	"github.com/docker/cli/cli/command/task"
	"github.com/docker/cli/opts"
	"github.com/moby/moby/client"
	"github.com/spf13/cobra"
)

type psOptions struct {
	services  []string
	quiet     bool
	noResolve bool
	noTrunc   bool
	format    string
	filter    opts.FilterOpt
}

func newPsCommand(dockerCLI command.Cli) *cobra.Command {
	options := psOptions{filter: opts.NewFilterOpt()}

	cmd := &cobra.Command{
		Use:   "ps [OPTIONS] SERVICE [SERVICE...]",
		Short: "List the tasks of one or more services",
		Args:  cli.RequiresMinArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			options.services = args
			return runPS(cmd.Context(), dockerCLI, options)
		},
		ValidArgsFunction:     completeServiceNames(dockerCLI),
		DisableFlagsInUseLine: true,
	}
	flags := cmd.Flags()
	flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs")
	flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output")
	flags.BoolVar(&options.noResolve, "no-resolve", false, "Do not map IDs to Names")
	flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template")
	flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")

	return cmd
}

func runPS(ctx context.Context, dockerCli command.Cli, options psOptions) error {
	apiClient := dockerCli.Client()

	filter, notfound, err := createFilter(ctx, apiClient, options)
	if err != nil {
		return err
	}
	if err := updateNodeFilter(ctx, apiClient, filter); err != nil {
		return err
	}

	tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{Filters: filter})
	if err != nil {
		return err
	}

	format := options.format
	if len(format) == 0 {
		format = task.DefaultFormat(dockerCli.ConfigFile(), options.quiet)
	}
	if options.quiet {
		options.noTrunc = true
	}
	if err := task.Print(ctx, dockerCli, tasks, idresolver.New(apiClient, options.noResolve), !options.noTrunc, options.quiet, format); err != nil {
		return err
	}
	if len(notfound) != 0 {
		return errors.New(strings.Join(notfound, "\n"))
	}
	return nil
}

func createFilter(ctx context.Context, apiClient client.APIClient, options psOptions) (client.Filters, []string, error) {
	filter := options.filter.Value()

	serviceIDFilter := make(client.Filters)
	serviceNameFilter := make(client.Filters)
	for _, service := range options.services {
		serviceIDFilter.Add("id", service)
		serviceNameFilter.Add("name", service)
	}
	serviceByID, err := apiClient.ServiceList(ctx, client.ServiceListOptions{Filters: serviceIDFilter})
	if err != nil {
		return filter, nil, err
	}
	serviceByName, err := apiClient.ServiceList(ctx, client.ServiceListOptions{Filters: serviceNameFilter})
	if err != nil {
		return filter, nil, err
	}

	var notfound []string
	serviceCount := 0
loop:
	// Match services by 1. Full ID, 2. Full name, 3. ID prefix. An error is returned if the ID-prefix match is ambiguous
	for _, service := range options.services {
		for _, s := range serviceByID.Items {
			if s.ID == service {
				filter.Add("service", s.ID)
				serviceCount++
				continue loop
			}
		}
		for _, s := range serviceByName.Items {
			if s.Spec.Annotations.Name == service {
				filter.Add("service", s.ID)
				serviceCount++
				continue loop
			}
		}
		found := false
		for _, s := range serviceByID.Items {
			if strings.HasPrefix(s.ID, service) {
				if found {
					return filter, nil, errors.New("multiple services found with provided prefix: " + service)
				}
				filter.Add("service", s.ID)
				serviceCount++
				found = true
			}
		}
		if !found {
			notfound = append(notfound, "no such service: "+service)
		}
	}
	if serviceCount == 0 {
		return filter, nil, errors.New(strings.Join(notfound, "\n"))
	}
	return filter, notfound, err
}

func updateNodeFilter(ctx context.Context, apiClient client.APIClient, filter client.Filters) error {
	if nodeFilters, ok := filter["node"]; ok {
		for nodeFilter := range nodeFilters {
			nodeReference, err := node.Reference(ctx, apiClient, nodeFilter)
			if err != nil {
				return err
			}
			// TODO(thaJeztah): add utility to remove?
			delete(filter["node"], nodeFilter)
			filter.Add("node", nodeReference)
		}
	}
	return nil
}
