package main

import (
	"crypto/ed25519"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"time"
)

type fileEntry struct {
	Name   string `json:"name"`
	SHA256 string `json:"sha256"`
	Size   int64  `json:"size"`
}

type manifestPayload struct {
	Product   string      `json:"product"`
	Version   string      `json:"version"`
	OS        string      `json:"os"`
	Arch      string      `json:"arch"`
	CreatedAt string      `json:"createdAt"`
	KeyID     string      `json:"keyId"`
	Files     []fileEntry `json:"files"`
}

type manifest struct {
	manifestPayload
	Signature string `json:"signature"`
}

func main() {
	var product, version, targetOS, arch, apiPath, workerPath, outputPath, keyID string
	var development, printLicensePublicKey, printUpdatePublicKey bool
	flag.StringVar(&product, "product", "qycw", "product code")
	flag.StringVar(&version, "version", "", "release version")
	flag.StringVar(&targetOS, "os", "", "target operating system")
	flag.StringVar(&arch, "arch", "", "target architecture")
	flag.StringVar(&apiPath, "api", "", "API binary path")
	flag.StringVar(&workerPath, "worker", "", "worker binary path")
	flag.StringVar(&outputPath, "output", "", "manifest output path")
	flag.StringVar(&keyID, "key-id", "", "update signing key ID")
	flag.BoolVar(&development, "development", false, "use deterministic development-only keys")
	flag.BoolVar(&printLicensePublicKey, "print-license-public-key", false, "print the development license public key")
	flag.BoolVar(&printUpdatePublicKey, "print-update-public-key", false, "print the development update public key")
	flag.Parse()

	if printLicensePublicKey {
		fmt.Println(base64.RawURLEncoding.EncodeToString(developmentKey("auth-center-development-only-ed25519-key").Public().(ed25519.PublicKey)))
		return
	}
	if printUpdatePublicKey {
		fmt.Println(base64.RawURLEncoding.EncodeToString(developmentKey("qycw-update-development-only-ed25519-key").Public().(ed25519.PublicKey)))
		return
	}
	if version == "" || targetOS == "" || arch == "" || apiPath == "" || workerPath == "" || outputPath == "" {
		fail("version, os, arch, api, worker, and output are required")
	}

	privateKey, resolvedKeyID, err := signingKey(development, keyID)
	if err != nil {
		fail(err.Error())
	}
	api, err := digestFile(apiPath)
	if err != nil {
		fail(err.Error())
	}
	worker, err := digestFile(workerPath)
	if err != nil {
		fail(err.Error())
	}
	payload := manifestPayload{
		Product:   product,
		Version:   version,
		OS:        targetOS,
		Arch:      arch,
		CreatedAt: time.Now().UTC().Format(time.RFC3339),
		KeyID:     resolvedKeyID,
		Files:     []fileEntry{api, worker},
	}
	encoded, err := json.Marshal(payload)
	if err != nil {
		fail(fmt.Sprintf("encode manifest: %v", err))
	}
	output := manifest{
		manifestPayload: payload,
		Signature:       "UPD-" + base64.RawURLEncoding.EncodeToString(ed25519.Sign(privateKey, encoded)),
	}
	encoded, err = json.MarshalIndent(output, "", "  ")
	if err != nil {
		fail(fmt.Sprintf("encode signed manifest: %v", err))
	}
	if err := os.WriteFile(outputPath, append(encoded, '\n'), 0o644); err != nil {
		fail(fmt.Sprintf("write manifest: %v", err))
	}
}

func developmentKey(label string) ed25519.PrivateKey {
	seed := sha256.Sum256([]byte(label))
	return ed25519.NewKeyFromSeed(seed[:])
}

func signingKey(development bool, keyID string) (ed25519.PrivateKey, string, error) {
	if development {
		return developmentKey("qycw-update-development-only-ed25519-key"), "update-dev-2026-01", nil
	}
	encoded := os.Getenv("UPDATE_SIGNING_PRIVATE_KEY")
	if encoded == "" {
		return nil, "", fmt.Errorf("UPDATE_SIGNING_PRIVATE_KEY is required for production packaging")
	}
	if keyID == "" {
		keyID = os.Getenv("UPDATE_SIGNING_KEY_ID")
	}
	if keyID == "" {
		return nil, "", fmt.Errorf("UPDATE_SIGNING_KEY_ID is required for production packaging")
	}
	decoded, err := decodeKey(encoded)
	if err != nil {
		return nil, "", err
	}
	if len(decoded) == ed25519.SeedSize {
		return ed25519.NewKeyFromSeed(decoded), keyID, nil
	}
	if len(decoded) == ed25519.PrivateKeySize {
		return ed25519.PrivateKey(decoded), keyID, nil
	}
	return nil, "", fmt.Errorf("UPDATE_SIGNING_PRIVATE_KEY has invalid length %d", len(decoded))
}

func decodeKey(value string) ([]byte, error) {
	for _, encoding := range []*base64.Encoding{base64.RawURLEncoding, base64.URLEncoding, base64.RawStdEncoding, base64.StdEncoding} {
		if decoded, err := encoding.DecodeString(value); err == nil {
			return decoded, nil
		}
	}
	return nil, fmt.Errorf("UPDATE_SIGNING_PRIVATE_KEY is not valid base64")
}

func digestFile(path string) (fileEntry, error) {
	file, err := os.Open(path)
	if err != nil {
		return fileEntry{}, fmt.Errorf("open %s: %w", path, err)
	}
	defer file.Close()
	info, err := file.Stat()
	if err != nil {
		return fileEntry{}, fmt.Errorf("stat %s: %w", path, err)
	}
	hash := sha256.New()
	if _, err := io.Copy(hash, file); err != nil {
		return fileEntry{}, fmt.Errorf("hash %s: %w", path, err)
	}
	return fileEntry{Name: filepath.Base(path), SHA256: hex.EncodeToString(hash.Sum(nil)), Size: info.Size()}, nil
}

func fail(message string) {
	fmt.Fprintln(os.Stderr, "release-manifest:", message)
	os.Exit(1)
}
