// Package credentiallease implements the bounded, signed, recipient-sealed
// credential lease used between the Control Plane, Edge, and Node.
package credentiallease

import (
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/ecdh"
	"crypto/ed25519"
	"crypto/hkdf"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/binary"
	"errors"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

const (
	Version               = 1
	MinTTL                = 5 * time.Second
	MaxTTL                = 300 * time.Second
	MinSetSize            = 1
	MaxSetSize            = 4096
	MaxCanonicalFieldSize = 4096
	MaxCiphertextSize     = 64 << 10

	canonicalDomain = "iop.credential-lease.scope.v1"
	sealDomain      = "iop.credential-lease.seal.v1"
	signDomain      = "iop.credential-lease.signature.v1"
)

var (
	ErrInvalid       = errors.New("credentiallease: invalid lease")
	ErrExpired       = errors.New("credentiallease: expired lease")
	ErrScopeMismatch = errors.New("credentiallease: scope mismatch")
	ErrReplay        = errors.New("credentiallease: replay rejected")
	ErrOverflow      = errors.New("credentiallease: live set full")
	ErrKey           = errors.New("credentiallease: key unavailable")
)

// Scope binds a lease to one exact authenticated route, selected Node, target,
// projection generation, and credential revision. HeaderName and Scheme are
// signed profile-auth instructions and never carry secret bytes.
type Scope struct {
	LeaseID              string
	PrincipalRef         string
	CredentialSlotRef    string
	RouteID              string
	ProfileID            string
	UpstreamTarget       string
	NodeID               string
	RecipientKeyID       string
	HeaderName           string
	Scheme               string
	CredentialRevision   uint64
	RouteRevision        uint64
	ProjectionGeneration uint64
	IssuedAtUnixNano     int64
	ExpiresAtUnixNano    int64
}

// Envelope contains no plaintext provider credential. Ciphertext is sealed to
// one Node X25519 key and the complete value is signed by the Control Plane's
// dedicated Ed25519 issuer key.
type Envelope struct {
	Version            uint32
	IssuerKeyID        string
	Scope              Scope
	EphemeralPublicKey []byte
	Nonce              []byte
	Ciphertext         []byte
	Signature          []byte
}

// ExpectedScope is supplied independently by the Node request boundary. Zero
// revision/generation fields are still exact values, not wildcards.
type ExpectedScope struct {
	PrincipalRef         string
	CredentialSlotRef    string
	RouteID              string
	ProfileID            string
	UpstreamTarget       string
	NodeID               string
	RecipientKeyID       string
	CredentialRevision   uint64
	RouteRevision        uint64
	ProjectionGeneration uint64
}

// Material is request-local plaintext returned only after atomic validation
// and replay consumption. The caller must call Zero on every terminal path.
type Material struct {
	HeaderName string
	Scheme     string
	Secret     []byte
}

func (m *Material) Zero() {
	if m == nil {
		return
	}
	zero(m.Secret)
	m.Secret = nil
}

// Issue seals secret to recipientPublic and signs the complete envelope.
func Issue(scope Scope, secret, recipientPublic []byte, issuerKeyID string, issuerPrivate ed25519.PrivateKey, random io.Reader) (*Envelope, error) {
	if err := validateScope(scope, time.Time{}); err != nil {
		return nil, err
	}
	if len(secret) == 0 || len(secret) > MaxCiphertextSize || strings.TrimSpace(issuerKeyID) == "" || len(issuerPrivate) != ed25519.PrivateKeySize {
		return nil, ErrInvalid
	}
	curve := ecdh.X25519()
	recipient, err := curve.NewPublicKey(recipientPublic)
	if err != nil {
		return nil, ErrKey
	}
	if random == nil {
		random = rand.Reader
	}
	ephemeral, err := curve.GenerateKey(random)
	if err != nil {
		return nil, ErrKey
	}
	shared, err := ephemeral.ECDH(recipient)
	if err != nil {
		return nil, ErrKey
	}
	defer zero(shared)
	canonical := canonicalScope(scope)
	defer zero(canonical)
	key, err := hkdf.Key(sha256.New, shared, canonical, sealDomain, 32)
	if err != nil {
		return nil, ErrKey
	}
	defer zero(key)
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, ErrKey
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return nil, ErrKey
	}
	nonce := make([]byte, aead.NonceSize())
	if _, err := io.ReadFull(random, nonce); err != nil {
		return nil, ErrKey
	}
	env := &Envelope{
		Version:            Version,
		IssuerKeyID:        strings.TrimSpace(issuerKeyID),
		Scope:              scope,
		EphemeralPublicKey: append([]byte(nil), ephemeral.PublicKey().Bytes()...),
		Nonce:              nonce,
	}
	env.Ciphertext = aead.Seal(nil, env.Nonce, secret, canonical)
	env.Signature = ed25519.Sign(issuerPrivate, canonicalEnvelope(env, false))
	return env, nil
}

// Consumer validates, atomically consumes, and decrypts a lease. Its replay
// fence prunes only expired IDs and fails closed when every entry is live.
type Consumer struct {
	mu               sync.Mutex
	nodeID           string
	recipientKeyID   string
	recipientPrivate *ecdh.PrivateKey
	issuerKeyID      string
	issuerPublic     ed25519.PublicKey
	maxReplay        int
	clock            func() time.Time
	replay           map[string]int64
}

func NewConsumer(nodeID, recipientKeyID string, recipientPrivate []byte, issuerKeyID string, issuerPublic ed25519.PublicKey, maxReplay int, clock func() time.Time) (*Consumer, error) {
	if maxReplay < MinSetSize || maxReplay > MaxSetSize || strings.TrimSpace(nodeID) == "" || strings.TrimSpace(recipientKeyID) == "" || strings.TrimSpace(issuerKeyID) == "" || len(issuerPublic) != ed25519.PublicKeySize {
		return nil, ErrInvalid
	}
	privateKey, err := ecdh.X25519().NewPrivateKey(recipientPrivate)
	if err != nil {
		return nil, ErrKey
	}
	if clock == nil {
		clock = time.Now
	}
	return &Consumer{
		nodeID: nodeID, recipientKeyID: recipientKeyID, recipientPrivate: privateKey,
		issuerKeyID: issuerKeyID, issuerPublic: append(ed25519.PublicKey(nil), issuerPublic...),
		maxReplay: maxReplay, clock: clock, replay: make(map[string]int64),
	}, nil
}

// RecipientPublicKey returns a fresh copy suitable for authenticated Node
// registration. Private key material never leaves the Consumer.
func (c *Consumer) RecipientPublicKey() []byte {
	if c == nil || c.recipientPrivate == nil {
		return nil
	}
	return append([]byte(nil), c.recipientPrivate.PublicKey().Bytes()...)
}

func (c *Consumer) RecipientKeyID() string {
	if c == nil {
		return ""
	}
	return c.recipientKeyID
}

func (c *Consumer) Consume(_ context.Context, env *Envelope, expected ExpectedScope) (*Material, error) {
	if c == nil || env == nil {
		return nil, ErrInvalid
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	now := c.clock().UTC()
	if err := validateEnvelope(env, now, c.issuerKeyID, c.issuerPublic); err != nil {
		return nil, err
	}
	if err := c.match(env.Scope, expected); err != nil {
		return nil, err
	}
	for id, expiry := range c.replay {
		if expiry <= now.UnixNano() {
			delete(c.replay, id)
		}
	}
	if _, exists := c.replay[env.Scope.LeaseID]; exists {
		return nil, ErrReplay
	}
	if len(c.replay) >= c.maxReplay {
		return nil, ErrOverflow
	}
	ephemeral, err := ecdh.X25519().NewPublicKey(env.EphemeralPublicKey)
	if err != nil {
		return nil, ErrInvalid
	}
	shared, err := c.recipientPrivate.ECDH(ephemeral)
	if err != nil {
		return nil, ErrInvalid
	}
	defer zero(shared)
	canonical := canonicalScope(env.Scope)
	defer zero(canonical)
	key, err := hkdf.Key(sha256.New, shared, canonical, sealDomain, 32)
	if err != nil {
		return nil, ErrInvalid
	}
	defer zero(key)
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, ErrInvalid
	}
	aead, err := cipher.NewGCM(block)
	if err != nil || len(env.Nonce) != aead.NonceSize() {
		return nil, ErrInvalid
	}
	plaintext, err := aead.Open(nil, env.Nonce, env.Ciphertext, canonical)
	if err != nil {
		return nil, ErrInvalid
	}
	c.replay[env.Scope.LeaseID] = env.Scope.ExpiresAtUnixNano
	return &Material{HeaderName: env.Scope.HeaderName, Scheme: env.Scope.Scheme, Secret: plaintext}, nil
}

func (c *Consumer) match(scope Scope, expected ExpectedScope) error {
	if scope.NodeID != c.nodeID || scope.RecipientKeyID != c.recipientKeyID ||
		scope.PrincipalRef != expected.PrincipalRef || scope.CredentialSlotRef != expected.CredentialSlotRef ||
		scope.RouteID != expected.RouteID || scope.ProfileID != expected.ProfileID ||
		scope.UpstreamTarget != expected.UpstreamTarget || scope.NodeID != expected.NodeID ||
		scope.RecipientKeyID != expected.RecipientKeyID || scope.CredentialRevision != expected.CredentialRevision ||
		scope.RouteRevision != expected.RouteRevision || scope.ProjectionGeneration != expected.ProjectionGeneration {
		return ErrScopeMismatch
	}
	return nil
}

func validateEnvelope(env *Envelope, now time.Time, issuerKeyID string, issuerPublic ed25519.PublicKey) error {
	if env.Version != Version || env.IssuerKeyID != issuerKeyID || len(env.EphemeralPublicKey) != 32 || len(env.Nonce) != 12 || len(env.Ciphertext) == 0 || len(env.Ciphertext) > MaxCiphertextSize+16 || len(env.Signature) != ed25519.SignatureSize {
		return ErrInvalid
	}
	if err := validateScope(env.Scope, now); err != nil {
		return err
	}
	if !ed25519.Verify(issuerPublic, canonicalEnvelope(env, false), env.Signature) {
		return ErrInvalid
	}
	return nil
}

func validateScope(scope Scope, now time.Time) error {
	for _, value := range []string{scope.LeaseID, scope.PrincipalRef, scope.CredentialSlotRef, scope.RouteID, scope.ProfileID, scope.UpstreamTarget, scope.NodeID, scope.RecipientKeyID, scope.HeaderName} {
		if strings.TrimSpace(value) == "" || len(value) > MaxCanonicalFieldSize {
			return ErrInvalid
		}
	}
	if http.CanonicalHeaderKey(scope.HeaderName) != scope.HeaderName || strings.ContainsAny(scope.HeaderName, "\r\n") || strings.ContainsAny(scope.Scheme, "\r\n") {
		return ErrInvalid
	}
	issued := time.Unix(0, scope.IssuedAtUnixNano)
	expires := time.Unix(0, scope.ExpiresAtUnixNano)
	ttl := expires.Sub(issued)
	if scope.IssuedAtUnixNano <= 0 || ttl < MinTTL || ttl > MaxTTL {
		return ErrInvalid
	}
	if !now.IsZero() && !now.Before(expires) {
		return ErrExpired
	}
	return nil
}

func canonicalScope(scope Scope) []byte {
	var b bytes.Buffer
	writeString(&b, canonicalDomain)
	writeString(&b, scope.LeaseID)
	writeString(&b, scope.PrincipalRef)
	writeString(&b, scope.CredentialSlotRef)
	writeString(&b, scope.RouteID)
	writeString(&b, scope.ProfileID)
	writeString(&b, scope.UpstreamTarget)
	writeString(&b, scope.NodeID)
	writeString(&b, scope.RecipientKeyID)
	writeString(&b, scope.HeaderName)
	writeString(&b, scope.Scheme)
	writeUint64(&b, scope.CredentialRevision)
	writeUint64(&b, scope.RouteRevision)
	writeUint64(&b, scope.ProjectionGeneration)
	writeUint64(&b, uint64(scope.IssuedAtUnixNano))
	writeUint64(&b, uint64(scope.ExpiresAtUnixNano))
	return b.Bytes()
}

func canonicalEnvelope(env *Envelope, includeSignature bool) []byte {
	var b bytes.Buffer
	writeString(&b, signDomain)
	writeUint64(&b, uint64(env.Version))
	writeString(&b, env.IssuerKeyID)
	writeBytes(&b, canonicalScope(env.Scope))
	writeBytes(&b, env.EphemeralPublicKey)
	writeBytes(&b, env.Nonce)
	writeBytes(&b, env.Ciphertext)
	if includeSignature {
		writeBytes(&b, env.Signature)
	}
	return b.Bytes()
}

func writeString(b *bytes.Buffer, value string) { writeBytes(b, []byte(value)) }
func writeBytes(b *bytes.Buffer, value []byte) {
	writeUint64(b, uint64(len(value)))
	b.Write(value)
}
func writeUint64(b *bytes.Buffer, value uint64) {
	var raw [8]byte
	binary.BigEndian.PutUint64(raw[:], value)
	b.Write(raw[:])
}

// LoadPrivateKeyFile loads a base64-encoded fixed-size private key from a
// strict regular file. It is used for X25519 and Ed25519 deployment keys.
func LoadPrivateKeyFile(path string, size int) ([]byte, error) { return loadKeyFile(path, size) }

// LoadPublicKeyFile loads a base64-encoded fixed-size public key. Public files
// need not be mode 0600, but must still be regular files.
func LoadPublicKeyFile(path string, size int) ([]byte, error) {
	return readKeyFile(path, size, false)
}

func loadKeyFile(path string, size int) ([]byte, error) {
	return readKeyFile(path, size, true)
}

func readKeyFile(path string, size int, private bool) ([]byte, error) {
	resolved, err := filepath.EvalSymlinks(path)
	if err != nil {
		return nil, ErrKey
	}
	file, err := os.Open(resolved)
	if err != nil {
		return nil, ErrKey
	}
	defer file.Close()
	info, err := file.Stat()
	if err != nil || !info.Mode().IsRegular() {
		return nil, ErrKey
	}
	if private && !privateKeyFileSecure(file, info) {
		return nil, ErrKey
	}
	raw, err := io.ReadAll(io.LimitReader(file, int64(size*4+129)))
	if err != nil {
		return nil, ErrKey
	}
	defer zero(raw)
	trimmed := bytes.TrimSpace(raw)
	decoded := make([]byte, base64.StdEncoding.DecodedLen(len(trimmed)))
	written, err := base64.StdEncoding.Decode(decoded, trimmed)
	if err != nil || written != size {
		zero(decoded)
		return nil, ErrKey
	}
	return decoded[:written], nil
}

func zero(value []byte) {
	for i := range value {
		value[i] = 0
	}
}
