package cli

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"runtime"

	"github.com/coreos/go-systemd/v22/unit"
	"github.com/kardianos/service"
)

// newService wraps service.New call to return service.Service
// wrapper which is suitable for the current platform.
func newService(i service.Interface, c *service.Config) (service.Service, error) {
	s, err := service.New(i, c)
	if err != nil {
		return nil, err
	}
	switch {
	case s.Platform() == "unix-systemv":
		return &sysV{s}, nil
	case s.Platform() == "linux-systemd":
		return &systemd{s}, nil
	case s.Platform() == "darwin-launchd":
		return newLaunchd(s), nil

	}
	return s, nil
}

// sysV wraps a service.Service, and provide start/stop/status command
// base on "/etc/init.d/<service_name>".
//
// Use this on system where "service" command is not available.
type sysV struct {
	service.Service
}

func (s *sysV) installed() bool {
	fi, err := os.Stat("/etc/init.d/ctrld")
	if err != nil {
		return false
	}
	mode := fi.Mode()
	return mode.IsRegular() && (mode&0111) != 0
}

func (s *sysV) Start() error {
	if !s.installed() {
		return service.ErrNotInstalled
	}
	_, err := exec.Command("/etc/init.d/ctrld", "start").CombinedOutput()
	return err
}

func (s *sysV) Stop() error {
	if !s.installed() {
		return service.ErrNotInstalled
	}
	_, err := exec.Command("/etc/init.d/ctrld", "stop").CombinedOutput()
	return err
}

func (s *sysV) Restart() error {
	if !s.installed() {
		return service.ErrNotInstalled
	}
	// We don't care about error returned by s.Stop,
	// because the service may already be stopped.
	_ = s.Stop()
	return s.Start()
}

func (s *sysV) Status() (service.Status, error) {
	if !s.installed() {
		return service.StatusUnknown, service.ErrNotInstalled
	}
	return unixSystemVServiceStatus()
}

// systemd wraps a service.Service, and provide status command to
// report the status correctly.
type systemd struct {
	service.Service
}

func (s *systemd) Status() (service.Status, error) {
	out, _ := exec.Command("systemctl", "status", "ctrld").CombinedOutput()
	if bytes.Contains(out, []byte("/FAILURE)")) {
		return service.StatusStopped, nil
	}
	return s.Service.Status()
}

func (s *systemd) Start() error {
	const systemdUnitFile = "/etc/systemd/system/ctrld.service"
	f, err := os.Open(systemdUnitFile)
	if err != nil {
		return err
	}
	defer f.Close()
	if opts, change := ensureSystemdKillMode(f); change {
		mode := os.FileMode(0644)
		buf, err := io.ReadAll(unit.Serialize(opts))
		if err != nil {
			return err
		}
		if err := os.WriteFile(systemdUnitFile, buf, mode); err != nil {
			return err
		}
		if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
			return fmt.Errorf("systemctl daemon-reload failed: %w\n%s", err, string(out))
		}
		mainLog.Load().Debug().Msg("Set KillMode=process successfully")
	}
	return s.Service.Start()
}

// ensureSystemdKillMode ensure systemd unit file is configured with KillMode=process.
// This is necessary for running self-upgrade flow.
func ensureSystemdKillMode(r io.Reader) (opts []*unit.UnitOption, change bool) {
	opts, err := unit.DeserializeOptions(r)
	if err != nil {
		mainLog.Load().Error().Err(err).Msg("Failed to deserialize options")
		return
	}
	change = true
	needKillModeOpt := true
	killModeOpt := unit.NewUnitOption("Service", "KillMode", "process")
	for _, opt := range opts {
		if opt.Match(killModeOpt) {
			needKillModeOpt = false
			change = false
			break
		}
		if opt.Section == killModeOpt.Section && opt.Name == killModeOpt.Name {
			opt.Value = killModeOpt.Value
			needKillModeOpt = false
			break
		}
	}
	if needKillModeOpt {
		opts = append(opts, killModeOpt)
	}
	return opts, change
}

// newLaunchd creates a new launchd service wrapper
func newLaunchd(s service.Service) *launchd {
	return &launchd{
		Service:      s,
		statusErrMsg: "Permission denied",
	}
}

// launchd wraps a service.Service, and provide status command to
// report the status correctly when not running as root on Darwin.
//
// TODO: remove this wrapper once https://github.com/kardianos/service/issues/400 fixed.
type launchd struct {
	service.Service
	statusErrMsg string
}

func (l *launchd) Status() (service.Status, error) {
	if os.Geteuid() != 0 {
		return service.StatusUnknown, errors.New(l.statusErrMsg)
	}
	return l.Service.Status()
}

type task struct {
	f            func() error
	abortOnError bool
	Name         string
}

// doTasks executes a list of tasks and returns success status
func doTasks(tasks []task) bool {
	for _, task := range tasks {
		mainLog.Load().Debug().Msgf("Running task %s", task.Name)
		if err := task.f(); err != nil {
			if task.abortOnError {
				mainLog.Load().Error().Msgf("Error running task %s: %v", task.Name, err)
				return false
			}
			// if this is darwin stop command, dont print debug
			// since launchctl complains on every start
			if runtime.GOOS != "darwin" || task.Name != "Stop" {
				mainLog.Load().Debug().Msgf("Error running task %s: %v", task.Name, err)
			}
		}
	}
	return true
}

// checkHasElevatedPrivilege checks if the process has elevated privileges and exits if not
func checkHasElevatedPrivilege() {
	ok, err := hasElevatedPrivilege()
	if err != nil {
		mainLog.Load().Error().Msgf("Could not detect user privilege: %v", err)
		return
	}
	if !ok {
		mainLog.Load().Error().Msg("Please relaunch process with admin/root privilege.")
		os.Exit(1)
	}
}

// unixSystemVServiceStatus checks the status of a Unix System V service
func unixSystemVServiceStatus() (service.Status, error) {
	out, err := exec.Command("/etc/init.d/ctrld", "status").CombinedOutput()
	if err != nil {
		return service.StatusUnknown, nil
	}

	switch string(bytes.ToLower(bytes.TrimSpace(out))) {
	case "running":
		return service.StatusRunning, nil
	default:
		return service.StatusStopped, nil
	}
}
