package snapshot

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"github.com/docker/containerd"
)

// Manager provides an API for allocating, snapshotting and mounting
// abstract, layer-based filesystems. The model works by building up sets of
// directories with parent-child relationships.
//
// These differ from the concept of the graphdriver in that the
// Manager has no knowledge of images or containers. Users simply
// prepare and commit directories. We also avoid the integration between graph
// driver's and the tar format used to represent the changesets.
//
// Importing a Layer
//
// To import a layer, we simply have the Manager provide a list of
// mounts to be applied such that our dst will capture a changeset. We start
// out by getting a path to the layer tar file and creating a temp location to
// unpack it to:
//
//	layerPath, tmpLocation := getLayerPath(), mkTmpDir() // just a path to layer tar file.
//
// We then use a Manager to prepare the temporary location as a
// snapshot point:
//
// 	lm := NewManager()
//	mounts, err := lm.Prepare(tmpLocation, "")
// 	if err != nil { ... }
//
// Note that we provide "" as the parent, since we are applying the diff to an
// empty directory. We get back a list of mounts from Manager.Prepare.
// Before proceeding, we perform all these mounts:
//
//	if err := MountAll(mounts); err != nil { ... }
//
// Once the mounts are performed, our temporary location is ready to capture
// a diff. In practice, this works similar to a filesystem transaction. The
// next step is to unpack the layer. We have a special function unpackLayer
// that applies the contents of the layer to target location and calculates the
// DiffID of the unpacked layer (this is a requirement for docker
// implementation):
//
// 	digest, err := unpackLayer(tmpLocation, layer) // unpack into layer location
// 	if err != nil { ... }
//
// When the above completes, we should have a filesystem the represents the
// contents of the layer. Careful implementations should verify that digest
// matches the expected DiffID. When completed, we unmount the mounts:
//
//	unmount(mounts) // optional, for now
//
// Now that we've verified and unpacked our layer, we create a location to
// commit the actual diff. For this example, we are just going to use the layer
// digest, but in practice, this will probably be the ChainID:
//
// 	diffPath := filepath.Join("/layers", digest) // name location for the uncompressed layer digest
//	if err := lm.Commit(diffPath, tmpLocation); err != nil { ... }
//
// Now, we have a layer in the Manager that can be accessed with the
// opaque diffPath provided during commit.
//
// Importing the Next Layer
//
// Making a layer depend on the above is identical to the process described
// above except that the parent is provided as diffPath when calling
// Manager.Prepare:
//
// 	mounts, err := lm.Prepare(tmpLocation, parentDiffPath)
//
// The diff will be captured at tmpLocation, as the layer is applied.
//
// Running a Container
//
// To run a container, we simply provide Manager.Prepare the diffPath
// of the image we want to start the container from. After mounting, the
// prepared path can be used directly as the container's filesystem:
//
// 	mounts, err := lm.Prepare(containerRootFS, imageDiffPath)
//
// The returned mounts can then be passed directly to the container runtime. If
// one would like to create a new image from the filesystem,
// Manager.Commit is called:
//
// 	if err := lm.Commit(newImageDiff, containerRootFS); err != nil { ... }
//
// Alternatively, for most container runs, Manager.Rollback will be
// called to signal Manager to abandon the changes.
//
// TODO(stevvooe): Consider an alternate API that provides an active object to
// represent the lifecycle:
//
// 	work, err := lm.Prepare(dst, parent)
//  mountAll(work.Mounts())
// 	work.Commit() || work.Rollback()
//
// TODO(stevvooe): Manager should be an interface with several
// implementations, similar to graphdriver.
type Manager struct {
	root string // root provides paths for internal storage.

	// just a simple overlay implementation.
	active  map[string]activeLayer
	parents map[string]string // diff to parent for all committed
}

type activeLayer struct {
	parent   string
	upperdir string
	workdir  string
}

func NewManager(root string) (*Manager, error) {
	if err := os.MkdirAll(root, 0777); err != nil {
		return nil, err
	}

	return &Manager{
		root:    root,
		active:  make(map[string]activeLayer),
		parents: make(map[string]string),
	}, nil
}

// Prepare returns a set of mounts such that dst can be used as a location for
// reading and writing data. If parent is provided, the dst will be setup to
// capture changes between dst and parent. The "default" parent, "", is an
// empty directory.
//
// If the caller intends to write data to dst, they should perform all mounts
// provided before doing so. The location defined by dst should be used as the
// working directory for any associated activity, such as running a container
// or importing a layer.
//
// The implementation may choose to write data directly to dst, opting to
// return no mounts instead.
//
// Once the writes have completed, Manager.Commit or
// Manager.Rollback should be called on dst.
func (lm *Manager) Prepare(dst, parent string) ([]containerd.Mount, error) {
	// we want to build up lowerdir, upperdir and workdir options for the
	// overlay mount.
	//
	// lowerdir is a list of parent diffs, ordered from top to bottom (base
	// layer to the "right").
	//
	// upperdir will become the diff location. This will be renamed to the
	// location provided in commit.
	//
	// workdir needs to be there but it is not really clear why.
	var opts []string

	upperdir, err := ioutil.TempDir(lm.root, "diff-")
	if err != nil {
		return nil, err
	}
	opts = append(opts, "upperdir="+upperdir)

	workdir, err := ioutil.TempDir(lm.root, "work-")
	if err != nil {
		return nil, err
	}
	opts = append(opts, "workdir="+workdir)

	empty := filepath.Join(lm.root, "empty")
	if err := os.MkdirAll(empty, 0777); err != nil {
		return nil, err
	}

	// TODO(stevvooe): Write this metadata to disk to make it useful.
	lm.active[dst] = activeLayer{
		parent:   parent,
		upperdir: upperdir,
		workdir:  workdir,
	}

	var parents []string
	for parent != "" {
		parents = append(parents, parent)
		parent = lm.Parent(parent)
	}

	if len(parents) == 0 {
		parents = []string{empty}
	}

	opts = append(opts, "lowerdir="+strings.Join(parents, ","))

	return []containerd.Mount{
		{
			Type:    "overlay",
			Source:  "none",
			Target:  dst,
			Options: opts,
		},
	}, nil
}

// View behaves identically to Prepare except the result may not be committed
// back to the snappshot manager.
//
// Whether or not these are readonly mounts is implementation specific, but the
// caller may write to dst freely.
//
// Calling Commit on dst will result in an error. Calling Rollback on dst
// should be done to cleanup resources.
func (lm *Manager) View(dst, parent string) ([]containerd.Mount, error) {
	panic("not implemented")
}

// Commit captures the changes between dst and its parent into the path
// provided by diff. The path diff can then be used with the layer
// manipulator's other methods to access the diff content.
//
// The contents of diff are opaque to the caller and may be specific to the
// implementation of the layer backend.
func (lm *Manager) Commit(diff, dst string) error {
	active, ok := lm.active[dst]
	if !ok {
		return fmt.Errorf("%q must be an active layer", dst)
	}

	// move upperdir into the diff dir
	if err := os.Rename(active.upperdir, diff); err != nil {
		return err
	}

	// Clean up the working directory; we may not want to do this if we want to
	// support re-entrant calls to Commit.
	if err := os.RemoveAll(active.workdir); err != nil {
		return err
	}

	lm.parents[diff] = active.parent
	delete(lm.active, dst) // remove from active, again, consider not doing this to support multiple commits.
	// note that allowing multiple commits would require copy for overlay.

	return nil
}

// Rollback can be called after prepare if the caller would like to abandon the
// changeset.
func (lm *Manager) Rollback(dst string) error {
	active, ok := lm.active[dst]
	if !ok {
		return fmt.Errorf("%q must be an active layer", dst)
	}

	var err error
	err = os.RemoveAll(active.upperdir)
	err = os.RemoveAll(active.workdir)

	delete(lm.active, dst)
	return err
}

// Parent returns the parent of the layer at diff.
func (lm *Manager) Parent(diff string) string {
	return lm.parents[diff]
}

type ChangeKind int

const (
	ChangeKindAdd = iota
	ChangeKindModify
	ChangeKindDelete
)

func (k ChangeKind) String() string {
	switch k {
	case ChangeKindAdd:
		return "add"
	case ChangeKindModify:
		return "modify"
	case ChangeKindDelete:
		return "delete"
	default:
		return ""
	}
}

// Change represents single change between a diff and its parent.
//
// TODO(stevvooe): There are some cool tricks we can do with this type. If we
// provide the path to the resource from both the diff and its parent, for
// example, we can have the differ actually decide the granularity represented
// in the final changeset.
type Change struct {
	Kind ChangeKind
	Path string
}

// TODO(stevvooe): Make this change emit through a Walk-like interface. We can
// see this patten used in several tar'ing methods in pkg/archive.

// Changes returns the list of changes from the diff's parent.
func (lm *Manager) Changes(diff string) ([]Change, error) {
	panic("not implemented")
}