package rescontainerlxc

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/google/uuid"
	"github.com/rs/zerolog"
	"github.com/vishvananda/netlink"
	"golang.org/x/crypto/ssh"

	"github.com/opensvc/om3/v3/core/actioncontext"
	"github.com/opensvc/om3/v3/core/actionrollback"
	"github.com/opensvc/om3/v3/core/naming"
	"github.com/opensvc/om3/v3/core/object"
	"github.com/opensvc/om3/v3/core/provisioned"
	"github.com/opensvc/om3/v3/core/rawconfig"
	"github.com/opensvc/om3/v3/core/resource"
	"github.com/opensvc/om3/v3/core/status"
	"github.com/opensvc/om3/v3/core/vpath"
	"github.com/opensvc/om3/v3/util/capabilities"
	"github.com/opensvc/om3/v3/util/command"
	"github.com/opensvc/om3/v3/util/envprovider"
	"github.com/opensvc/om3/v3/util/file"
	"github.com/opensvc/om3/v3/util/funcopt"
	"github.com/opensvc/om3/v3/util/hostname"
	"github.com/opensvc/om3/v3/util/ping"
)

const (
	cpusetDir = "/sys/fs/cgroup/cpuset"
)

var (
	prefixes = []string{
		"/",
		"/usr",
		"/usr/local",
	}
)

var _ resource.Encaper = (*T)(nil)

type (
	T struct {
		resource.T
		resource.Restart
		resource.SSH
		resource.SCSIPersistentReservation
		Path                     naming.Path    `json:"path"`
		ObjectID                 uuid.UUID      `json:"object_id"`
		Nodes                    []string       `json:"nodes"`
		DNS                      []string       `json:"dns"`
		SCSIReserv               bool           `json:"scsireserv"`
		PromoteRW                bool           `json:"promote_rw"`
		OsvcRootPath             string         `json:"osvc_root_path"`
		GuestOS                  string         `json:"guest_os"`
		Name                     string         `json:"name"`
		Hostname                 string         `json:"hostname"`
		DataDir                  string         `json:"data_dir"`
		RootDir                  string         `json:"root_dir"`
		ConfigFile               string         `json:"cf"`
		Template                 string         `json:"template"`
		TemplateOptions          []string       `json:"template_options"`
		CreateSecretsEnvironment []string       `json:"create_secrets_environment"`
		CreateConfigsEnvironment []string       `json:"create_configs_environment"`
		CreateEnvironment        []string       `json:"create_environment"`
		RCmd                     []string       `json:"rcmd"`
		StartTimeout             *time.Duration `json:"start_timeout"`
		StopTimeout              *time.Duration `json:"stop_timeout"`

		cache map[string]interface{}
	}

	header interface {
		Head() string
	}
	resourceLister interface {
		Resources() resource.Drivers
	}
)

func New() resource.Driver {
	t := &T{
		cache: make(map[string]interface{}),
	}
	return t
}

func (t *T) stopCgroup() error {
	p := t.cgroupDir()
	if p == "" {
		return nil
	}
	p = filepath.Join(cpusetDir, p)
	if err := t.cleanupCgroup(p); err != nil {
		return err
	}
	return nil
}

func (t *T) startCgroup() error {
	p := t.cgroupDir()
	if p == "" {
		return nil
	}
	p = filepath.Join(cpusetDir, p)
	if err := t.cleanupCgroup(p); err != nil {
		return err
	}
	if err := t.setCpusetCloneChildren(); err != nil {
		return err
	}
	if err := t.createCgroup(p); err != nil {
		return err
	}
	return nil
}

func (t *T) Start(ctx context.Context) error {
	if v, err := t.isUp(ctx); err != nil {
		return err
	} else if v {
		t.Log().Infof("container %s is already up", t.Name)
		return nil
	}
	if err := t.startCgroup(); err != nil {
		return err
	}
	if err := t.installCF(ctx); err != nil {
		return err
	}
	if err := t.start(ctx); err != nil {
		return err
	}
	actionrollback.Register(ctx, func(ctx context.Context) error {
		return t.Stop(ctx)
	})
	return nil
}

func (t *T) Stop(ctx context.Context) error {
	if v, err := t.isUp(ctx); err != nil {
		return err
	} else if !v {
		t.Log().Infof("container %s is already down", t.Name)
		return nil
	}
	links := t.getLinks(ctx)
	if err := t.stopOrKill(ctx); err != nil {
		return err
	}
	if err := t.cleanupLinks(links); err != nil {
		return err
	}
	if err := t.stopCgroup(); err != nil {
		return err
	}
	return nil
}

// NetNSPath implements the resource.NetNSPather optional interface.
// Used by ip.netns and ip.route to configure network stuff in the container.
func (t *T) NetNSPath(ctx context.Context) (string, error) {
	if pid, err := t.getPID(ctx); err != nil {
		return "", err
	} else if pid == 0 {
		return "", nil
	} else {
		return fmt.Sprintf("/proc/%d/ns/net", pid), nil
	}
}

// PID implements the resource.PIDer optional interface.
// Used by ip.netns to name the veth pair devices.
func (t *T) PID(ctx context.Context) int {
	pid, _ := t.getPID(ctx)
	return pid
}

func (t *T) Status(ctx context.Context) status.T {
	/*
		if t.PG.IsFrozen() {
			return status.NotApplicable
		}
	*/
	if v, err := t.isUp(ctx); err != nil {
		t.StatusLog().Error("%s", err)
		return status.Undef
	} else if v {
		return status.Up
	}
	return status.Down
}

// Label implements Label from resource.Driver interface,
// it returns a formatted short description of the Resource
func (t *T) Label(_ context.Context) string {
	return t.Name
}

func (t *T) UnprovisionAsLeader(ctx context.Context) error {
	return t.unprovision(ctx)
}

func (t *T) UnprovisionAsFollower(ctx context.Context) error {
	return t.unprovision(ctx)
}

func (t *T) unprovision(ctx context.Context) error {
	if err := t.purgeLxcVar(ctx); err != nil {
		return err
	}
	if err := t.purgeConfigFile(ctx); err != nil {
		return err
	}
	return nil
}

func (t *T) purgeConfigFile(ctx context.Context) error {
	p, err := t.configFile(ctx)
	if err != nil {
		return err
	}
	if !file.Exists(p) {
		return nil
	}
	t.Log().Infof("remove %s", p)
	if err := os.Remove(p); err != nil {
		return err
	}
	return nil
}

func (t *T) purgeLxcVar(ctx context.Context) error {
	p := t.lxcPath(ctx)
	if p == "" {
		t.Log().Tracef("purgeLxcVar: lxcPath() is empty. consider we have nothing to purge.")
		return nil
	}
	p = filepath.Join(p, t.Name)
	if !file.Exists(p) {
		t.Log().Infof("%s is already cleaned up", p)
		return nil
	}
	if file.IsProtected(p) {
		t.Log().Warnf("refuse to remove %s", p)
		return nil
	}
	t.Log().Infof("remove %s", p)
	if err := os.RemoveAll(p); err != nil {
		return err
	}
	return nil
}

func (t *T) ProvisionAsLeader(ctx context.Context) error {
	if t.exists(ctx) {
		t.Log().Infof("container %s is already created", t.Name)
		return nil
	}
	args := []string{"--name", t.Name}
	rootDir, err := t.rootDir(ctx)
	if err == nil {
		if !file.Exists(rootDir) {
			if err := os.MkdirAll(rootDir, 0755); err != nil {
				return err
			}
		}
		args = append(args, "--dir", rootDir)
	}
	cf, err := t.configFile(ctx)
	if err == nil && cf != "" && file.Exists(cf) {
		args = append(args, "-f", cf)
	}
	dataDir, err := t.dataDir(ctx)
	if err == nil && dataDir != "" {
		args = append(args, "-P", dataDir)
		if cf == "" {
			cf = filepath.Join(dataDir, t.Name, "config")
			if file.Exists(cf) {
				args = append(args, "-f", cf)
			}
		}
	}
	if t.Template != "" {
		args = append(args, "-t", t.Template)
		if len(t.TemplateOptions) > 0 {
			args = append(args, "--")
			args = append(args, t.TemplateOptions...)
		}
	} else {
		return fmt.Errorf("the template keyword is mandatory for provision")
	}
	env, err := t.createEnv()
	if err != nil {
		return err
	}
	cmd := command.New(
		command.WithName("lxc-create"),
		command.WithArgs(args),
		command.WithLogger(t.Log()),
		command.WithCommandLogLevel(zerolog.InfoLevel),
		command.WithStdoutLogLevel(zerolog.InfoLevel),
		command.WithStderrLogLevel(zerolog.InfoLevel),
		//command.WithTimeout(*t.StartTimeout),
		command.WithEnv(env),
	)
	return cmd.Run()
}

func (t *T) createEnv() ([]string, error) {
	env := []string{
		"DEBIAN_FRONTEND=noninteractive",
		"DEBIAN_PRIORITY=critical",
	}
	env = append(env, t.createEnvProxy()...)
	env = append(env, t.CreateEnvironment...)
	more, err := envprovider.From(t.CreateSecretsEnvironment, t.Path.Namespace, "sec")
	if err != nil {
		return env, err
	}
	env = append(env, more...)
	more, err = envprovider.From(t.CreateConfigsEnvironment, t.Path.Namespace, "cfg")
	if err != nil {
		return env, err
	}
	env = append(env, more...)
	return env, nil
}

func (t *T) createEnvSecrets() ([]string, error) {
	return envprovider.From(t.CreateSecretsEnvironment, t.Path.Namespace, "sec")
}

func (t *T) createEnvConfigs() ([]string, error) {
	return envprovider.From(t.CreateConfigsEnvironment, t.Path.Namespace, "cfg")
}

func (t *T) createEnvProxy() []string {
	env := []string{}
	keys := []string{
		"http_proxy", "https_proxy", "ftp_proxy", "rsync_proxy",
	}
	for _, k := range keys {
		if v, ok := os.LookupEnv(k); ok {
			env = append(env, k+"="+v)
		}
		k = strings.ToUpper(k)
		if v, ok := os.LookupEnv(k); ok {
			env = append(env, k+"="+v)
		}
	}
	return env
}

func (t *T) Unprovision(ctx context.Context) error {
	return nil
}

func (t *T) Provisioned(ctx context.Context) (provisioned.T, error) {
	return provisioned.NotApplicable, nil
}

// Signal implements object.signaler
func (t *T) Signal(ctx context.Context, sig syscall.Signal) error {
	pid := t.PID(ctx)
	if pid == 0 {
		return nil
	}
	return syscall.Kill(pid, sig)
}

func (t *T) copyFrom(ctx context.Context, src, dst string) error {
	rootDir, err := t.rootDir(ctx)
	if err != nil {
		return err
	}
	src = filepath.Join(rootDir, src)
	return file.Copy(src, dst)
}

func (t *T) copyTo(ctx context.Context, src, dst string) error {
	rootDir, err := t.rootDir(ctx)
	if err != nil {
		return err
	}
	dst = filepath.Join(rootDir, dst)
	return file.Copy(src, dst)
}

func (t *T) rcmd(ctx context.Context, envs []string) ([]string, error) {
	var args []string
	if len(t.RCmd) > 0 {
		args = t.RCmd
		args = append(args, t.GetHostname())
		args = append(args, "--")
		args = append(args, envs...)
		return args, nil
	}

	hasPIDNS := file.Exists("/proc/1/ns/pid")
	if exe, err := exec.LookPath("lxc-attach"); err == nil && hasPIDNS {
		if p, err := t.dataDir(ctx); err == nil && p != "" {
			args = []string{exe, "-n", t.Name, "-P", p, "--clear-env"}
		} else {
			args = []string{exe, "-n", t.Name, "--clear-env"}
		}
	}
	for _, e := range envs {
		args = append(args, "-v", e)
	}
	if args[len(args)-1] != "--" {
		args = append(args, "--")
	}
	return args, nil
}

// SetEncapFileOwnership sets the ownership of the file to be the
// same ownership than the container root dir, which may be not root
// for unprivileged containers.
func (t *T) SetEncapFileOwnership(ctx context.Context, p string) error {
	rootDir, err := t.rootDir(ctx)
	if err != nil {
		return err
	}
	return file.CopyOwnership(rootDir, p)
}

func (t *T) Enter(ctx context.Context) error {
	sh := "/bin/bash"
	rcmd, err := t.rcmd(ctx, []string{})
	if err != nil {
		return err
	}
	args := append(rcmd, sh)
	cmd := exec.Command(args[0], args[1:]...)
	_ = cmd.Run()

	switch cmd.ProcessState.ExitCode() {
	case 126, 127:
		sh = "/bin/sh"
	}
	args = append(rcmd, sh)
	cmd = exec.CommandContext(ctx, args[0], args[1:]...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func (t *T) stopTimeout() *int {
	if t.StopTimeout == nil {
		return nil
	}
	i := int(t.StopTimeout.Seconds())
	return &i
}

func (t *T) GetHostname() string {
	if t.Hostname != "" {
		return t.Hostname
	}
	return t.Name
}

func (t *T) setHostname(ctx context.Context) error {
	if err := t.checkHostname(ctx); err != nil {
		t.Log().Infof("container hostname already set")
		return nil
	}
	p, err := t.hostnameFile(ctx)
	if err != nil {
		return err
	}
	h := t.GetHostname()
	if err := os.WriteFile(p, []byte(h+"\n"), 0644); err != nil {
		return err
	}
	t.Log().Infof("container hostname set to %s", h)
	return nil
}

func (t *T) checkHostname(ctx context.Context) error {
	p, err := t.hostnameFile(ctx)
	if err != nil {
		return err
	}
	b, err := os.ReadFile(p)
	if err != nil {
		return fmt.Errorf("can not read container hostname: %w", err)
	}
	target := t.GetHostname()
	if string(b) != target {
		return fmt.Errorf("container hostname is %s, should be %s", string(b), target)
	}
	return nil
}

func (t *T) hostnameFile(ctx context.Context) (string, error) {
	rootDir, err := t.rootDir(ctx)
	if err != nil {
		return "", err
	}
	return filepath.Join(rootDir, "etc/hostname"), nil
}

func (t *T) configFile(ctx context.Context) (string, error) {
	if p, ok := t.cache["configFile"]; ok {
		return p.(string), nil
	}
	if p, err := t.getConfigFile(ctx); err == nil {
		t.cache["configFile"] = p
		return p, nil
	} else {
		return "", err
	}
}

func (t *T) getPrefix() (string, error) {
	for _, p := range prefixes {
		f := filepath.Join(p, "bin/lxc-start")
		if file.Exists(f) {
			return p, nil
		}
	}
	return "", fmt.Errorf("lxc install prefix not found")
}

// orderedPrefixes returns prefixes with the one containing lxc-start first
func (t *T) orderedPrefixes(prefix string) ([]string, error) {
	l := []string{prefix}
	for _, p := range prefixes {
		if p == prefix {
			continue
		}
		l = append(l, p)
	}
	return l, nil
}

func (t *T) prefix() (string, error) {
	if p, ok := t.cache["prefix"]; ok {
		return p.(string), nil
	}
	if p, err := t.getPrefix(); err == nil {
		t.cache["prefix"] = p
		return p, nil
	} else {
		return "", err
	}
}

func (t *T) getConfigFile(ctx context.Context) (string, error) {
	if t.ConfigFile != "" {
		return vpath.HostPath(ctx, t.ConfigFile, t.Path.Namespace)
	}
	if t.DataDir != "" {
		p := filepath.Join(t.DataDir, t.Name, "config")
		return vpath.HostPath(ctx, p, t.Path.Namespace)
	}
	relDir := "/var/lib/lxc"

	// seen on debian squeeze : prefix is /usr, but containers'
	// config files paths are /var/lib/lxc/$name/config
	// try prefix first, fallback to other know prefixes
	prefix, err := t.prefix()
	if err != nil {
		return "", err
	}
	prefixes, err := t.orderedPrefixes(prefix)
	if err != nil {
		return "", err
	}
	for _, p := range prefixes {
		p = filepath.Join(p, relDir, t.Name, "config")
		if file.Exists(p) {
			return p, nil
		}
	}

	// on Oracle Linux, config is in /etc/lxc
	p := filepath.Join("/etc/lxc", t.Name, "config")
	if file.Exists(p) {
		return p, nil
	}

	return "", fmt.Errorf("unable to find the container configuration file")
}

func (t *T) getConfigValue(ctx context.Context, key string) (string, error) {
	cf, err := t.configFile(ctx)
	f, err := os.Open(cf)
	if err != nil {
		return "", err
	}
	defer f.Close()
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		s := scanner.Text()
		l := strings.SplitN(s, "=", 2)
		if len(l) != 2 {
			continue
		}
		k := l[0]
		k = strings.TrimLeft(k, "#; \t")
		k = strings.TrimSpace(k)
		if key != k {
			continue
		}
		v := l[1]
		v = strings.TrimSpace(v)
		return v, nil
	}
	return "", fmt.Errorf("key %s not found in %s", key, cf)
}

func (t *T) rootDirFromConfigFile(ctx context.Context) (string, error) {
	p, err := t.rootfsFromConfigFile(ctx)
	if err != nil {
		return "", err
	}
	if !strings.Contains(p, ":") {
		return p, nil
	}
	// zfs:/tank/svc1, nbd:file1, dir:/foo ...
	l := strings.SplitN(p, ":", 2)
	p = l[1]
	if p != "" && !strings.HasPrefix(p, "/") {
		// zfs:tank/svc1
		p = "/" + p
	}
	return p, nil
}

func (t *T) rootfsFromConfigFile(ctx context.Context) (string, error) {
	if p, err := t.getConfigValue(ctx, "lxc.rootfs"); err == nil {
		return p, nil
	}
	if p, err := t.getConfigValue(ctx, "lxc.rootfs.path"); err == nil {
		return p, nil
	}
	return "", fmt.Errorf("could not determine lxc container rootfs")
}

func (t *T) getRootDir(ctx context.Context) (string, error) {
	if t.RootDir != "" {
		return vpath.HostPath(ctx, t.RootDir, t.Path.Namespace)
	}
	return t.rootDirFromConfigFile(ctx)
}

func (t *T) rootDir(ctx context.Context) (string, error) {
	if p, ok := t.cache["rootDir"]; ok {
		return p.(string), nil
	}
	if p, err := t.getRootDir(ctx); err == nil {
		t.cache["rootDir"] = p
		return p, nil
	} else {
		return "", err
	}
}

func (t *T) dataDir(ctx context.Context) (string, error) {
	if t.DataDir == "" {
		return "", nil
	}
	if p, ok := t.cache["dataDir"]; ok {
		return p.(string), nil
	}
	if p, err := vpath.HostPath(ctx, t.DataDir, t.Path.Namespace); err == nil {
		t.cache["dataDir"] = p
		return p, nil
	} else {
		return "", err
	}
}

func (t *T) nativeConfigFile(ctx context.Context) string {
	if p, ok := t.cache["nativeConfigFile"]; ok {
		return p.(string)
	}
	p := func() string {
		if p := t.lxcPath(ctx); p != "" {
			return filepath.Join(p, t.Name, "config")
		}
		exe, err := exec.LookPath("lxc-info")
		if err != nil {
			return ""
		}
		dir := filepath.Dir(exe)
		if !strings.HasSuffix(dir, "bin") {
			return ""
		}
		dir = filepath.Dir(dir)
		switch dir {
		case "/", "/usr":
			if v, _ := file.ExistsAndDir("/var/lib/lxc"); v {
				return fmt.Sprintf("/var/lib/lxc/%s/config", t.Name)
			}
			if v, _ := file.ExistsAndDir("/etc/lxc"); v {
				return fmt.Sprintf("/etc/lxc/%s/config", t.Name)
			}
		case "/usr/local":
			if v, _ := file.ExistsAndDir("/usr/local/var/lib/lxc"); v {
				return fmt.Sprintf("/usr/local/var/lib/lxc/%s/config", t.Name)
			}
		}
		return ""
	}()
	t.cache["nativeConfigFile"] = p
	return p
}

func (t *T) lxcPath(ctx context.Context) string {
	if p, ok := t.cache["lxcPath"]; ok {
		return p.(string)
	}
	p := func() string {
		if p, err := t.dataDir(ctx); err != nil {
			return p
		}
		p := "/var/lib/lxc"
		if v, _ := file.ExistsAndDir(p); v {
			return p
		}
		p = "/usr/local/var/lib/lxc"
		if v, _ := file.ExistsAndDir(p); v {
			return p
		}
		return ""
	}()
	t.cache["lxcPath"] = p
	return p
}

func (t *T) ToSync(ctx context.Context) []string {
	l := make([]string, 0)

	// Don't synchronize container.lxc config in /var/lib/lxc if not shared
	// Non shared container resource mandates a private container for each
	// service instance, thus synchronizing the lxc config is counter productive
	// and can even lead to provisioning failure on secondary nodes.
	if !t.Shared {
		return l
	}

	// The config file might be in a umounted fs resource,
	// in which case, no need to ask for its sync as the sync won't happen
	cf, err := t.configFile(ctx)
	if err != nil {
		return l
	}
	if !file.Exists(cf) {
		return l
	}

	// The config file is hosted on a fs resource.
	// Let the user replicate it via a sync resource if the fs is not
	// shared. If the fs is shared, it must not be replicated to avoid
	// copying on the remote underlying fs (which may block a zfs dataset
	// mount).
	r, err := t.resourceHandlingFile(ctx, cf)
	if err != nil {
		return l
	}
	if r == nil {
		return l
	}

	// replicate the config file in the system standard path
	l = append(l, cf)
	return l

}

func (t *T) obj() (interface{}, error) {
	return object.New(t.Path, object.WithVolatile(true))
}

func (t *T) resourceHandlingFile(ctx context.Context, p string) (resource.Driver, error) {
	obj, err := t.obj()
	if err != nil {
		return nil, err
	}
	b, ok := obj.(resourceLister)
	if !ok {
		return nil, nil
	}
	for _, r := range b.Resources() {
		h, ok := r.(header)
		if !ok {
			continue
		}
		if v, err := r.Provisioned(ctx); err != nil {
			continue
		} else if v == provisioned.False {
			continue
		}
		if h.Head() == p {
			return r, nil
		}
	}
	return nil, nil
}

// ContainerHead implements the interface replacing b2.1 the zonepath resource attribute
func (t *T) ContainerHead(ctx context.Context) (string, error) {
	return t.rootDir(ctx)
}

func (t *T) cpusetDir() string {
	path := ""
	if !file.Exists(cpusetDir) {
		t.Log().Tracef("startCgroup: %s does not exist", cpusetDir)
		return ""
	}
	if t.cgroupDirCapable() {
		p := t.cgroupDir()
		if p == "" {
			return ""
		}
		path = filepath.Join(cpusetDir, p)
	} else {
		path = filepath.Join(cpusetDir, "lxc")
	}
	return path
}

func (t *T) setCpusetCloneChildren() error {
	path := t.cpusetDir()
	if path == "" {
		return nil
	}
	if !file.Exists(path) {
		if err := os.MkdirAll(path, 0755); err != nil {
			return fmt.Errorf("set clone_children for container %s: %w", t.Name, err)
		} else {
			t.Log().Infof("%s created", path)
		}
	}
	paths := make([]string, 0)
	for p := path; p != cpusetDir; p = filepath.Dir(p) {
		paths = append(paths, p)
	}
	sort.Sort(sort.StringSlice(paths))

	setFile := func(p string, v []byte) error {
		b, err := os.ReadFile(p)
		if err != nil {
			t.Log().Tracef("%s does not exist", p)
			return nil
		}
		if bytes.Compare(b, v) == 0 {
			t.Log().Tracef("%s already set to %s", p, v)
			return nil
		}
		err = os.WriteFile(p, v, 0644)
		if err != nil {
			return err
		}
		t.Log().Infof("%s set to %s", p, v)
		return nil
	}
	alignFile := func(p string) error {
		base := filepath.Base(p)
		ref := filepath.Join(cpusetDir, base)
		b, err := os.ReadFile(ref)
		if err != nil {
			t.Log().Tracef("%s does not exist", ref)
			return nil
		}
		return setFile(p, b)
	}
	setDir := func(p string) error {
		if err := alignFile(filepath.Join(p, "cpuset.mems")); err != nil {
			return err
		}
		if err := alignFile(filepath.Join(p, "cpuset.cpus")); err != nil {
			return err
		}
		if err := setFile(filepath.Join(p, "cgroup.clone_children"), []byte("1\n")); err != nil {
			return err
		}
		return nil
	}

	for _, p := range paths {
		if err := setDir(p); err != nil {
			return err
		}
	}
	return nil
}

// cgroupDir returns the container resource cgroup path, relative to a controller head.
func (t *T) cgroupDir() string {
	return strings.TrimPrefix(t.GetPGID(), "/")
}

func (t *T) cgroupDirCapable() bool {
	return capabilities.Has(drvID.Cap() + ".cgroup_dir")
}

func (t *T) createCgroup(p string) error {
	if file.Exists(p) {
		t.Log().Tracef("%s already exists", p)
		return nil
	}
	if err := os.MkdirAll(p, 0755); err != nil {
		return fmt.Errorf("create %s: %w", p, err)
	}
	t.Log().Infof("%s created", p)
	return nil
}

func (t *T) cleanupCgroup(p string) error {
	patterns := []string{
		fmt.Sprintf("/sys/fs/cgroup/*/lxc/%s-[0-9]", t.Name),
		fmt.Sprintf("/sys/fs/cgroup/*/lxc/%s", t.Name),
		fmt.Sprintf("%s", p),
		fmt.Sprintf("%s/lxc.*", p),
	}
	paths := []string{}
	for _, pattern := range patterns {
		more, err := filepath.Glob(pattern)
		if err != nil {
			return err
		}
		paths = append(paths, more...)
	}
	sort.Sort(sort.Reverse(sort.StringSlice(paths)))
	for _, path := range paths {
		t.Log().Infof("remove %s", path)
		if err := os.Remove(path); err != nil {
			t.Log().Warnf("%s", err)
		}
	}
	return nil
}

func (t *T) installCF(ctx context.Context) error {
	cf, err := t.configFile(ctx)
	if err != nil {
		return err
	}
	nativeCF := t.nativeConfigFile(ctx)
	if nativeCF == "" {
		t.Log().Tracef("could not determine the config file standard hosting directory")
		return nil
	}
	if cf == nativeCF {
		return nil
	}
	nativeDir := filepath.Dir(nativeCF)
	if !file.Exists(nativeDir) {
		if err := os.MkdirAll(nativeDir, 0755); err != nil {
			return err
		}
	}
	if err := file.Copy(cf, nativeCF); err != nil {
		return fmt.Errorf("install %s as %s: %w", cf, nativeCF, err)
	}
	t.Log().Infof("%s installed as %s", cf, nativeCF)
	return err
}

func (t *T) dataDirArgs(ctx context.Context) []string {
	if dataDir, err := t.dataDir(ctx); err == nil && dataDir != "" {
		return []string{"-P", dataDir}
	}
	return []string{}
}

func (t *T) isUpInfo(ctx context.Context) bool {
	args := []string{"--name", t.Name}
	args = append(args, t.dataDirArgs(ctx)...)
	cmd := command.New(
		command.WithContext(ctx),
		command.WithName("lxc-info"),
		command.WithArgs(args),
		command.WithBufferedStdout(),
	)
	b, err := cmd.Output()
	if err != nil {
		return false
	}
	v := strings.Contains(string(b), "RUNNING")
	return v
}

func (t *T) exists(ctx context.Context) bool {
	args := []string{"--name", t.Name}
	args = append(args, t.dataDirArgs(ctx)...)
	cmd := command.New(
		command.WithContext(ctx),
		command.WithName("lxc-info"),
		command.WithArgs(args),
		command.WithBufferedStdout(),
		//command.WithCommandLogLevel(zerolog.InfoLevel),
		//command.WithStdoutLogLevel(zerolog.InfoLevel),
		//command.WithStderrLogLevel(zerolog.ErrorLevel),
	)
	return cmd.Run() == nil
}

func (t *T) cleanupLink(s string) error {
	link, err := netlink.LinkByName(s)
	if err != nil {
		t.Log().Tracef("link %s already deleted", s)
		return nil
	}
	if err := netlink.LinkDel(link); err != nil {
		return fmt.Errorf("link %s delete: %w", s, err)
	}

	t.Log().Infof("link %s deleted", s)
	return nil
}

func (t *T) cleanupLinks(links []string) error {
	for _, link := range links {
		if err := t.cleanupLink(link); err != nil {
			return err
		}
	}
	return nil
}

func (t *T) getPID(ctx context.Context) (int, error) {
	args := []string{"--name", t.Name}
	args = append(args, t.dataDirArgs(ctx)...)
	opts := []funcopt.O{
		command.WithContext(ctx),
		command.WithName("lxc-info"),
		command.WithArgs(args),
		command.WithBufferedStdout(),
		command.WithIgnoredExitCodes(0, 1),
	}
	cmd := command.New(opts...)
	b, err := cmd.Output()
	if err != nil {
		return 0, err
	}
	if cmd.ExitCode() == 1 {
		// container is not running
		return 0, nil
	}
	scanner := bufio.NewScanner(bytes.NewReader(b))
	for scanner.Scan() {
		s := scanner.Text()
		if strings.HasPrefix(s, "PID:") {
			fields := strings.Fields(s)
			if len(fields) < 2 {
				continue
			}
			return strconv.Atoi(fields[1])
		}
	}
	return 0, nil
}

func (t *T) getLinks(ctx context.Context) []string {
	l := make([]string, 0)
	args := []string{"--name", t.Name}
	args = append(args, t.dataDirArgs(ctx)...)
	cmd := command.New(
		command.WithContext(ctx),
		command.WithName("lxc-info"),
		command.WithArgs(args),
		command.WithBufferedStdout(),
	)
	b, err := cmd.Output()
	if err != nil {
		return l
	}
	scanner := bufio.NewScanner(bytes.NewReader(b))
	for scanner.Scan() {
		s := scanner.Text()
		if strings.HasPrefix(s, "Link:") {
			fields := strings.Fields(s)
			if len(fields) < 2 {
				continue
			}
			l = append(l, fields[1])
		}
	}
	return l
}

func (t *T) isUpPS(ctx context.Context) bool {
	cmd := command.New(
		command.WithContext(ctx),
		command.WithName("lxc-ps"),
		command.WithVarArgs("--name", t.Name),
		command.WithBufferedStdout(),
	)
	b, err := cmd.Output()
	if err != nil {
		return false
	}
	v := strings.Contains(string(b), t.Name)
	return v
}

func (t *T) isUp(ctx context.Context) (bool, error) {
	if p, err := exec.LookPath("lxc-ps"); err == nil && p != "" {
		return t.isUpPS(ctx), nil
	}
	return t.isUpInfo(ctx), nil
}

func (t *T) start(ctx context.Context) error {
	cgroupDir := t.cgroupDir()
	cf, err := t.configFile(ctx)
	if err != nil {
		return err
	}
	outFile := fmt.Sprintf("/var/tmp/svc_%s_lxc_start.log", t.Name)
	args := []string{"-d", "-n", t.Name, "-o", outFile}
	if t.cgroupDirCapable() {
		args = append(args, "-s", "lxc.cgroup.dir="+cgroupDir)
	}
	if cf != "" {
		args = append(args, "-f", cf)
	}
	args = append(args, t.dataDirArgs(ctx)...)
	cmd := command.New(
		command.WithContext(ctx),
		command.WithName("lxc-start"),
		command.WithArgs(args),
		command.WithLogger(t.Log()),
		command.WithCommandLogLevel(zerolog.InfoLevel),
		command.WithStdoutLogLevel(zerolog.InfoLevel),
		command.WithStderrLogLevel(zerolog.ErrorLevel),
		command.WithTimeout(*t.StartTimeout),
	)
	return cmd.Run()
}

func (t *T) stopOrKill(ctx context.Context) error {
	if actioncontext.IsForce(ctx) {
		return t.kill(ctx)
	}
	if err := t.stop(ctx); err == nil {
		return err
	} else {
		t.Log().Warnf("stop: %s", err)
	}
	return t.kill(ctx)
}

func (t *T) stop(ctx context.Context) error {
	args := []string{"-n", t.Name}
	args = append(args, t.dataDirArgs(ctx)...)
	cmd := command.New(
		command.WithContext(ctx),
		command.WithName("lxc-stop"),
		command.WithArgs(args),
		command.WithLogger(t.Log()),
		command.WithCommandLogLevel(zerolog.InfoLevel),
		command.WithStdoutLogLevel(zerolog.InfoLevel),
		command.WithStderrLogLevel(zerolog.ErrorLevel),
		command.WithTimeout(*t.StartTimeout),
	)
	return cmd.Run()
}

func (t *T) kill(ctx context.Context) error {
	args := []string{"-n", t.Name, "--kill"}
	args = append(args, t.dataDirArgs(ctx)...)
	cmd := command.New(
		command.WithContext(ctx),
		command.WithName("lxc-stop"),
		command.WithArgs(args),
		command.WithLogger(t.Log()),
		command.WithCommandLogLevel(zerolog.InfoLevel),
		command.WithStdoutLogLevel(zerolog.InfoLevel),
		command.WithStderrLogLevel(zerolog.ErrorLevel),
		command.WithTimeout(*t.StartTimeout),
	)
	return cmd.Run()
}

// LinkNames implements the interface necessary for the container.lxc resources
// to be targeted by ip.cni, ip.netns, ...
func (t *T) LinkNames() []string {
	return []string{t.RID()}
}

func (t *T) abortPing(hn string) bool {
	timeout := 5 * time.Second
	t.Log().Infof("abort? checking %s availability with ping (%s)", hn, timeout)
	isAlive, err := ping.Ping(hn, timeout)
	if err != nil {
		t.Log().Errorf("abort? ping failed: %s", err)
		return true
	}
	if isAlive {
		t.Log().Errorf("abort! %s is alive", hn)
		return true
	} else {
		t.Log().Tracef("abort? %s is not alive", hn)
		return false
	}
}

func (t *T) Abort(ctx context.Context) bool {
	if v, err := t.isUp(ctx); err != nil {
		t.Log().Warnf("abort? %s", err)
		return false
	} else if v {
		// the local instance is already up.
		// let the local start report the unnecessary start steps
		return false
	}
	hn := t.GetHostname()
	if t.abortPing(hn) {
		return true
	}
	if n, err := t.upPeer(); err != nil {
		return false
	} else if n != "" {
		t.Log().Infof("abort! %s is up on %s", hn, n)
		return true
	}
	return false
}

func (t *T) upPeer() (string, error) {
	hn := hostname.Hostname()
	isPeerUp := func(n string) (bool, error) {
		client, err := t.NewSSHClient(n)
		if err != nil {
			return false, err
		}
		defer client.Close()
		session, err := client.NewSession()
		if err != nil {
			return false, err
		}
		defer session.Close()
		var b bytes.Buffer
		session.Stdout = &b
		err = session.Run(fmt.Sprintf("lxc-info -n %s -p", t.Name))
		if err == nil {
			return true, nil
		}
		ee := err.(*ssh.ExitError)
		ec := ee.Waitmsg.ExitStatus()
		return ec == 0, err
	}
	for _, n := range t.Nodes {
		if hn == n {
			continue
		}
		if v, err := isPeerUp(n); err != nil {
			t.Log().Tracef("ssh abort check on %s: %s", n, err)
			continue
		} else if v {
			return n, nil
		}
	}
	return "", nil
}

func (t *T) EncapCmd(ctx context.Context, args []string, envs []string, stdin io.Reader) (resource.Commander, error) {
	baseArgs, err := t.rcmd(ctx, envs)
	if err != nil {
		return nil, err
	}
	cmd := exec.CommandContext(ctx, baseArgs[0], append(baseArgs[1:], args...)...)
	cmd.Stdin = stdin
	return cmd, nil
}

func (t *T) EncapCp(ctx context.Context, src, dst string) error {
	rootDir, err := t.rootDir(ctx)
	if err != nil {
		return err
	}
	dst = filepath.Join(rootDir, dst)
	return file.Copy2(src, dst)
}

func (t *T) GetOsvcRootPath() string {
	if t.OsvcRootPath != "" {
		return filepath.Join(t.OsvcRootPath, "bin", "om")
	}
	return filepath.Join(rawconfig.Paths.Bin, "om")
}
