package s6netdev

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
)

type S6SvcTree struct {
	s             map[S6SvcName]*S6Svc
	rcdir, envdir string
	root          string
}

type kv_file struct {
	name, value string
	perm        int
}

type S6Svc struct {
	Name                     S6SvcName
	Type                     S6SvcType
	Children                 []*S6Svc // Either dependencies or contents depending on type
	ProducerFor, ConsumerFor *S6Svc
	NotifFd                  int
	FlagEssential            bool
	Up, Down, Run            string
	env                      map[string]string
}

type S6SvcName string

func (s S6SvcName) Sanitize() S6SvcName {
	// Only sanitize slashes for now
	return S6SvcName(strings.ReplaceAll(string(s), "/", "_"))
}

type S6SvcType *string

var S6SvcTypes = struct {
	Oneshot, Longrun, Bundle string
}{
	Oneshot: "oneshot",
	Longrun: "longrun",
	Bundle:  "bundle",
}

func S6NewTree() *S6SvcTree {
	t := new(S6SvcTree)
	t.s = make(map[S6SvcName]*S6Svc)
	t.root = "/etc/s6"
	t.rcdir = "rc"
	t.envdir = "env"
	return t
}

func (t *S6SvcTree) S6New(name S6SvcName, svc_type S6SvcType) *S6Svc {
	if name == "" || svc_type == nil {
		return nil
	}
	if e, ok := t.s[name]; ok && e.Type == svc_type {
		return e
	}
	s := new(S6Svc)
	s.Name = name
	s.Type = svc_type
	t.s[name] = s
	return s
}
func S6Pipe(src, dst *S6Svc) {
	src.ProducerFor = dst
	dst.ConsumerFor = src
}

func (s *S6Svc) S6Children(children ...*S6Svc) {
	s.Children = append(s.Children, children...)
}

func (s *S6Svc) S6AddEnv(key, value string) {
	s.env[key] = value
}

func (s *S6Svc) S6DelEnv(key string) {
	delete(s.env, key)
}

func (t *S6SvcTree) S6AddSvc(s *S6Svc) {
	t.s[s.Name] = s
}

func (t *S6SvcTree) S6GetSvc(name S6SvcName) *S6Svc {
	return t.s[name]
}

func (t *S6SvcTree) S6Services() (r []*S6Svc) {
	for _, v := range t.s {
		r = append(r, v)
	}
	return
}

func (t *S6SvcTree) S6CommitService(s *S6Svc) (err error) {
	if err = os.Mkdir(t.rcdir, fs.ModeDir|0777); err != nil && !errors.Is(err, os.ErrExist) {
		return
	}

	sdir := filepath.Join(t.rcdir, string(s.Name))
	if err = os.Mkdir(sdir, fs.ModeDir|0777); err != nil {
		return
	}

	var kvfiles []kv_file
	kvfiles = append(kvfiles, kv_file{"type", *s.Type, 0666})
	if s.FlagEssential {
		kvfiles = append(kvfiles, kv_file{"flag-essential", "", 0666})
	}
	if s.ProducerFor != nil {
		kvfiles = append(kvfiles, kv_file{"producer-for", string(s.ProducerFor.Name), 0666})
	}
	if s.ConsumerFor != nil {
		kvfiles = append(kvfiles, kv_file{"consumer-for", string(s.ConsumerFor.Name), 0666})
	}
	if s.NotifFd != 0 {
		kvfiles = append(kvfiles, kv_file{"notification-fd", fmt.Sprintf("%d", s.NotifFd), 0666})
	}

	// Scripts
	switch s.Type {
	case &S6SvcTypes.Longrun:
		{
			if s.Run != "" {
				kvfiles = append(kvfiles, kv_file{"run", s.Run, 0777})
			}
		}
	case &S6SvcTypes.Oneshot:
		{
			if s.Up != "" {
				kvfiles = append(kvfiles, kv_file{"up", s.Up, 0777})
			}
			if s.Down != "" {
				kvfiles = append(kvfiles, kv_file{"down", s.Down, 0777})
			}
		}
	}

	for _, v := range kvfiles {
		var f *os.File
		if f, err = os.OpenFile(filepath.Join(sdir, v.name), os.O_CREATE|os.O_RDWR, fs.FileMode(v.perm)); err != nil && !errors.Is(err, os.ErrExist) {
			return
		}
		_, err = fmt.Fprintln(f, v.value)
		f.Close()
		if err != nil {
			return
		}
	}

	// Children
	var dirname string
	switch s.Type {
	case &S6SvcTypes.Bundle:
		dirname = "contents.d"
	default:
		dirname = "dependencies.d"
	}
	cdir := filepath.Join(sdir, dirname)
	if err = os.Mkdir(cdir, fs.ModeDir|0777); err != nil {
		return
	}

	for _, v := range s.Children {
		var f *os.File
		if f, err = os.Create(filepath.Join(cdir, string(v.Name))); err != nil && !errors.Is(err, os.ErrExist) {
			return
		}
		f.Close()
	}

	if len(s.env) != 0 {
		if err = os.Mkdir(t.envdir, fs.ModeDir|0777); err != nil && !errors.Is(err, os.ErrExist) {
			return
		}
		sdir := filepath.Join(t.envdir, string(s.Name))
		if err = os.Mkdir(sdir, fs.ModeDir|0777); err != nil {
			return
		}
		for k, v := range s.env {
			var f *os.File
			if f, err = os.Create(filepath.Join(sdir, string(k))); err != nil && !errors.Is(err, os.ErrExist) {
				return
			}
			_, err = fmt.Fprintln(f, v)
			f.Close()
			if err != nil {
				return
			}
		}
	}

	return
}