package cli

import (
	"context"
	"errors"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/kardianos/service"
	"github.com/minio/selfupdate"
	"github.com/spf13/cobra"
)

const (
	upgradeChannelDev     = "dev"
	upgradeChannelProd    = "prod"
	upgradeChannelDefault = "default"
)

// UpgradeCommand handles upgrade-related operations
type UpgradeCommand struct {
}

// NewUpgradeCommand creates a new upgrade command handler
func NewUpgradeCommand() (*UpgradeCommand, error) {
	return &UpgradeCommand{}, nil
}

// Upgrade performs the upgrade operation
func (uc *UpgradeCommand) Upgrade(cmd *cobra.Command, args []string) error {
	upgradeChannel := map[string]string{
		upgradeChannelDefault: "https://dl.controld.dev",
		upgradeChannelDev:     "https://dl.controld.dev",
		upgradeChannelProd:    "https://dl.controld.com",
	}
	if isStableVersion(curVersion()) {
		upgradeChannel[upgradeChannelDefault] = upgradeChannel[upgradeChannelProd]
	}

	bin, err := os.Executable()
	if err != nil {
		mainLog.Load().Fatal().Err(err).Msg("Failed to get current ctrld binary path")
	}

	readConfig(false)
	v.Unmarshal(&cfg)
	svcCmd := NewServiceCommand()
	s, p, err := svcCmd.initializeServiceManager()
	if err != nil {
		mainLog.Load().Error().Msg(err.Error())
		return nil
	}

	if iface == "" {
		iface = autoIface
	}
	p.preRun()
	if ir := runningIface(s); ir != nil {
		p.runningIface = ir.Name
		p.requiredMultiNICsConfig = ir.All
	}

	svcInstalled := true
	if _, err := s.Status(); errors.Is(err, service.ErrNotInstalled) {
		svcInstalled = false
	}

	oldBin := bin + oldBinSuffix
	baseUrl := upgradeChannel[upgradeChannelDefault]
	if len(args) > 0 {
		channel := args[0]
		switch channel {
		case upgradeChannelProd, upgradeChannelDev: // ok
		default:
			mainLog.Load().Fatal().Msgf("Upgrade argument must be either %q or %q", upgradeChannelProd, upgradeChannelDev)
		}
		baseUrl = upgradeChannel[channel]
	}

	dlUrl := upgradeUrl(baseUrl)
	mainLog.Load().Debug().Msgf("Downloading binary: %s", dlUrl)

	resp, err := getWithRetry(dlUrl, downloadServerIp)
	if err != nil {
		mainLog.Load().Fatal().Err(err).Msg("Failed to download binary")
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		mainLog.Load().Fatal().Msgf("Could not download binary: %s", http.StatusText(resp.StatusCode))
	}

	mainLog.Load().Debug().Msg("Updating current binary")
	if err := selfupdate.Apply(resp.Body, selfupdate.Options{OldSavePath: oldBin}); err != nil {
		if rerr := selfupdate.RollbackError(err); rerr != nil {
			mainLog.Load().Error().Err(rerr).Msg("Could not rollback old binary")
		}
		mainLog.Load().Fatal().Err(err).Msg("Failed to update current binary")
	}

	doRestart := func() bool {
		if !svcInstalled {
			return true
		}
		tasks := []task{
			{s.Stop, true, "Stop"},
			{func() error {
				// restore static DNS settings or DHCP
				p.resetDNS(false, true)
				return nil
			}, false, "Cleanup"},
			{func() error {
				time.Sleep(time.Second * 1)
				return nil
			}, false, "Waiting for service to stop"},
		}
		doTasks(tasks)

		tasks = []task{
			{s.Start, true, "Start"},
		}
		if doTasks(tasks) {
			if dir, err := socketDir(); err == nil {
				if cc := newSocketControlClient(context.TODO(), s, dir); cc != nil {
					_, _ = cc.post(ifacePath, nil)
					return true
				}
			}
		}
		return false
	}

	if svcInstalled {
		mainLog.Load().Debug().Msg("Restarting ctrld service using new binary")
	}

	if doRestart() {
		_ = os.Remove(oldBin)
		_ = os.Chmod(bin, 0755)
		ver := "unknown version"
		out, err := exec.Command(bin, "--version").CombinedOutput()
		if err != nil {
			mainLog.Load().Warn().Err(err).Msg("Failed to get new binary version")
		}
		if after, found := strings.CutPrefix(string(out), "ctrld version "); found {
			ver = after
		}
		mainLog.Load().Notice().Msgf("Upgrade successful - %s", ver)
		return nil
	}

	mainLog.Load().Warn().Msgf("Upgrade failed, restoring previous binary: %s", oldBin)
	if err := os.Remove(bin); err != nil {
		mainLog.Load().Fatal().Err(err).Msg("Failed to remove new binary")
	}
	if err := os.Rename(oldBin, bin); err != nil {
		mainLog.Load().Fatal().Err(err).Msg("Failed to restore old binary")
	}
	if doRestart() {
		mainLog.Load().Notice().Msg("Restored previous binary successfully")
		return nil
	}

	return nil
}

// InitUpgradeCmd creates the upgrade command with proper logic
func InitUpgradeCmd(rootCmd *cobra.Command) *cobra.Command {
	upgradeCmd := &cobra.Command{
		Use:       "upgrade",
		Short:     "Upgrading ctrld to latest version",
		ValidArgs: []string{upgradeChannelDev, upgradeChannelProd},
		Args:      cobra.MaximumNArgs(1),
		PreRun: func(cmd *cobra.Command, args []string) {
			checkHasElevatedPrivilege()
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			uc, err := NewUpgradeCommand()
			if err != nil {
				return err
			}
			return uc.Upgrade(cmd, args)
		},
	}

	rootCmd.AddCommand(upgradeCmd)

	return upgradeCmd
}
