package commands

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"text/tabwriter"

	"github.com/spf13/cobra"

	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless"
	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
	argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
	projectpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/project"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	"github.com/argoproj/argo-cd/v3/util/errors"
	utilio "github.com/argoproj/argo-cd/v3/util/io"
)

// NewProjectWindowsCommand returns a new instance of the `argocd proj windows` command
func NewProjectWindowsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	roleCommand := &cobra.Command{
		Use:   "windows",
		Short: "Manage a project's sync windows",
		Example: `
#Add a sync window to a project
argocd proj windows add my-project \
--schedule "0 0 * * 1-5" \
--duration 3600 \
--prune

#Delete a sync window from a project
argocd proj windows delete <project-name> <window-id>

#List project sync windows
argocd proj windows list <project-name>`,
		Run: func(c *cobra.Command, args []string) {
			c.HelpFunc()(c, args)
			os.Exit(1)
		},
	}
	roleCommand.AddCommand(NewProjectWindowsDisableManualSyncCommand(clientOpts))
	roleCommand.AddCommand(NewProjectWindowsEnableManualSyncCommand(clientOpts))
	roleCommand.AddCommand(NewProjectWindowsDisableSyncOverrunCommand(clientOpts))
	roleCommand.AddCommand(NewProjectWindowsEnableSyncOverrunCommand(clientOpts))
	roleCommand.AddCommand(NewProjectWindowsAddWindowCommand(clientOpts))
	roleCommand.AddCommand(NewProjectWindowsDeleteCommand(clientOpts))
	roleCommand.AddCommand(NewProjectWindowsListCommand(clientOpts))
	roleCommand.AddCommand(NewProjectWindowsUpdateCommand(clientOpts))
	return roleCommand
}

// newProjectWindowsToggleCommand creates a command for toggling a boolean field on a sync window
func newProjectWindowsToggleCommand(clientOpts *argocdclient.ClientOptions, use, short, long, example string, updateFn func(*v1alpha1.SyncWindow)) *cobra.Command {
	return &cobra.Command{
		Use:     use,
		Short:   short,
		Long:    long,
		Example: example,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 2 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}

			projName := args[0]
			id, err := strconv.Atoi(args[1])
			errors.CheckError(err)

			conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
			defer utilio.Close(conn)

			proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
			errors.CheckError(err)

			found := false
			for i, window := range proj.Spec.SyncWindows {
				if id == i {
					updateFn(window)
					found = true
					break
				}
			}
			if !found {
				errors.CheckError(fmt.Errorf("window with id '%d' not found", id))
			}

			_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
			errors.CheckError(err)
		},
	}
}

// NewProjectWindowsDisableManualSyncCommand returns a new instance of an `argocd proj windows disable-manual-sync` command
func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	return newProjectWindowsToggleCommand(
		clientOpts,
		"disable-manual-sync PROJECT ID",
		"Disable manual sync for a sync window",
		"Disable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
		`
#Disable manual sync for a sync window for the Project
argocd proj windows disable-manual-sync PROJECT ID

#Disabling manual sync for a windows set on the default project with Id 0
argocd proj windows disable-manual-sync default 0`,
		func(window *v1alpha1.SyncWindow) {
			window.ManualSync = false
		},
	)
}

// NewProjectWindowsEnableManualSyncCommand returns a new instance of an `argocd proj windows enable-manual-sync` command
func NewProjectWindowsEnableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	return newProjectWindowsToggleCommand(
		clientOpts,
		"enable-manual-sync PROJECT ID",
		"Enable manual sync for a sync window",
		"Enable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
		`
#Enabling manual sync for a general case
argocd proj windows enable-manual-sync PROJECT ID

#Enabling manual sync for a windows set on the default project with Id 2
argocd proj windows enable-manual-sync default 2

#Enabling manual sync with a custom message
argocd proj windows enable-manual-sync my-app-project --message "Manual sync initiated by admin`,
		func(window *v1alpha1.SyncWindow) {
			window.ManualSync = true
		},
	)
}

// NewProjectWindowsDisableSyncOverrunCommand returns a new instance of an `argocd proj windows disable-sync-overrun` command
func NewProjectWindowsDisableSyncOverrunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	return newProjectWindowsToggleCommand(
		clientOpts,
		"disable-sync-overrun PROJECT ID",
		"Disable sync overrun for a sync window",
		"Disable sync overrun for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
		`
#Disable sync overrun for a sync window for the Project
argocd proj windows disable-sync-overrun PROJECT ID

#Disabling sync overrun for a window set on the default project with Id 0
argocd proj windows disable-sync-overrun default 0`,
		func(window *v1alpha1.SyncWindow) {
			window.SyncOverrun = false
		},
	)
}

// NewProjectWindowsEnableSyncOverrunCommand returns a new instance of an `argocd proj windows enable-sync-overrun` command
func NewProjectWindowsEnableSyncOverrunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	return newProjectWindowsToggleCommand(
		clientOpts,
		"enable-sync-overrun PROJECT ID",
		"Enable sync overrun for a sync window",
		"Enable sync overrun for a sync window. When enabled on a deny window, syncs that started before the deny window will be allowed to continue. When enabled on an allow window, syncs that started during the allow window can continue after the window ends. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
		`
#Enable sync overrun for a sync window
argocd proj windows enable-sync-overrun PROJECT ID

#Enabling sync overrun for a window set on the default project with Id 2
argocd proj windows enable-sync-overrun default 2`,
		func(window *v1alpha1.SyncWindow) {
			window.SyncOverrun = true
		},
	)
}

// NewProjectWindowsAddWindowCommand returns a new instance of an `argocd proj windows add` command
func NewProjectWindowsAddWindowCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		kind         string
		schedule     string
		duration     string
		applications []string
		namespaces   []string
		clusters     []string
		manualSync   bool
		syncOverrun  bool
		timeZone     string
		andOperator  bool
		description  string
	)
	command := &cobra.Command{
		Use:   "add PROJECT",
		Short: "Add a sync window to a project",
		Example: `
#Add a 1 hour allow sync window
argocd proj windows add PROJECT \
    --kind allow \
    --schedule "0 22 * * *" \
    --duration 1h \
    --applications "*" \
    --description "Ticket 123"

#Add a deny sync window with the ability to manually sync and sync overrun.
argocd proj windows add PROJECT \
    --kind deny \
    --schedule "30 10 * * *" \
    --duration 30m \
    --applications "prod-\\*,website" \
    --namespaces "default,\\*-prod" \
    --clusters "prod,staging" \
    --manual-sync \
    --sync-overrun \
    --description "Ticket 123"`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			projName := args[0]
			conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
			defer utilio.Close(conn)

			proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
			errors.CheckError(err)

			err = proj.Spec.AddWindow(kind, schedule, duration, applications, namespaces, clusters, manualSync, timeZone, andOperator, description, syncOverrun)
			errors.CheckError(err)

			_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
			errors.CheckError(err)
		},
	}
	command.Flags().StringVarP(&kind, "kind", "k", "", "Sync window kind, either allow or deny")
	command.Flags().StringVar(&schedule, "schedule", "", "Sync window schedule in cron format. (e.g. --schedule \"0 22 * * *\")")
	command.Flags().StringVar(&duration, "duration", "", "Sync window duration. (e.g. --duration 1h)")
	command.Flags().StringSliceVar(&applications, "applications", []string{}, "Applications that the schedule will be applied to. Comma separated, wildcards supported (e.g. --applications prod-\\*,website)")
	command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
	command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
	command.Flags().BoolVar(&manualSync, "manual-sync", false, "Allow manual syncs for both deny and allow windows")
	command.Flags().BoolVar(&syncOverrun, "sync-overrun", false, "Allow syncs to continue: for deny windows, syncs that started before the window; for allow windows, syncs that started during the window")
	command.Flags().StringVar(&timeZone, "time-zone", "UTC", "Time zone of the sync window")
	command.Flags().BoolVar(&andOperator, "use-and-operator", false, "Use AND operator for matching applications, namespaces and clusters instead of the default OR operator")
	command.Flags().StringVar(&description, "description", "", `Sync window description`)

	return command
}

// NewProjectWindowsDeleteCommand returns a new instance of an `argocd proj windows delete` command
func NewProjectWindowsDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	command := &cobra.Command{
		Use:   "delete PROJECT ID",
		Short: "Delete a sync window from a project. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
		Example: `
#Delete a sync window from a project (default) with ID 0
argocd proj windows delete default 0

#Delete a sync window from a project (new-project) with ID 1
argocd proj windows delete new-project 1`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 2 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}

			projName := args[0]
			id, err := strconv.Atoi(args[1])
			errors.CheckError(err)

			conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
			defer utilio.Close(conn)

			proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
			errors.CheckError(err)

			err = proj.Spec.DeleteWindow(id)
			errors.CheckError(err)

			promptUtil := utils.NewPrompt(clientOpts.PromptsEnabled)
			canDelete := promptUtil.Confirm("Are you sure you want to delete sync window? [y/n]")
			if canDelete {
				_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
				errors.CheckError(err)
			} else {
				fmt.Print("The command to delete the sync window was cancelled\n")
			}
		},
	}
	return command
}

// NewProjectWindowsUpdateCommand returns a new instance of an `argocd proj windows update` command
func NewProjectWindowsUpdateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		schedule     string
		duration     string
		applications []string
		namespaces   []string
		clusters     []string
		timeZone     string
		description  string
	)
	command := &cobra.Command{
		Use:   "update PROJECT ID",
		Short: "Update a project sync window",
		Long:  "Update a project sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
		Example: `# Change a sync window's schedule
argocd proj windows update PROJECT ID \
    --schedule "0 20 * * *"
`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 2 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}

			projName := args[0]
			id, err := strconv.Atoi(args[1])
			errors.CheckError(err)

			conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
			defer utilio.Close(conn)

			proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
			errors.CheckError(err)

			for i, window := range proj.Spec.SyncWindows {
				if id == i {
					err := window.Update(schedule, duration, applications, namespaces, clusters, timeZone, description)
					if err != nil {
						errors.CheckError(err)
					}
				}
			}

			_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
			errors.CheckError(err)
		},
	}
	command.Flags().StringVar(&schedule, "schedule", "", "Sync window schedule in cron format. (e.g. --schedule \"0 22 * * *\")")
	command.Flags().StringVar(&duration, "duration", "", "Sync window duration. (e.g. --duration 1h)")
	command.Flags().StringSliceVar(&applications, "applications", []string{}, "Applications that the schedule will be applied to. Comma separated, wildcards supported (e.g. --applications prod-\\*,website)")
	command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
	command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
	command.Flags().StringVar(&timeZone, "time-zone", "UTC", "Time zone of the sync window. (e.g. --time-zone \"America/New_York\")")
	command.Flags().StringVar(&description, "description", "", "Sync window description")
	return command
}

// NewProjectWindowsListCommand returns a new instance of an `argocd proj windows list` command
func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var output string
	command := &cobra.Command{
		Use:   "list PROJECT",
		Short: "List project sync windows",
		Example: `
#List project windows
argocd proj windows list PROJECT

#List project windows in yaml format
argocd proj windows list PROJECT -o yaml

#List project windows info for a project name (test-project)
argocd proj windows list test-project`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			projName := args[0]
			conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
			defer utilio.Close(conn)

			proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
			errors.CheckError(err)
			switch output {
			case "yaml", "json":
				err := PrintResourceList(proj.Spec.SyncWindows, output, false)
				errors.CheckError(err)
			case "wide", "":
				printSyncWindows(proj)
			default:
				errors.CheckError(fmt.Errorf("unknown output format: %s", output))
			}
		},
	}
	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
	return command
}

// Print table of sync window data
func printSyncWindows(proj *v1alpha1.AppProject) {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	var fmtStr string
	headers := []any{"ID", "STATUS", "KIND", "SCHEDULE", "DURATION", "APPLICATIONS", "NAMESPACES", "CLUSTERS", "MANUALSYNC", "SYNCOVERRUN", "TIMEZONE", "USEANDOPERATOR"}
	fmtStr = strings.Repeat("%s\t", len(headers)) + "\n"
	fmt.Fprintf(w, fmtStr, headers...)
	if proj.Spec.SyncWindows.HasWindows() {
		for i, window := range proj.Spec.SyncWindows {
			isActive, _ := window.Active()
			vals := []any{
				strconv.Itoa(i),
				formatBoolOutput(isActive),
				window.Kind,
				window.Schedule,
				window.Duration,
				formatListOutput(window.Applications),
				formatListOutput(window.Namespaces),
				formatListOutput(window.Clusters),
				formatBoolEnabledOutput(window.ManualSync),
				formatBoolEnabledOutput(window.SyncOverrun),
				window.TimeZone,
				formatBoolEnabledOutput(window.UseAndOperator),
			}
			fmt.Fprintf(w, fmtStr, vals...)
		}
	}
	_ = w.Flush()
}

func formatListOutput(list []string) string {
	var o string
	if len(list) == 0 {
		o = "-"
	} else {
		o = strings.Join(list, ",")
	}
	return o
}

func formatBoolOutput(active bool) string {
	var o string
	if active {
		o = "Active"
	} else {
		o = "Inactive"
	}
	return o
}

func formatBoolEnabledOutput(active bool) string {
	var o string
	if active {
		o = "Enabled"
	} else {
		o = "Disabled"
	}
	return o
}
