package execution

import (
	"context"
	"io/ioutil"
	"os"
	"path/filepath"
	"sync"

	"github.com/docker/containerd/log"
	"github.com/pkg/errors"
)

const (
	InitProcessID    = "init"
	processesDirName = "processes"
	bundleFileName   = "bundle"
)

func LoadContainer(ctx context.Context, stateDir, id string) (c *Container, err error) {
	c = &Container{
		id:        id,
		stateDir:  stateDir,
		processes: make(map[string]Process, 1),
		ctx:       ctx,
		status:    Unknown,
	}

	data, err := ioutil.ReadFile(filepath.Join(stateDir, bundleFileName))
	if err != nil {
		err = errors.Wrapf(err, "failed to read bundle path")
		return
	}
	c.bundle = string(data)

	return
}

func NewContainer(ctx context.Context, stateDir, id, bundle string) (c *Container, err error) {
	c = &Container{
		id:        id,
		stateDir:  stateDir,
		bundle:    bundle,
		processes: make(map[string]Process, 1),
		status:    Created,
		ctx:       ctx,
	}
	defer func() {
		if err != nil {
			c.Cleanup()
			c = nil
		}
	}()

	if err = os.Mkdir(stateDir, 0700); err != nil {
		err = errors.Wrap(err, "failed to create container state dir")
		return
	}

	bundleFile := filepath.Join(stateDir, bundleFileName)
	if err = ioutil.WriteFile(bundleFile, []byte(bundle), 0600); err != nil {
		err = errors.Wrap(err, "failed to store bundle path")
		return
	}

	processesDir := filepath.Join(stateDir, processesDirName)
	if err = os.Mkdir(processesDir, 0700); err != nil {
		err = errors.Wrap(err, "failed to create processes statedir")
		return
	}

	return
}

type Container struct {
	id        string
	stateDir  string
	bundle    string
	processes map[string]Process
	status    Status

	ctx context.Context
	mu  sync.Mutex
}

func (c *Container) ID() string {
	return c.id
}

func (c *Container) Bundle() string {
	return c.bundle
}

func (c *Container) Wait() (uint32, error) {
	initProcess := c.GetProcess(InitProcessID)
	return initProcess.Wait()
}

func (c *Container) Status() Status {
	initProcess := c.GetProcess(InitProcessID)
	return initProcess.Status()
}

func (c *Container) AddProcess(p Process) {
	c.mu.Lock()
	c.processes[p.ID()] = p
	c.mu.Unlock()
}

func (c *Container) RemoveProcess(id string) error {
	if _, ok := c.processes[id]; !ok {
		return errors.Errorf("no such process %s", id)
	}

	c.mu.Lock()
	delete(c.processes, id)
	c.mu.Unlock()

	processStateDir := filepath.Join(c.stateDir, processesDirName, id)
	err := os.RemoveAll(processStateDir)
	if err != nil {
		return errors.Wrap(err, "failed to remove process state dir")
	}

	return nil
}

func (c *Container) GetProcess(id string) Process {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.processes[id]
}

func (c *Container) Processes() []Process {
	var procs []Process

	c.mu.Lock()
	for _, p := range c.processes {
		procs = append(procs, p)
	}
	c.mu.Unlock()

	return procs
}

// ProcessStateDir returns the path of the state dir for a given
// process id. The process doesn't have to exist for this to succeed.
func (c *Container) ProcessStateDir(id string) string {
	return filepath.Join(c.stateDir, processesDirName, id)
}

// ProcessesStateDir returns a map matching process ids to their state
// directory
func (c *Container) ProcessesStateDir() (map[string]string, error) {
	root := filepath.Join(c.stateDir, processesDirName)
	dirs, err := ioutil.ReadDir(root)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to list processes state dirs")
	}

	procs := make(map[string]string, 1)
	for _, d := range dirs {
		if d.IsDir() {
			procs[d.Name()] = filepath.Join(root, d.Name())
		}
	}

	return procs, nil
}

func (c *Container) Cleanup() {
	if err := os.RemoveAll(c.stateDir); err != nil {
		log.G(c.ctx).Warnf("failed to remove container state dir: %v", err)
	}
}

func (c *Container) Context() context.Context {
	return c.ctx
}