package builder

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

	"github.com/docker/docker/pkg/archive"
	"github.com/docker/docker/pkg/chrootarchive"
	"github.com/docker/docker/pkg/ioutils"
	"github.com/docker/docker/pkg/symlink"
	"github.com/docker/docker/pkg/tarsum"
)

type tarSumContext struct {
	root string
	sums tarsum.FileInfoSums
}

func (c *tarSumContext) Close() error {
	return os.RemoveAll(c.root)
}

func convertPathError(err error, cleanpath string) error {
	if err, ok := err.(*os.PathError); ok {
		err.Path = cleanpath
		return err
	}
	return err
}

func (c *tarSumContext) Open(path string) (io.ReadCloser, error) {
	cleanpath, fullpath, err := c.normalize(path)
	if err != nil {
		return nil, err
	}
	r, err := os.Open(fullpath)
	if err != nil {
		return nil, convertPathError(err, cleanpath)
	}
	return r, nil
}

func (c *tarSumContext) Stat(path string) (fi FileInfo, err error) {
	cleanpath, fullpath, err := c.normalize(path)
	if err != nil {
		return nil, err
	}

	st, err := os.Lstat(fullpath)
	if err != nil {
		return nil, convertPathError(err, cleanpath)
	}

	fi = PathFileInfo{st, fullpath}
	// we set sum to path by default for the case where GetFile returns nil.
	// The usual case is if cleanpath is empty.
	sum := path
	if tsInfo := c.sums.GetFile(cleanpath); tsInfo != nil {
		sum = tsInfo.Sum()
	}
	fi = &HashedFileInfo{fi, sum}
	return fi, nil
}

// MakeTarSumContext returns a build Context from a tar stream.
//
// It extracts the tar stream to a temporary folder that is deleted as soon as
// the Context is closed.
// As the extraction happens, a tarsum is calculated for every file, and the set of
// all those sums then becomes the source of truth for all operations on this Context.
//
// Closing tarStream has to be done by the caller.
func MakeTarSumContext(tarStream io.Reader) (ModifiableContext, error) {
	root, err := ioutils.TempDir("", "docker-builder")
	if err != nil {
		return nil, err
	}

	tsc := &tarSumContext{root: root}

	// Make sure we clean-up upon error.  In the happy case the caller
	// is expected to manage the clean-up
	defer func() {
		if err != nil {
			tsc.Close()
		}
	}()

	decompressedStream, err := archive.DecompressStream(tarStream)
	if err != nil {
		return nil, err
	}

	sum, err := tarsum.NewTarSum(decompressedStream, true, tarsum.Version1)
	if err != nil {
		return nil, err
	}

	if err := chrootarchive.Untar(sum, root, nil); err != nil {
		return nil, err
	}

	tsc.sums = sum.GetSums()

	return tsc, nil
}

func (c *tarSumContext) normalize(path string) (cleanpath, fullpath string, err error) {
	cleanpath = filepath.Clean(string(os.PathSeparator) + path)[1:]
	fullpath, err = symlink.FollowSymlinkInScope(filepath.Join(c.root, path), c.root)
	if err != nil {
		return "", "", fmt.Errorf("Forbidden path outside the build context: %s (%s)", path, fullpath)
	}
	_, err = os.Stat(fullpath)
	if err != nil {
		return "", "", convertPathError(err, path)
	}
	return
}

func (c *tarSumContext) Walk(root string, walkFn WalkFunc) error {
	for _, tsInfo := range c.sums {
		path := tsInfo.Name()
		path, fullpath, err := c.normalize(path)
		if err != nil {
			return err
		}

		// Any file in the context that starts with the given path will be
		// picked up and its hashcode used.  However, we'll exclude the
		// root dir itself.  We do this for a coupel of reasons:
		// 1 - ADD/COPY will not copy the dir itself, just its children
		//     so there's no reason to include it in the hash calc
		// 2 - the metadata on the dir will change when any child file
		//     changes.  This will lead to a miss in the cache check if that
		//     child file is in the .dockerignore list.
		if rel, err := filepath.Rel(root, path); err != nil {
			return err
		} else if rel == "." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
			continue
		}

		info, err := os.Lstat(fullpath)
		if err != nil {
			return convertPathError(err, path)
		}
		// TODO check context breakout?
		fi := &HashedFileInfo{PathFileInfo{info, fullpath}, tsInfo.Sum()}
		if err := walkFn(path, fi, nil); err != nil {
			return err
		}
	}
	return nil
}

func (c *tarSumContext) Remove(path string) error {
	_, fullpath, err := c.normalize(path)
	if err != nil {
		return err
	}
	return os.RemoveAll(fullpath)
}