package cacheitem

import (
	"archive/tar"
	"os"
	"path/filepath"
	"strings"

	"github.com/vercel/turbo/cli/internal/turbopath"
)

// restoreDirectory restores a directory.
func restoreDirectory(dirCache *cachedDirTree, anchor turbopath.AbsoluteSystemPath, header *tar.Header) (turbopath.AnchoredSystemPath, error) {
	processedName, err := canonicalizeName(header.Name)
	if err != nil {
		return "", err
	}

	// We need to traverse `processedName` from base to root split at
	// `os.Separator` to make sure we don't end up following a symlink
	// outside of the restore path.

	// Create the directory.
	if err := safeMkdirAll(dirCache, anchor, processedName, header.Mode); err != nil {
		return "", err
	}

	return processedName, nil
}

type cachedDirTree struct {
	anchorAtDepth []turbopath.AbsoluteSystemPath
	prefix        []turbopath.RelativeSystemPath
}

func (cr *cachedDirTree) getStartingPoint(path turbopath.AnchoredSystemPath) (turbopath.AbsoluteSystemPath, []turbopath.RelativeSystemPath) {
	pathSegmentStrings := strings.Split(path.ToString(), string(os.PathSeparator))
	pathSegments := make([]turbopath.RelativeSystemPath, len(pathSegmentStrings))
	for index, pathSegmentString := range pathSegmentStrings {
		pathSegments[index] = turbopath.RelativeSystemPathFromUpstream(pathSegmentString)
	}

	i := 0
	for i = 0; i < len(cr.prefix) && i < len(pathSegments); i++ {
		if pathSegments[i] != cr.prefix[i] {
			break
		}
	}

	// 0: root anchor, can't remove it.
	cr.anchorAtDepth = cr.anchorAtDepth[:i+1]

	// 0: first prefix.
	cr.prefix = cr.prefix[:i]

	return cr.anchorAtDepth[i], pathSegments[i:]
}

func (cr *cachedDirTree) Update(anchor turbopath.AbsoluteSystemPath, newSegment turbopath.RelativeSystemPath) {
	cr.anchorAtDepth = append(cr.anchorAtDepth, anchor)
	cr.prefix = append(cr.prefix, newSegment)
}

// safeMkdirAll creates all directories, assuming that the leaf node is a directory.
// FIXME: Recheck the symlink cache before creating a directory.
func safeMkdirAll(dirCache *cachedDirTree, anchor turbopath.AbsoluteSystemPath, processedName turbopath.AnchoredSystemPath, mode int64) error {
	// Iterate through path segments by os.Separator, appending them onto the anchor.
	// Check to see if that path segment is a symlink with a target outside of anchor.

	// Pull the iteration starting point from thie directory cache.
	calculatedAnchor, pathSegments := dirCache.getStartingPoint(processedName)
	for _, segment := range pathSegments {
		calculatedAnchor, checkPathErr := checkPath(anchor, calculatedAnchor, segment)
		// We hit an existing directory or absolute path that was invalid.
		if checkPathErr != nil {
			return checkPathErr
		}

		// Otherwise we continue and check the next segment.
		dirCache.Update(calculatedAnchor, segment)
	}

	// If we have made it here we know that it is safe to call os.MkdirAll
	// on the Join of anchor and processedName.
	//
	// This could _still_ error, but we don't care.
	return processedName.RestoreAnchor(anchor).MkdirAll(os.FileMode(mode))
}

// checkPath ensures that the resolved path (if restoring symlinks).
// It makes sure to never traverse outside of the anchor.
func checkPath(originalAnchor turbopath.AbsoluteSystemPath, accumulatedAnchor turbopath.AbsoluteSystemPath, segment turbopath.RelativeSystemPath) (turbopath.AbsoluteSystemPath, error) {
	// Check if the segment itself is sneakily an absolute path...
	// (looking at you, Windows. CON, AUX...)
	if filepath.IsAbs(segment.ToString()) {
		return "", errTraversal
	}

	// Find out if this portion of the path is a symlink.
	combinedPath := accumulatedAnchor.Join(segment)
	fileInfo, err := combinedPath.Lstat()

	// Getting an error here means we failed to stat the path.
	// Assume that means we're safe and continue.
	if err != nil {
		return combinedPath, nil
	}

	// Find out if we have a symlink.
	isSymlink := fileInfo.Mode()&os.ModeSymlink != 0

	// If we don't have a symlink it's safe.
	if !isSymlink {
		return combinedPath, nil
	}

	// Check to see if the symlink targets outside of the originalAnchor.
	// We don't do eval symlinks because we could find ourself in a totally
	// different place.

	// 1. Get the target.
	linkTarget, readLinkErr := combinedPath.Readlink()
	if readLinkErr != nil {
		return "", readLinkErr
	}

	// 2. See if the target is absolute.
	if filepath.IsAbs(linkTarget) {
		absoluteLinkTarget := turbopath.AbsoluteSystemPathFromUpstream(linkTarget)
		if originalAnchor.HasPrefix(absoluteLinkTarget) {
			return absoluteLinkTarget, nil
		}
		return "", errTraversal
	}

	// 3. Target is relative (or absolute Windows on a Unix device)
	relativeLinkTarget := turbopath.RelativeSystemPathFromUpstream(linkTarget)
	computedTarget := accumulatedAnchor.UntypedJoin(linkTarget)
	if computedTarget.HasPrefix(originalAnchor) {
		// Need to recurse and make sure the target doesn't link out.
		return checkPath(originalAnchor, accumulatedAnchor, relativeLinkTarget)
	}
	return "", errTraversal
}
