// Package sign implements the Ed25519 signing envelope used for offline
// signing of endpoint and notice distribution documents (doc/06 §3 密码学口径).
//
// Trust model:
//   - The signing private key lives OFFLINE, in two physically separate
//     locations. It must never enter the server, CI, or this repository.
//   - The verifying public key is embedded in the client install package.
//   - key_id selects which public key verifies a document; a KeyRing may hold
//     more than one key so a new signing key can be rolled out before the old
//     one is retired (双公钥轮换过渡).
//
// Document shape (the on-disk JSON):
//
//	{
//	  "version":   <monotonic uint64>,   // anti-rollback: clients only accept larger
//	  "issued_at": "<RFC3339 UTC>",
//	  "key_id":    "<key identifier>",
//	  "payload":   { ... arbitrary JSON ... },
//	  "sig":       "<base64(ed25519 signature)>"
//	}
//
// The signature covers the canonical JSON encoding of the document WITHOUT the
// "sig" field, i.e. {version, issued_at, key_id, payload}. Because version,
// key_id and issued_at are all inside the signed bytes, an attacker cannot
// downgrade the version or swap the key without breaking the signature.
package sign

import (
	"bytes"
	"crypto/ed25519"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
)

// Errors returned by Verify.
var (
	ErrUnknownKeyID = errors.New("sign: unknown key_id (no matching public key in ring)")
	ErrBadSignature = errors.New("sign: signature verification failed")
	ErrMissingSig   = errors.New("sign: document has no signature")
	ErrEmptyKeyID   = errors.New("sign: key_id is empty")
	ErrBadPayload   = errors.New("sign: payload is not valid JSON")
)

// Envelope is the signed distribution document.
type Envelope struct {
	Version  uint64          `json:"version"`
	IssuedAt string          `json:"issued_at"`
	KeyID    string          `json:"key_id"`
	Payload  json.RawMessage `json:"payload"`
	Sig      string          `json:"sig,omitempty"`
}

// KeyRing maps key_id -> public key. Holding more than one entry enables a
// rotation window where documents signed by either key validate.
type KeyRing map[string]ed25519.PublicKey

// signingBytes returns the canonical bytes that are signed/verified: the
// envelope without its signature.
func (e Envelope) signingBytes() ([]byte, error) {
	if e.KeyID == "" {
		return nil, ErrEmptyKeyID
	}
	if !json.Valid(e.Payload) {
		return nil, ErrBadPayload
	}
	unsigned := Envelope{
		Version:  e.Version,
		IssuedAt: e.IssuedAt,
		KeyID:    e.KeyID,
		Payload:  e.Payload,
		// Sig intentionally empty -> omitted by omitempty.
	}
	raw, err := json.Marshal(unsigned)
	if err != nil {
		return nil, err
	}
	return Canonicalize(raw)
}

// Sign computes the Ed25519 signature over e's canonical bytes and stores it in
// e.Sig (base64). The private key is supplied by the caller (loaded from the
// offline key file) and is never persisted by this package.
func Sign(priv ed25519.PrivateKey, e *Envelope) error {
	if len(priv) != ed25519.PrivateKeySize {
		return fmt.Errorf("sign: invalid private key size %d", len(priv))
	}
	msg, err := e.signingBytes()
	if err != nil {
		return err
	}
	sig := ed25519.Sign(priv, msg)
	e.Sig = base64.StdEncoding.EncodeToString(sig)
	return nil
}

// Verify checks e's signature against the public key selected by e.KeyID from
// ring. It returns nil only if the key is known and the signature is valid.
func Verify(e Envelope, ring KeyRing) error {
	if e.Sig == "" {
		return ErrMissingSig
	}
	pub, ok := ring[e.KeyID]
	if !ok {
		return ErrUnknownKeyID
	}
	sig, err := base64.StdEncoding.DecodeString(e.Sig)
	if err != nil {
		return fmt.Errorf("sign: signature is not valid base64: %w", err)
	}
	msg, err := e.signingBytes()
	if err != nil {
		return err
	}
	if !ed25519.Verify(pub, msg, sig) {
		return ErrBadSignature
	}
	return nil
}

// Marshal renders the signed envelope as indented JSON suitable for publishing.
func Marshal(e Envelope) ([]byte, error) {
	if e.Sig == "" {
		return nil, ErrMissingSig
	}
	return json.MarshalIndent(e, "", "  ")
}

// Parse decodes a published document into an Envelope.
func Parse(raw []byte) (Envelope, error) {
	var e Envelope
	if err := json.Unmarshal(raw, &e); err != nil {
		return Envelope{}, fmt.Errorf("sign: cannot parse document: %w", err)
	}
	return e, nil
}

// Canonicalize returns a deterministic JSON encoding of raw: object keys sorted
// lexicographically, no insignificant whitespace, array order preserved. This
// guarantees signer and verifier hash identical bytes regardless of field order
// or formatting.
func Canonicalize(raw []byte) ([]byte, error) {
	dec := json.NewDecoder(bytes.NewReader(raw))
	dec.UseNumber() // keep integers exact; never widen to float64
	var v any
	if err := dec.Decode(&v); err != nil {
		return nil, fmt.Errorf("sign: canonicalize decode: %w", err)
	}
	var buf bytes.Buffer
	if err := writeCanonical(&buf, v); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

func writeCanonical(buf *bytes.Buffer, v any) error {
	switch t := v.(type) {
	case map[string]any:
		keys := make([]string, 0, len(t))
		for k := range t {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		buf.WriteByte('{')
		for i, k := range keys {
			if i > 0 {
				buf.WriteByte(',')
			}
			kb, err := json.Marshal(k)
			if err != nil {
				return err
			}
			buf.Write(kb)
			buf.WriteByte(':')
			if err := writeCanonical(buf, t[k]); err != nil {
				return err
			}
		}
		buf.WriteByte('}')
	case []any:
		buf.WriteByte('[')
		for i, e := range t {
			if i > 0 {
				buf.WriteByte(',')
			}
			if err := writeCanonical(buf, e); err != nil {
				return err
			}
		}
		buf.WriteByte(']')
	case string:
		b, err := json.Marshal(t)
		if err != nil {
			return err
		}
		buf.Write(b)
	case json.Number:
		buf.WriteString(t.String())
	case bool:
		if t {
			buf.WriteString("true")
		} else {
			buf.WriteString("false")
		}
	case nil:
		buf.WriteString("null")
	default:
		return fmt.Errorf("sign: unsupported JSON type %T in canonicalization", v)
	}
	return nil
}
