// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package talos

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"google.golang.org/protobuf/types/known/durationpb"

	"github.com/siderolabs/talos/cmd/talosctl/pkg/talos/global"
	"github.com/siderolabs/talos/cmd/talosctl/pkg/talos/helpers"
	machineapi "github.com/siderolabs/talos/pkg/machinery/api/machine"
	"github.com/siderolabs/talos/pkg/machinery/client"
	"github.com/siderolabs/talos/pkg/machinery/client/multiplex"
	"github.com/siderolabs/talos/pkg/machinery/config/configpatcher"
	"github.com/siderolabs/talos/pkg/machinery/constants"
)

var applyConfigCmdFlags struct {
	helpers.Mode
	global.InsecureFlags

	patches          []string
	filename         string
	dryRun           bool
	configTryTimeout time.Duration
}

// applyConfigCmd represents the applyConfiguration command.
var applyConfigCmd = &cobra.Command{
	Use:     "apply-config",
	Aliases: []string{"apply"},
	Short:   "Apply a new configuration to a node",
	Long:    ``,
	Args:    cobra.MaximumNArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		var (
			cfgBytes []byte
			err      error
		)

		if len(args) > 0 {
			if args[0] != "config" && !strings.EqualFold(args[0], "machineconfig") {
				cmd.Help() //nolint:errcheck

				return fmt.Errorf("unknown positional argument %s", args[0])
			} else if cmd.CalledAs() == "apply-config" {
				cmd.Help() //nolint:errcheck

				return errors.New("expected no positional arguments")
			}
		}

		if applyConfigCmdFlags.filename == "" {
			return errors.New("no filename supplied for configuration")
		}

		cfgBytes, err = os.ReadFile(applyConfigCmdFlags.filename)
		if err != nil {
			return fmt.Errorf("failed to read configuration from %q: %w", applyConfigCmdFlags.filename, err)
		}

		if len(cfgBytes) < 1 {
			return errors.New("no configuration data read")
		}

		if len(applyConfigCmdFlags.patches) != 0 {
			var (
				cfg     configpatcher.Input
				patches []configpatcher.Patch
			)

			patches, err = configpatcher.LoadPatches(applyConfigCmdFlags.patches)
			if err != nil {
				return err
			}

			cfg, err = configpatcher.Apply(configpatcher.WithBytes(cfgBytes), patches)
			if err != nil {
				return err
			}

			cfgBytes, err = cfg.Bytes()
			if err != nil {
				return err
			}
		}

		ctx := cmd.Context()

		clientFactory, err := NewClientFactory(ctx, &applyConfigCmdFlags)
		if err != nil {
			return err
		}

		defer clientFactory.Close() //nolint:errcheck

		respCh := multiplex.UnaryViaFactory(
			ctx,
			clientFactory,
			func(ctx context.Context, c *client.Client) (*machineapi.ApplyConfigurationResponse, error) {
				return c.ApplyConfiguration(ctx, &machineapi.ApplyConfigurationRequest{
					Data:           cfgBytes,
					Mode:           applyConfigCmdFlags.Mode.Mode,
					DryRun:         applyConfigCmdFlags.dryRun,
					TryModeTimeout: durationpb.New(applyConfigCmdFlags.configTryTimeout),
				})
			},
		)

		var errs error

		for resp := range respCh {
			if resp.Err != nil {
				errs = errors.Join(errs, fmt.Errorf("%s: error applying configuration: %w", resp.Node, resp.Err))

				continue
			}

			helpers.PrintApplyResults(resp.Payload)
		}

		return errs
	},
}

func init() {
	applyConfigCmdFlags.InsecureFlags.AddFlags(applyConfigCmd)
	applyConfigCmd.Flags().StringVarP(&applyConfigCmdFlags.filename, "file", "f", "", "the filename of the updated configuration")
	applyConfigCmd.Flags().BoolVar(&applyConfigCmdFlags.dryRun, "dry-run", false, "check how the config change will be applied in dry-run mode")
	applyConfigCmd.Flags().StringArrayVarP(&applyConfigCmdFlags.patches, "config-patch", "p", nil, "the list of config patches to apply to the local config file before sending it to the node")
	applyConfigCmd.Flags().DurationVar(&applyConfigCmdFlags.configTryTimeout, "timeout", constants.ConfigTryTimeout, "the config will be rolled back after specified timeout (if try mode is selected)")
	helpers.AddModeFlags(&applyConfigCmdFlags.Mode, applyConfigCmd)
	addCommand(applyConfigCmd)
}
