package tar

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

func Extract(input io.Reader, destDir string) error {
	var err error

	des, err := os.ReadDir(destDir)
	if err != nil {
		return fmt.Errorf("cannot read dest dir: %w", err)
	}
	for _, de := range des {
		err = os.RemoveAll(filepath.Join(destDir, de.Name()))
		if err != nil {
			return fmt.Errorf("cannot purge dest dir: %w", err)
		}
	}

	r := tar.NewReader(input)

	var first = true
	for {
		var hdr *tar.Header
		hdr, err = r.Next()
		if err != nil {
			if errors.Is(err, io.EOF) {
				if first {
					// mimic tar output on empty input
					return fmt.Errorf("does not look like a tar")
				}
				return nil
			}
			return err
		}
		first = false

		name := hdr.Name
		linkname := hdr.Linkname
		if strings.Contains(name, "..") {
			return fmt.Errorf("name contains '..': %s", name)
		}
		if path.IsAbs(linkname) {
			return fmt.Errorf("absolute symlink: %s->%s", name, linkname)
		}
		if strings.HasPrefix(path.Clean(path.Join(path.Dir(name), linkname)), "..") {
			return fmt.Errorf("link target escapes: %s->%s", name, linkname)
		}

		var destPath, rel string
		destPath = filepath.Join(destDir, filepath.FromSlash(name))
		rel, err = filepath.Rel(destDir, destPath)
		if err != nil {
			return fmt.Errorf("cannot get relative path: %w", err)
		}
		if strings.HasPrefix(rel, "..") {
			return fmt.Errorf("name escapes")
		}

		// ensure parent
		err = os.MkdirAll(filepath.Dir(destPath), os.FileMode(hdr.Mode)&fs.ModePerm|0111)
		if err != nil {
			return fmt.Errorf("cannot ensure parent: %w", err)
		}

		switch hdr.Typeflag {
		case tar.TypeReg:
			err = writeRegularFile(destPath, os.FileMode(hdr.Mode&0777), r)
		case tar.TypeDir:
			err = os.MkdirAll(destPath, os.FileMode(hdr.Mode)&fs.ModePerm)
		case tar.TypeSymlink:
			err = os.Symlink(linkname, destPath)
		default:
			_, _ = fmt.Printf("unsupported type flag: %d\n", hdr.Typeflag)
		}
		if err != nil {
			return fmt.Errorf("cannot create entry: %w", err)
		}
	}
}

func writeRegularFile(target string, perm os.FileMode, content io.Reader) error {
	f, err := os.OpenFile(target, os.O_CREATE|os.O_EXCL|os.O_WRONLY, perm)
	if err != nil {
		return err
	}
	defer func(f *os.File) {
		_ = f.Close()
	}(f)
	_, err = io.Copy(f, content)
	if err != nil {
		return err
	}
	return nil
}
