// Command lkctl issues and inspects Vantage licences by hand.
//
//	lkctl keypair
//	lkctl issue --instance-id=<uuid> --instance-name="Acme" --tier=professional --term=1y
//	lkctl inspect <blob-or-file>
//
// issue reads the signing key from LICENSE_SIGNING_KEY.
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"strings"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/shared/license"
	"github.com/google/uuid"
	"github.com/hyperboloide/lk"
)

func main() {
	if len(os.Args) < 2 {
		usage()
	}
	switch os.Args[1] {
	case "keypair":
		keypair()
	case "issue":
		issue(os.Args[2:])
	case "inspect":
		inspect(os.Args[2:])
	default:
		usage()
	}
}

func usage() {
	fmt.Fprintln(os.Stderr, "usage: lkctl keypair | issue | inspect")
	os.Exit(2)
}

func keypair() {
	priv, err := lk.NewPrivateKey()
	if err != nil {
		fatal("generate key: %v", err)
	}
	privStr, err := priv.ToB32String()
	if err != nil {
		fatal("encode private key: %v", err)
	}
	// PublicKey.ToB32String returns one value, unlike its private counterpart.
	pubStr := priv.GetPublicKey().ToB32String()

	fmt.Println("PRIVATE KEY (store in a password manager and in the admin service's")
	fmt.Println("LICENSE_SIGNING_KEY; back it up in two places, it cannot be recovered):")
	fmt.Println()
	fmt.Println(privStr)
	fmt.Println()
	fmt.Println("PUBLIC KEY (paste into trustedPublicKeys in shared/license/keys.go):")
	fmt.Println()
	fmt.Println(pubStr)
}

func issue(args []string) {
	fs := flag.NewFlagSet("issue", flag.ExitOnError)
	instanceID := fs.String("instance-id", "", "instance UUID the licence is bound to (required)")
	instanceName := fs.String("instance-name", "", "display name")
	accountID := fs.String("account-id", "", "admin-side account id, optional")
	tier := fs.String("tier", "", "free | professional | enterprise (required)")
	deployment := fs.String("deployment", "cloud", "cloud | self_hosted")
	term := fs.String("term", "1y", "1m or 1y")
	expires := fs.String("expires", "", "explicit RFC3339 expiry, overrides --term")
	out := fs.String("out", "", "write the blob to this file instead of stdout")
	fs.Parse(args)

	if *instanceID == "" || *tier == "" {
		fatal("--instance-id and --tier are required")
	}

	plan, ok := license.PlanFor(*deployment, *tier)
	if !ok {
		fatal("no plan for deployment %q tier %q", *deployment, *tier)
	}

	key := os.Getenv("LICENSE_SIGNING_KEY")
	if key == "" {
		fatal("LICENSE_SIGNING_KEY is not set")
	}

	now := time.Now().UTC()
	var exp time.Time
	switch {
	case *expires != "":
		t, err := time.Parse(time.RFC3339, *expires)
		if err != nil {
			fatal("parse --expires: %v", err)
		}
		exp = t.UTC()
	case *term == "1m":
		exp = now.AddDate(0, 1, 0)
	case *term == "1y":
		exp = now.AddDate(1, 0, 0)
	default:
		fatal("--term must be 1m or 1y")
	}

	// Self Hosted is sold annually only, so the window in which a cancelled
	// licence keeps working is bounded at a year.
	if plan.Deployment == license.DeploymentSelfHosted && *term == "1m" && *expires == "" {
		fatal("self_hosted is annual only; use --term=1y or an explicit --expires")
	}

	name := *instanceName
	if name == "" {
		name = *instanceID
	}

	l := license.License{
		ID:           uuid.NewString(),
		InstanceID:   *instanceID,
		AccountID:    *accountID,
		InstanceName: name,
		Tier:         plan.Tier,
		Deployment:   plan.Deployment,
		SupportLevel: plan.SupportLevel,
		IssuedAt:     now,
		ExpiresAt:    exp,
		Limits:       plan.Limits,
		Features:     plan.Features,
	}

	blob, err := license.Sign(l, key)
	if err != nil {
		fatal("%v", err)
	}

	if *out != "" {
		if err := os.WriteFile(*out, []byte(blob+"\n"), 0o600); err != nil {
			fatal("write %s: %v", *out, err)
		}
		fmt.Fprintf(os.Stderr, "wrote %s (tier=%s deployment=%s expires=%s)\n",
			*out, l.Tier, l.Deployment, l.ExpiresAt.Format(time.RFC3339))
		return
	}
	fmt.Println(blob)
}

func inspect(args []string) {
	if len(args) < 1 {
		fatal("usage: lkctl inspect <blob-or-file>")
	}
	blob := args[0]
	if b, err := os.ReadFile(blob); err == nil {
		blob = strings.TrimSpace(string(b))
	}

	l, err := license.Parse(blob)
	if err != nil {
		fatal("%v", err)
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	if err := enc.Encode(l); err != nil {
		fatal("%v", err)
	}

	if time.Now().After(l.ExpiresAt) {
		fmt.Fprintf(os.Stderr, "\nNOTE: expired %s\n", l.ExpiresAt.Format(time.RFC3339))
	}
}

func fatal(format string, args ...any) {
	fmt.Fprintf(os.Stderr, format+"\n", args...)
	os.Exit(1)
}
