// Package credentialseal implements a versioned AES-256-GCM keyring that seals
// provider secrets into opaque envelopes and opens them again.
//
// It is a lower-layer cryptographic primitive. It depends only on the
// data-only credentialstore.SecretEnvelope value and never imports the
// credentialops service package, so no import cycle or layer inversion is
// introduced: credentialops -> credentialseal -> credentialstore.
//
// Key material is supplied out of band by a deployment secret manager that
// mounts a strict YAML manifest as a `0600` file (optionally through a
// secret-manager atomic symlink). The manifest, key bytes, and decrypted
// plaintext are never written to logs, errors, config, or the database.
package credentialseal

import (
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"syscall"

	"gopkg.in/yaml.v3"

	"iop/apps/control-plane/internal/credentialstore"
)

// AlgorithmAES256GCM is the only authenticated-encryption algorithm this
// package produces. It is recorded verbatim in every sealed envelope.
const AlgorithmAES256GCM = "AES-256-GCM"

// aeadKeyLength is the decoded key length required for AES-256 (32 bytes).
const aeadKeyLength = 32

// aadDomain is a fixed domain-separation prefix mixed into every AAD so a
// blob sealed by this package can never be confused with any other AEAD use.
const aadDomain = "iop.credentialseal.v1"

var (
	// ErrSealFailed is returned when sealing cannot complete. It is deliberately
	// constant so no plaintext, key metadata, or context leaks to the caller.
	ErrSealFailed = errors.New("credentialseal: seal failed")

	// ErrOpenFailed is returned when opening cannot complete, including unknown
	// key revision, wrong context, or any ciphertext/nonce/AAD tamper. It is
	// constant so authentication failures reveal nothing.
	ErrOpenFailed = errors.New("credentialseal: open failed")

	// ErrConfigIncomplete is returned when the encryption config supplies some
	// but not all of key_file, active_key_id, active_key_version.
	ErrConfigIncomplete = errors.New("credentialseal: incomplete credential encryption config")

	// ErrKeyFileAccess is returned when the key file cannot be resolved, opened,
	// or read. It never includes the file contents.
	ErrKeyFileAccess = errors.New("credentialseal: key file is not accessible")

	// ErrKeyFileInsecure is returned when the resolved key file is not a regular
	// file, is not owned by the current user or root, or is group/world
	// accessible.
	ErrKeyFileInsecure = errors.New("credentialseal: key file has insecure ownership or permissions")

	// ErrManifest is returned when the key manifest is malformed, empty, has a
	// duplicate id/version, or a non-positive version.
	ErrManifest = errors.New("credentialseal: invalid key manifest")

	// ErrKeyMaterial is returned when a key's material does not decode to exactly
	// 32 bytes. It never includes the decoded bytes.
	ErrKeyMaterial = errors.New("credentialseal: invalid key material")

	// ErrUnknownActiveKey is returned when the configured active key id/version
	// is absent from the manifest.
	ErrUnknownActiveKey = errors.New("credentialseal: active key not present in manifest")
)

// Context binds ciphertext to its intended resource. It must never carry raw
// secret material; it holds only the principal, slot, and credential kind.
type Context struct {
	PrincipalID string
	SlotID      string
	Kind        string
}

// Sealer seals plaintext into an opaque authenticated envelope bound to a
// Context. The raw plaintext must never leave the boundary.
type Sealer interface {
	Seal(ctx context.Context, plaintext []byte, sealCtx Context) (credentialstore.SecretEnvelope, error)
}

// FileConfig selects the external key manifest and the active sealing key. All
// three fields are omitted to disable at-rest encryption; any present field
// requires the full triple.
type FileConfig struct {
	KeyFile          string `yaml:"key_file"`
	ActiveKeyID      string `yaml:"active_key_id"`
	ActiveKeyVersion uint64 `yaml:"active_key_version"`
}

type keyRef struct {
	id      string
	version uint64
}

// Keyring is a versioned AES-256-GCM keyring. It seals with the configured
// active key and can open any revision it holds. It is safe for concurrent use
// because every cipher.AEAD is immutable after construction.
type Keyring struct {
	aeads     map[keyRef]cipher.AEAD
	active    keyRef
	randomSrc io.Reader
}

var (
	_ Sealer                              = (*Keyring)(nil)
	_ credentialstore.EnvelopeKeyRegistry = (*Keyring)(nil)
)

// manifest is the strict on-disk key manifest schema.
type manifest struct {
	Keys []manifestKey `yaml:"keys"`
}

type manifestKey struct {
	ID       string `yaml:"id"`
	Version  uint64 `yaml:"version"`
	Material string `yaml:"material"`
}

// LoadFile builds a Keyring from an operator-supplied FileConfig.
//
// When key_file, active_key_id, and active_key_version are all omitted it
// returns (nil, nil) so the caller can run with at-rest encryption disabled and
// every provider-secret mutation fails closed. Any present field requires the
// complete triple; a partial config returns ErrConfigIncomplete.
func LoadFile(cfg FileConfig) (*Keyring, error) {
	keyFile := strings.TrimSpace(cfg.KeyFile)
	activeID := strings.TrimSpace(cfg.ActiveKeyID)
	activeVersion := cfg.ActiveKeyVersion

	if keyFile == "" && activeID == "" && activeVersion == 0 {
		return nil, nil
	}
	if keyFile == "" || activeID == "" || activeVersion == 0 {
		return nil, ErrConfigIncomplete
	}

	raw, err := readSecureManifestFile(keyFile)
	if err != nil {
		return nil, err
	}
	defer zero(raw)

	return newKeyring(raw, keyRef{id: activeID, version: activeVersion}, nil)
}

// newKeyring parses a manifest byte slice and assembles the AEAD map. It is
// separated from LoadFile so tests can supply an alternate random source.
func newKeyring(rawManifest []byte, active keyRef, randomSrc io.Reader) (*Keyring, error) {
	var m manifest
	dec := yaml.NewDecoder(bytes.NewReader(rawManifest))
	dec.KnownFields(true)
	if err := dec.Decode(&m); err != nil {
		return nil, fmt.Errorf("%w: parse", ErrManifest)
	}
	if len(m.Keys) == 0 {
		return nil, fmt.Errorf("%w: no keys", ErrManifest)
	}

	aeads := make(map[keyRef]cipher.AEAD, len(m.Keys))
	for _, mk := range m.Keys {
		id := strings.TrimSpace(mk.ID)
		if id == "" {
			return nil, fmt.Errorf("%w: empty key id", ErrManifest)
		}
		if mk.Version == 0 {
			return nil, fmt.Errorf("%w: non-positive key version", ErrManifest)
		}
		ref := keyRef{id: id, version: mk.Version}
		if _, dup := aeads[ref]; dup {
			return nil, fmt.Errorf("%w: duplicate key id/version", ErrManifest)
		}
		aead, err := newAEAD(mk.Material)
		if err != nil {
			return nil, err
		}
		aeads[ref] = aead
	}

	if _, ok := aeads[active]; !ok {
		return nil, ErrUnknownActiveKey
	}

	src := randomSrc
	if src == nil {
		src = rand.Reader
	}
	return &Keyring{aeads: aeads, active: active, randomSrc: src}, nil
}

// newAEAD decodes base64 key material, verifies its length, and builds an
// AES-256-GCM AEAD. The decoded material is zeroed once the cipher has copied
// it into its own key schedule.
func newAEAD(material string) (cipher.AEAD, error) {
	decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(material))
	if err != nil {
		return nil, ErrKeyMaterial
	}
	defer zero(decoded)
	if len(decoded) != aeadKeyLength {
		return nil, ErrKeyMaterial
	}
	block, err := aes.NewCipher(decoded)
	if err != nil {
		return nil, ErrKeyMaterial
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return nil, ErrKeyMaterial
	}
	return aead, nil
}

// Seal encrypts plaintext with the active key and returns an opaque envelope.
// The nonce is freshly random, the AAD is canonically bound to sealCtx, and all
// returned buffers are fresh copies.
func (k *Keyring) Seal(_ context.Context, plaintext []byte, sealCtx Context) (credentialstore.SecretEnvelope, error) {
	if k == nil {
		return credentialstore.SecretEnvelope{}, ErrSealFailed
	}
	aead, ok := k.aeads[k.active]
	if !ok {
		return credentialstore.SecretEnvelope{}, ErrSealFailed
	}
	nonce := make([]byte, aead.NonceSize())
	if _, err := io.ReadFull(k.randomSrc, nonce); err != nil {
		return credentialstore.SecretEnvelope{}, ErrSealFailed
	}
	aad := canonicalAAD(sealCtx)
	ciphertext := aead.Seal(nil, nonce, plaintext, aad)

	return credentialstore.SecretEnvelope{
		Algorithm:  AlgorithmAES256GCM,
		KeyID:      k.active.id,
		KeyVersion: k.active.version,
		Nonce:      nonce,
		Ciphertext: ciphertext,
		AAD:        aad,
	}, nil
}

// Open decrypts an envelope for the exact sealCtx it was sealed under. It
// rejects an unknown key revision, a mismatched context, or any tampered
// nonce/ciphertext/AAD, always with the constant ErrOpenFailed.
func (k *Keyring) Open(_ context.Context, env credentialstore.SecretEnvelope, sealCtx Context) ([]byte, error) {
	if k == nil {
		return nil, ErrOpenFailed
	}
	if env.Algorithm != AlgorithmAES256GCM {
		return nil, ErrOpenFailed
	}
	aead, ok := k.aeads[keyRef{id: env.KeyID, version: env.KeyVersion}]
	if !ok {
		return nil, ErrOpenFailed
	}
	if len(env.Nonce) != aead.NonceSize() {
		return nil, ErrOpenFailed
	}
	expectedAAD := canonicalAAD(sealCtx)
	if !bytes.Equal(env.AAD, expectedAAD) {
		return nil, ErrOpenFailed
	}
	plaintext, err := aead.Open(nil, env.Nonce, env.Ciphertext, expectedAAD)
	if err != nil {
		return nil, ErrOpenFailed
	}
	return plaintext, nil
}

// HasEnvelopeKey reports whether the keyring holds the given key revision. It
// lets a Keyring double as a credentialstore.EnvelopeKeyRegistry.
func (k *Keyring) HasEnvelopeKey(_ context.Context, keyID string, keyVersion uint64) (bool, error) {
	if k == nil {
		return false, nil
	}
	_, ok := k.aeads[keyRef{id: keyID, version: keyVersion}]
	return ok, nil
}

// canonicalAAD builds a deterministic, length-delimited AAD over the domain
// prefix and the principal/slot/kind context so no two distinct contexts can
// ever collide (e.g. "ab"+"c" differs from "a"+"bc").
func canonicalAAD(c Context) []byte {
	var b bytes.Buffer
	writeField(&b, aadDomain)
	writeField(&b, c.PrincipalID)
	writeField(&b, c.SlotID)
	writeField(&b, c.Kind)
	return b.Bytes()
}

func writeField(b *bytes.Buffer, s string) {
	var lenBuf [8]byte
	binary.BigEndian.PutUint64(lenBuf[:], uint64(len(s)))
	b.Write(lenBuf[:])
	b.WriteString(s)
}

// readSecureManifestFile resolves any secret-manager atomic symlink once, then
// validates and reads the resolved target through the same file descriptor it
// stat'd, so the material cannot be swapped underneath the check.
func readSecureManifestFile(path string) ([]byte, error) {
	resolved, err := filepath.EvalSymlinks(path)
	if err != nil {
		return nil, fmt.Errorf("%w: resolve", ErrKeyFileAccess)
	}
	f, err := os.Open(resolved)
	if err != nil {
		return nil, fmt.Errorf("%w: open", ErrKeyFileAccess)
	}
	defer f.Close()

	info, err := f.Stat()
	if err != nil {
		return nil, fmt.Errorf("%w: stat", ErrKeyFileAccess)
	}
	if !info.Mode().IsRegular() {
		return nil, fmt.Errorf("%w: not a regular file", ErrKeyFileInsecure)
	}
	if info.Mode().Perm()&0o077 != 0 {
		return nil, fmt.Errorf("%w: group or world accessible", ErrKeyFileInsecure)
	}
	if err := verifyOwner(info); err != nil {
		return nil, err
	}

	data, err := io.ReadAll(f)
	if err != nil {
		return nil, fmt.Errorf("%w: read", ErrKeyFileAccess)
	}
	return data, nil
}

// verifyOwner requires the file to be owned by the current user or root.
func verifyOwner(info os.FileInfo) error {
	st, ok := info.Sys().(*syscall.Stat_t)
	if !ok {
		return fmt.Errorf("%w: owner unverifiable", ErrKeyFileInsecure)
	}
	if st.Uid != uint32(os.Getuid()) && st.Uid != 0 {
		return fmt.Errorf("%w: unexpected owner", ErrKeyFileInsecure)
	}
	return nil
}

// zero best-effort wipes a byte buffer of sensitive material.
func zero(b []byte) {
	for i := range b {
		b[i] = 0
	}
}
