package updater

import (
	"archive/tar"
	"compress/gzip"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
)

// expectedReleaseEntries is the canonical set of files a release
// tarball must contain. Anything missing OR anything extra is
// rejected — banger update should not unpack arbitrary files into
// the staging dir.
var expectedReleaseEntries = []string{
	"banger",
	"bangerd",
	"banger-vsock-agent",
}

// StagedRelease describes the result of unpacking a release tarball
// into a staging directory.
type StagedRelease struct {
	BangerPath     string
	BangerdPath    string
	VsockAgentPath string
}

// StageTarball reads the gzipped tar at tarballPath and extracts the
// expected three banger binaries into stagingDir. Any extra entries,
// any path-traversal members, any non-regular-file members, and any
// missing required entry are rejected.
//
// The extracted binaries are mode 0o755 regardless of what the
// tarball claims — banger update is a privileged operation; we
// don't honour weird modes from the wire.
func StageTarball(tarballPath, stagingDir string) (StagedRelease, error) {
	if err := os.MkdirAll(stagingDir, 0o700); err != nil {
		return StagedRelease{}, err
	}
	f, err := os.Open(tarballPath)
	if err != nil {
		return StagedRelease{}, err
	}
	defer f.Close()
	gz, err := gzip.NewReader(f)
	if err != nil {
		return StagedRelease{}, fmt.Errorf("open gzip: %w", err)
	}
	defer gz.Close()

	expected := map[string]struct{}{}
	for _, name := range expectedReleaseEntries {
		expected[name] = struct{}{}
	}
	seen := map[string]string{}

	tr := tar.NewReader(gz)
	for {
		hdr, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return StagedRelease{}, fmt.Errorf("read tar: %w", err)
		}
		rel := filepath.Clean(hdr.Name)
		if rel == "." || rel == string(filepath.Separator) {
			continue
		}
		if filepath.IsAbs(rel) || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
			return StagedRelease{}, fmt.Errorf("unsafe path in tarball: %q", hdr.Name)
		}
		if _, ok := expected[rel]; !ok {
			return StagedRelease{}, fmt.Errorf("unexpected entry in release tarball: %q (allowed: %v)", hdr.Name, expectedReleaseEntries)
		}
		if hdr.Typeflag != tar.TypeReg {
			return StagedRelease{}, fmt.Errorf("entry %q is not a regular file (typeflag %d)", hdr.Name, hdr.Typeflag)
		}
		dst := filepath.Join(stagingDir, rel)
		out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
		if err != nil {
			return StagedRelease{}, err
		}
		if _, err := io.Copy(out, tr); err != nil {
			_ = out.Close()
			return StagedRelease{}, err
		}
		if err := out.Close(); err != nil {
			return StagedRelease{}, err
		}
		seen[rel] = dst
	}

	for _, want := range expectedReleaseEntries {
		if _, ok := seen[want]; !ok {
			return StagedRelease{}, fmt.Errorf("release tarball is missing required entry %q", want)
		}
	}
	return StagedRelease{
		BangerPath:     seen["banger"],
		BangerdPath:    seen["bangerd"],
		VsockAgentPath: seen["banger-vsock-agent"],
	}, nil
}
