// Command endpoint-signer is the OFFLINE CLI that signs endpoint and notice
// distribution documents with an Ed25519 key (doc/06 §3).
//
// It is meant to run on an air-gapped machine. The private key never leaves that
// machine; only the produced *.v1.json (and the public key, embedded in the
// client) are distributed.
//
// Subcommands:
//
//	keygen   -out-prefix <p> [-key-id id]   generate an offline keypair
//	sign     -type endpoints|notices -in payload.json -key priv.key -key-id id
//	         -version N [-prev prev.json] [-issued-at RFC3339] [-out out.json]
//	verify   -in doc.json -keys "id=b64[,id2=b64]" [-type ...] [-min-version N]
//	inspect  -in doc.json
package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"time"

	"github.com/wangjia/pangolin/infra/domains/tools/internal/endpoint"
	"github.com/wangjia/pangolin/infra/domains/tools/internal/notice"
	"github.com/wangjia/pangolin/infra/domains/tools/internal/sign"
)

func main() {
	if len(os.Args) < 2 {
		usage()
		os.Exit(2)
	}
	var err error
	switch os.Args[1] {
	case "keygen":
		err = cmdKeygen(os.Args[2:])
	case "sign":
		err = cmdSign(os.Args[2:])
	case "verify":
		err = cmdVerify(os.Args[2:])
	case "inspect":
		err = cmdInspect(os.Args[2:])
	case "-h", "--help", "help":
		usage()
		return
	default:
		fmt.Fprintf(os.Stderr, "unknown subcommand %q\n", os.Args[1])
		usage()
		os.Exit(2)
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}

func usage() {
	fmt.Fprint(os.Stderr, `endpoint-signer — offline Ed25519 signer for endpoint/notice distribution

  keygen   -out-prefix <p> [-key-id id]
  sign     -type endpoints|notices -in payload.json -key priv.key -key-id id -version N [-prev prev.json] [-issued-at RFC3339] [-out out.json]
  verify   -in doc.json -keys "id=b64[,id2=b64]" [-type endpoints|notices] [-min-version N]
  inspect  -in doc.json
`)
}

func cmdKeygen(args []string) error {
	fs := newFlagSet("keygen")
	outPrefix := fs.String("out-prefix", "", "output prefix; writes <prefix>.ed25519.key (private, 0600) and <prefix>.ed25519.pub")
	keyID := fs.String("key-id", "", "suggested key_id label (informational)")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if *outPrefix == "" {
		return errors.New("-out-prefix is required")
	}
	pub, priv, err := sign.GenerateKey()
	if err != nil {
		return err
	}
	privPath := *outPrefix + ".ed25519.key"
	pubPath := *outPrefix + ".ed25519.pub"
	if err := os.WriteFile(privPath, []byte(sign.EncodePrivate(priv)+"\n"), 0o600); err != nil {
		return err
	}
	if err := os.WriteFile(pubPath, []byte(sign.EncodePublic(pub)+"\n"), 0o644); err != nil {
		return err
	}
	fmt.Printf("private key (0600, store OFFLINE in TWO locations): %s\n", privPath)
	fmt.Printf("public key (embed in client install package):       %s\n", pubPath)
	fmt.Printf("public key (base64): %s\n", sign.EncodePublic(pub))
	if *keyID != "" {
		fmt.Printf("key ring spec:       %s=%s\n", *keyID, sign.EncodePublic(pub))
	}
	return nil
}

func cmdSign(args []string) error {
	fs := newFlagSet("sign")
	docType := fs.String("type", "endpoints", "payload type: endpoints|notices")
	in := fs.String("in", "", "input payload JSON file")
	keyFile := fs.String("key", "", "offline private key file (base64)")
	keyID := fs.String("key-id", "", "key_id stamped into the document")
	version := fs.Uint64("version", 0, "monotonic version (0 = derive from -prev + 1)")
	prev := fs.String("prev", "", "previous signed document; version defaults to its version + 1")
	issuedAt := fs.String("issued-at", "", "RFC3339 UTC timestamp (default: now)")
	out := fs.String("out", "", "output file (default: stdout)")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if *in == "" || *keyFile == "" || *keyID == "" {
		return errors.New("-in, -key and -key-id are required")
	}

	keyBytes, err := os.ReadFile(*keyFile)
	if err != nil {
		return err
	}
	priv, err := sign.DecodePrivate(string(keyBytes))
	if err != nil {
		return err
	}

	ver := *version
	if ver == 0 {
		if *prev == "" {
			return errors.New("-version is required when -prev is not given")
		}
		prevRaw, err := os.ReadFile(*prev)
		if err != nil {
			return err
		}
		prevEnv, err := sign.Parse(prevRaw)
		if err != nil {
			return err
		}
		ver = prevEnv.Version + 1
	}

	ts := *issuedAt
	if ts == "" {
		ts = time.Now().UTC().Format(time.RFC3339)
	}

	payloadRaw, err := os.ReadFile(*in)
	if err != nil {
		return err
	}

	var env sign.Envelope
	switch *docType {
	case "endpoints":
		var p endpoint.Payload
		if err := json.Unmarshal(payloadRaw, &p); err != nil {
			return fmt.Errorf("parse endpoints payload: %w", err)
		}
		env, err = endpoint.Build(p, *keyID, ver, ts, priv)
		if err != nil {
			return err
		}
	case "notices":
		var l notice.List
		if err := json.Unmarshal(payloadRaw, &l); err != nil {
			return fmt.Errorf("parse notices payload: %w", err)
		}
		if err := l.Validate(); err != nil {
			return err
		}
		canon, err := json.Marshal(l)
		if err != nil {
			return err
		}
		env = sign.Envelope{Version: ver, IssuedAt: ts, KeyID: *keyID, Payload: canon}
		if err := sign.Sign(priv, &env); err != nil {
			return err
		}
	default:
		return fmt.Errorf("unknown -type %q", *docType)
	}

	rendered, err := sign.Marshal(env)
	if err != nil {
		return err
	}
	rendered = append(rendered, '\n')
	if *out == "" {
		_, err = os.Stdout.Write(rendered)
		return err
	}
	if err := os.WriteFile(*out, rendered, 0o644); err != nil {
		return err
	}
	fmt.Fprintf(os.Stderr, "signed %s document v%d -> %s\n", *docType, ver, *out)
	return nil
}

func cmdVerify(args []string) error {
	fs := newFlagSet("verify")
	in := fs.String("in", "", "signed document file")
	keys := fs.String("keys", "", "key ring: id=base64pub[,id2=base64pub] (multiple = rotation window)")
	docType := fs.String("type", "endpoints", "payload type for schema validation: endpoints|notices")
	minVersion := fs.Uint64("min-version", 0, "reject documents with version <= this (anti-rollback)")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if *in == "" || *keys == "" {
		return errors.New("-in and -keys are required")
	}
	ring, err := sign.ParseKeyRing([]string{*keys})
	if err != nil {
		return err
	}
	raw, err := os.ReadFile(*in)
	if err != nil {
		return err
	}

	switch *docType {
	case "endpoints":
		env, p, err := endpoint.VerifyDocument(raw, ring, *minVersion)
		if err != nil {
			return err
		}
		fmt.Printf("OK: endpoints v%d key_id=%s api_domains=%d mirror_urls=%d\n",
			env.Version, env.KeyID, len(p.APIDomains), len(p.MirrorURLs))
	case "notices":
		env, err := sign.Parse(raw)
		if err != nil {
			return err
		}
		if err := sign.Verify(env, ring); err != nil {
			return err
		}
		if env.Version <= *minVersion {
			return endpoint.ErrRollback
		}
		var l notice.List
		if err := json.Unmarshal(env.Payload, &l); err != nil {
			return err
		}
		if err := l.Validate(); err != nil {
			return err
		}
		fmt.Printf("OK: notices v%d key_id=%s notices=%d\n", env.Version, env.KeyID, len(l.Notices))
	default:
		return fmt.Errorf("unknown -type %q", *docType)
	}
	return nil
}

func cmdInspect(args []string) error {
	fs := newFlagSet("inspect")
	in := fs.String("in", "", "signed document file")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if *in == "" {
		return errors.New("-in is required")
	}
	raw, err := os.ReadFile(*in)
	if err != nil {
		return err
	}
	env, err := sign.Parse(raw)
	if err != nil {
		return err
	}
	fmt.Printf("version:   %d\n", env.Version)
	fmt.Printf("issued_at: %s\n", env.IssuedAt)
	fmt.Printf("key_id:    %s\n", env.KeyID)
	fmt.Printf("sig:       %s...\n", truncate(env.Sig, 16))
	fmt.Printf("payload:   %s\n", string(env.Payload))
	return nil
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n]
}
