// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
	"archive/tar"
	"compress/bzip2"
	"crypto/sha512"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"runtime"
	"strings"

	"gvisor.dev/gvisor/runsc/config"
	"gvisor.dev/gvisor/runsc/gvisorbinaries"
	"gvisor.dev/gvisor/runsc/version"
)

// releaseArches maps GOARCH to the architecture names used in release URLs.
var releaseArches = map[string]string{
	"amd64": "x86_64",
	"arm64": "aarch64",
}

func releaseTarballURL(ver, goarch string) (string, error) {
	if !config.IsReleaseVersion(ver) {
		return "", fmt.Errorf("cannot map version %q to a release download URL; please download sidecar binaries manually", ver)
	}
	arch, ok := releaseArches[goarch]
	if !ok {
		return "", fmt.Errorf("unknown architecture %q", goarch)
	}
	// The release date follows the first dash, whichever the prefix.
	date := ver[strings.IndexByte(ver, '-')+1:]
	return fmt.Sprintf("https://storage.googleapis.com/gvisor/releases/release/%s/%s/gvisor.tar.bz2", date, arch), nil
}

// fetch downloads url to dest using curl or wget, whichever exists.
// We do this because we cannot link net/http in `runsc`.
func fetch(url, dest string) error {
	const timeoutSec = 300
	var cmd *exec.Cmd
	if curl, err := exec.LookPath("curl"); err == nil {
		cmd = exec.Command(curl, "-fsSL", "--max-time", fmt.Sprintf("%d", timeoutSec), "-o", dest, url)
	} else if wget, err := exec.LookPath("wget"); err == nil {
		cmd = exec.Command(wget, "-q", "--timeout", fmt.Sprintf("%d", timeoutSec), "-O", dest, url)
	} else {
		return errors.New("cannot download sidecar binaries: neither curl nor wget found in $PATH")
	}
	if out, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("cannot download (%s): %w: %s", strings.Join(cmd.Args, " "), err, out)
	}
	return nil
}

// verifySHA512 checks a file against the sha512 checksum file.
func verifySHA512(file, sumFile string) error {
	sum, err := os.ReadFile(sumFile)
	if err != nil {
		return fmt.Errorf("cannot read checksum file: %w", err)
	}
	fields := strings.Fields(string(sum))
	if len(fields) == 0 {
		return fmt.Errorf("checksum file %q is empty", sumFile)
	}
	f, err := os.Open(file)
	if err != nil {
		return fmt.Errorf("cannot open tarball: %w", err)
	}
	defer f.Close()
	h := sha512.New()
	if _, err := io.Copy(h, f); err != nil {
		return fmt.Errorf("cannot hash tarball: %w", err)
	}
	if got, want := hex.EncodeToString(h.Sum(nil)), fields[0]; !strings.EqualFold(got, want) {
		return fmt.Errorf("SHA-512 mismatch for %q: got %s, expected %s", file, got, want)
	}
	return nil
}

// extractSidecars extracts `gvisor-bin/`.
func extractSidecars(tarball, dir string) error {
	parent := filepath.Dir(dir)
	tmpDir := filepath.Join(parent, ".gvisor-bin.tmp")
	oldDir := filepath.Join(parent, ".gvisor-bin.old")
	if err := os.RemoveAll(tmpDir); err != nil {
		return fmt.Errorf("cannot remove stale directory: %w", err)
	}
	if err := os.RemoveAll(oldDir); err != nil {
		return fmt.Errorf("cannot remove stale directory: %w", err)
	}
	if err := os.Mkdir(tmpDir, 0755); err != nil {
		return fmt.Errorf("cannot create extraction directory: %w", err)
	}
	defer os.RemoveAll(tmpDir)
	f, err := os.Open(tarball)
	if err != nil {
		return fmt.Errorf("cannot open tarball: %w", err)
	}
	defer f.Close()
	tr := tar.NewReader(bzip2.NewReader(f))
	extracted := 0
	for {
		hdr, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("reading %q: %w", tarball, err)
		}
		if hdr.Typeflag != tar.TypeReg {
			continue
		}
		name, ok := strings.CutPrefix(strings.TrimPrefix(path.Clean(hdr.Name), "/"), "gvisor-bin/")
		if !ok || name == "" || strings.Contains(name, "/") {
			continue
		}
		dst, err := os.OpenFile(filepath.Join(tmpDir, name), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0755)
		if err != nil {
			return fmt.Errorf("extracting %q: %w", hdr.Name, err)
		}
		_, err = io.Copy(dst, tr)
		if closeErr := dst.Close(); err == nil {
			err = closeErr
		}
		if err != nil {
			return fmt.Errorf("extracting %q: %w", hdr.Name, err)
		}
		extracted++
	}
	if extracted == 0 {
		return fmt.Errorf("%q contains no gvisor-bin/ members", tarball)
	}
	// Swap the new directory into place.
	if _, err := os.Stat(dir); err == nil {
		if err := os.Rename(dir, oldDir); err != nil {
			return fmt.Errorf("cannot move old sidecar directory aside: %w", err)
		}
	}
	if err := os.Rename(tmpDir, dir); err != nil {
		return fmt.Errorf("cannot move new sidecar directory into place: %w", err)
	}
	if err := os.RemoveAll(oldDir); err != nil {
		return fmt.Errorf("cannot remove old sidecar directory: %w", err)
	}
	return nil
}

// installSidecars ensures that all sidecar binaries are present.
func (i *Install) installSidecars() error {
	var missing []string
	for _, b := range gvisorbinaries.All {
		if _, err := b.Path(); err != nil {
			missing = append(missing, b.Name)
		}
	}
	if len(missing) == 0 {
		return nil
	}
	dir, err := gvisorbinaries.Dir()
	if err != nil {
		return err
	}
	if !i.DownloadSidecars.Applies() {
		return fmt.Errorf("sidecar binaries %v missing (expected under %q); not downloading them (--download-sidecars=%s, release build: %t)", missing, dir, i.DownloadSidecars, config.IsReleaseVersion(version.Version()))
	}
	url := i.SidecarURL
	if url == "" {
		if url, err = releaseTarballURL(version.Version(), runtime.GOARCH); err != nil {
			return fmt.Errorf("missing sidecar binaries %v (expected under %q): %v", missing, dir, err)
		}
	}
	// Download next to the final location to be on the same filesystem.
	parent := filepath.Dir(dir)
	stale, _ := filepath.Glob(filepath.Join(parent, ".runsc-sidecars.*"))
	for _, d := range stale {
		os.RemoveAll(d)
	}
	tmp, err := os.MkdirTemp(parent, ".runsc-sidecars.*")
	if err != nil {
		return fmt.Errorf("cannot create download directory: %w", err)
	}
	defer os.RemoveAll(tmp)
	tarball := filepath.Join(tmp, "gvisor.tar.bz2")
	if err := fetch(url, tarball); err != nil {
		return err
	}
	sumFile := tarball + ".sha512"
	if err := fetch(url+".sha512", sumFile); err != nil {
		return err
	}
	if err := verifySHA512(tarball, sumFile); err != nil {
		return err
	}
	if err := extractSidecars(tarball, dir); err != nil {
		return err
	}
	log.Printf("Installed sidecar binaries from %s to %q. This is BEST-EFFORT FUNCTIONALITY which will STOP WORKING in a few weeks. Please see gvisor.dev/issue/13718", url, dir)
	return nil
}
