// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later

package licensing

import (
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
)

const (
	keyFileName    = "licensing_ed25519.key"
	downloadTTL    = 5 * time.Minute
	tokenSeparator = "|"
)

var (
	privateKey ed25519.PrivateKey
	publicKey  ed25519.PublicKey
	keyOnce    sync.Once
)

// initKeys loads or generates the ed25519 keypair used for signing download tokens.
func initKeys() {
	keyOnce.Do(func() {
		keyPath := filepath.Join(setting.AppDataPath, keyFileName)

		data, err := os.ReadFile(keyPath)
		if err == nil && len(data) == ed25519.SeedSize {
			privateKey = ed25519.NewKeyFromSeed(data)
			publicKey = privateKey.Public().(ed25519.PublicKey)
			log.Info("Licensing: loaded ed25519 key from %s", keyPath)
			return
		}

		// Generate new keypair
		seed := make([]byte, ed25519.SeedSize)
		if _, err := rand.Read(seed); err != nil {
			log.Error("Licensing: failed to generate ed25519 seed: %v", err)
			return
		}
		privateKey = ed25519.NewKeyFromSeed(seed)
		publicKey = privateKey.Public().(ed25519.PublicKey)

		if err := os.WriteFile(keyPath, seed, 0600); err != nil {
			log.Error("Licensing: failed to save ed25519 key to %s: %v", keyPath, err)
		} else {
			log.Info("Licensing: generated new ed25519 key at %s", keyPath)
		}
	})
}

// SignDownloadToken creates a signed, time-limited download token.
// The message format is: product|version|dlid|expires
func SignDownloadToken(product, version, dlid string) (token string, expires int64) {
	initKeys()
	if privateKey == nil {
		return "", 0
	}

	expires = time.Now().Add(downloadTTL).Unix()
	message := fmt.Sprintf("%s%s%s%s%s%s%d",
		product, tokenSeparator,
		version, tokenSeparator,
		dlid, tokenSeparator,
		expires)

	sig := ed25519.Sign(privateKey, []byte(message))
	token = base64.RawURLEncoding.EncodeToString(sig)
	return token, expires
}

// VerifyDownloadToken validates a signed download token.
// Returns the parsed product, version, dlid, and any error.
func VerifyDownloadToken(token string, product, version, dlid string, expires int64) bool {
	initKeys()
	if publicKey == nil {
		return false
	}

	// Check expiry
	if time.Now().Unix() > expires {
		return false
	}

	// Reconstruct message
	message := fmt.Sprintf("%s%s%s%s%s%s%d",
		product, tokenSeparator,
		version, tokenSeparator,
		dlid, tokenSeparator,
		expires)

	sig, err := base64.RawURLEncoding.DecodeString(token)
	if err != nil {
		return false
	}

	return ed25519.Verify(publicKey, []byte(message), sig)
}

// ParseDownloadParams extracts product and version from the URL path segment.
// Expects format: "{version}.zip" with product as a separate path param.
func ParseDownloadParams(versionFile string) (version string, ok bool) {
	if !strings.HasSuffix(versionFile, ".zip") {
		return "", false
	}
	version = strings.TrimSuffix(versionFile, ".zip")
	if version == "" {
		return "", false
	}
	return version, true
}

// ParseExpires converts the expires query parameter to int64.
func ParseExpires(s string) (int64, bool) {
	v, err := strconv.ParseInt(s, 10, 64)
	if err != nil {
		return 0, false
	}
	return v, true
}
