package main

import (
	"archive/zip"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
)

const (
	wintunVersion = "0.14.1"
	wintunZipName = "wintun-" + wintunVersion + ".zip"
	wintunZipURL  = "https://www.wintun.net/builds/" + wintunZipName
	wintunSHA256  = "07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51"
)

func main() {
	if len(os.Args) != 4 {
		log.Fatalf("usage: %s arch stage-dir cache-dir", os.Args[0])
	}

	arch := os.Args[1]
	stageDir := os.Args[2]
	cacheDir := os.Args[3]

	zipPath, err := ensureWintunArchive(cacheDir)
	if err != nil {
		log.Fatalf("preparing %s: %v", wintunZipName, err)
	}
	if err := stageWintunAssets(zipPath, arch, stageDir); err != nil {
		log.Fatalf("staging wintun assets: %v", err)
	}
}

func ensureWintunArchive(cacheDir string) (string, error) {
	if err := os.MkdirAll(cacheDir, 0o755); err != nil {
		return "", fmt.Errorf("creating cache dir %q: %w", cacheDir, err)
	}

	zipPath := filepath.Join(cacheDir, wintunZipName)
	if err := verifySHA256(zipPath, wintunSHA256); err == nil {
		return zipPath, nil
	} else if !errors.Is(err, os.ErrNotExist) {
		if removeErr := os.Remove(zipPath); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
			return "", fmt.Errorf("removing invalid archive %q: %w", zipPath, removeErr)
		}
	}

	if err := downloadFile(wintunZipURL, zipPath); err != nil {
		return "", err
	}
	if err := verifySHA256(zipPath, wintunSHA256); err != nil {
		return "", err
	}
	return zipPath, nil
}

func wintunZipMemberForArch(arch string) (string, error) {
	switch arch {
	case "amd64":
		return "wintun/bin/amd64/wintun.dll", nil
	case "386":
		return "wintun/bin/x86/wintun.dll", nil
	case "arm64":
		return "wintun/bin/arm64/wintun.dll", nil
	case "arm":
		return "wintun/bin/arm/wintun.dll", nil
	default:
		return "", fmt.Errorf("unsupported arch %q", arch)
	}
}

func stageWintunAssets(zipPath, arch, stageDir string) error {
	dllMember, err := wintunZipMemberForArch(arch)
	if err != nil {
		return err
	}
	if err := os.MkdirAll(stageDir, 0o755); err != nil {
		return fmt.Errorf("creating stage dir %q: %w", stageDir, err)
	}

	archive, err := zip.OpenReader(zipPath)
	if err != nil {
		return fmt.Errorf("opening zip %q: %w", zipPath, err)
	}
	defer archive.Close()

	targets := map[string]string{
		dllMember:            filepath.Join(stageDir, "wintun.dll"),
		"wintun/LICENSE.txt": filepath.Join(stageDir, "LICENSE.wintun.txt"),
	}
	for member, dstPath := range targets {
		if err := extractZipMember(&archive.Reader, member, dstPath); err != nil {
			return err
		}
	}
	return nil
}

func extractZipMember(zr *zip.Reader, member, dstPath string) error {
	for _, file := range zr.File {
		if file.Name != member {
			continue
		}
		rc, err := file.Open()
		if err != nil {
			return fmt.Errorf("opening %q from zip: %w", member, err)
		}

		if err := writeFileAtomically(dstPath, rc, 0o644); err != nil {
			rc.Close()
			return fmt.Errorf("writing %q: %w", dstPath, err)
		}
		if err := rc.Close(); err != nil {
			return fmt.Errorf("closing %q from zip: %w", member, err)
		}
		return nil
	}
	return fmt.Errorf("zip archive missing %q", member)
}

func downloadFile(url, dstPath string) error {
	resp, err := http.Get(url)
	if err != nil {
		return fmt.Errorf("downloading %q: %w", url, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("downloading %q: unexpected status %s", url, resp.Status)
	}
	if err := writeFileAtomically(dstPath, resp.Body, 0o644); err != nil {
		return fmt.Errorf("writing %q: %w", dstPath, err)
	}
	return nil
}

func verifySHA256(path, want string) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()

	hasher := sha256.New()
	if _, err := io.Copy(hasher, f); err != nil {
		return fmt.Errorf("hashing %q: %w", path, err)
	}

	got := hex.EncodeToString(hasher.Sum(nil))
	if got != want {
		return fmt.Errorf("sha256 mismatch for %q: got %s want %s", path, got, want)
	}
	return nil
}

func writeFileAtomically(dstPath string, r io.Reader, mode os.FileMode) error {
	if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
		return err
	}

	tmp, err := os.CreateTemp(filepath.Dir(dstPath), filepath.Base(dstPath)+".tmp-*")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()
	defer func() {
		_ = os.Remove(tmpPath)
	}()

	if _, err := io.Copy(tmp, r); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Chmod(mode); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	return os.Rename(tmpPath, dstPath)
}
