// Sync binaries to packages directory from goreleaser's dist/ directory.
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"

	"github.com/go-playground/validator/v10"
)

type GoreleaserArtifact struct {
	Path   string `json:"path"   validate:"required"`
	Goos   string `json:"goos"   validate:"required"`
	Goarch string `json:"goarch"`
	Type   string `json:"type"   validate:"required"`
}

var goArchToNodeArchMap = map[string]string{
	"amd64": "x64",
	"386":   "x86",
	"arm64": "arm64",
}

var goOsToNodeOsMap = map[string]string{
	"windows": "win32",
}

func main() {
	artifactsPath := flag.String("artifacts-path", "dist/artifacts.json", "Path to goreleaser artifacts.json")
	packagesPath := flag.String("packages-path", "./packages", "Path to the npm packages directory")
	strict := flag.Bool("strict", true, "Fail if a package directory does not exist for a built artifact")
	setVersion := flag.String("set-version", "", "Semver x.y.z to write into all non-private package.json files under packages-path")
	verifyBins := flag.Bool("verify-bins", false, "Verify that each platform package has a non-empty bin/ directory after sync")
	flag.Parse()

	artifactsBytes, err := os.ReadFile(*artifactsPath)
	if err != nil {
		log.Fatalf("failed to read artifacts.json (did you run goreleaser build?): %v", err)
	}

	var artifacts []GoreleaserArtifact
	if err := json.Unmarshal(artifactsBytes, &artifacts); err != nil {
		log.Fatalf("failed to parse artifacts.json: %v", err)
	}

	validate := validator.New(validator.WithRequiredStructEnabled())

	for _, artifact := range artifacts {
		switch artifact.Type {
		case "Binary":
			if err := validate.Struct(artifact); err != nil {
				log.Printf("skipping invalid artifact: %v", err)
				continue
			}

			// goreleaser v2 emits a universal macOS binary as type "Binary"
			// with goarch "all" when universal_binaries.replace is true.
			// Copy it to both darwin platform packages.
			if artifact.Goos == "darwin" && artifact.Goarch == "all" {
				for _, nodeArch := range []string{"x64", "arm64"} {
					packagePath := filepath.Join(*packagesPath, fmt.Sprintf("pmg-darwin-%s", nodeArch))
					if err := copyToBin(artifact.Path, packagePath, "pmg", *strict); err != nil {
						log.Fatalf("sync darwin universal -> %s: %v", nodeArch, err)
					}
				}
				continue
			}

			if err := syncBinary(artifact, *packagesPath, *strict); err != nil {
				log.Fatalf("sync: %v", err)
			}

		case "Universal Binary":
			// Retained for compatibility with older goreleaser versions that
			// emitted a distinct type for universal binaries.
			if artifact.Goos != "darwin" {
				log.Printf("unexpected universal binary for goos=%s, skipping", artifact.Goos)
				continue
			}
			for _, nodeArch := range []string{"x64", "arm64"} {
				packagePath := filepath.Join(*packagesPath, fmt.Sprintf("pmg-darwin-%s", nodeArch))
				if err := copyToBin(artifact.Path, packagePath, "pmg", *strict); err != nil {
					log.Fatalf("sync darwin universal -> %s: %v", nodeArch, err)
				}
			}
		}
	}

	if *setVersion != "" {
		if err := setPackageVersions(*packagesPath, *setVersion); err != nil {
			log.Fatalf("set-version: %v", err)
		}
	}

	if *verifyBins {
		if err := verifyPackageBins(*packagesPath); err != nil {
			log.Fatalf("verify-bins: %v", err)
		}
	}
}

func syncBinary(artifact GoreleaserArtifact, packagesPath string, strict bool) error {
	nodeArch, ok := goArchToNodeArchMap[artifact.Goarch]
	if !ok {
		nodeArch = artifact.Goarch
	}
	nodeOs, ok := goOsToNodeOsMap[artifact.Goos]
	if !ok {
		nodeOs = artifact.Goos
	}

	packagePath := filepath.Join(packagesPath, fmt.Sprintf("pmg-%s-%s", nodeOs, nodeArch))
	binName := "pmg"
	if artifact.Goos == "windows" {
		binName = "pmg.exe"
	}
	return copyToBin(artifact.Path, packagePath, binName, strict)
}

func copyToBin(src, packagePath, binName string, strict bool) error {
	if _, err := os.Stat(packagePath); os.IsNotExist(err) {
		if strict {
			return fmt.Errorf("package directory %s does not exist (add the platform package or remove the goreleaser target)", packagePath)
		}
		log.Printf("package directory %s does not exist, skipping", packagePath)
		return nil
	}

	binDir := filepath.Join(packagePath, "bin")
	if err := os.MkdirAll(binDir, 0o755); err != nil { //nolint:gosec // bin/ needs execute permission
		return fmt.Errorf("create bin dir %s: %w", binDir, err)
	}

	dst := filepath.Join(binDir, binName)
	log.Printf("copying %s -> %s", src, dst)
	return copyFile(src, dst)
}

func copyFile(src, dst string) error {
	srcFile, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("open source: %w", err)
	}
	defer srcFile.Close() //nolint:errcheck // read-only; close error is negligible

	dstFile, err := os.Create(dst)
	if err != nil {
		return fmt.Errorf("create destination: %w", err)
	}
	defer dstFile.Close() //nolint:errcheck // Sync() is called explicitly before return; deferred close is best-effort

	if _, err := io.Copy(dstFile, srcFile); err != nil {
		return fmt.Errorf("copy: %w", err)
	}
	if err := dstFile.Sync(); err != nil {
		return fmt.Errorf("sync: %w", err)
	}

	srcInfo, err := os.Stat(src)
	if err != nil {
		return fmt.Errorf("stat source: %w", err)
	}
	return os.Chmod(dst, srcInfo.Mode())
}
