// Package secrets implements orca's encrypted .env.secrets subsystem
// (REQ-080, v0.11 milestone, gate C-19).
//
// The model is master-key + per-namespace sub-keys + per-line AES-256-GCM:
//
//   - The master key is 32 random bytes generated at `orca init`, persisted
//     at ClusterDir()/master.key with mode 0600. LoadMasterKey refuses to
//     load a key whose permissions are looser than 0600.
//   - Per-namespace sub-keys are derived with HKDF-SHA256 using the
//     namespace name as the `info` parameter. This isolates namespaces
//     cryptographically without requiring operators to manage per-ns keys.
//   - Each .env.secrets line is encrypted independently with AES-256-GCM.
//     The nonce is 12 random bytes. The AAD is the 1-based line number
//     encoded as a big-endian uint32 — this binds each ciphertext to its
//     position, defeating line-swap / line-reorder attacks.
//   - The on-disk format is one base64(nonce||ciphertext||tag) blob per
//     line. There is no `KEY=` prefix; the entire line is the encrypted
//     blob. Plaintext lines use the standard `KEY=value` form.
//
// The package never logs plaintext or key material. slog calls carry
// only metadata (line counts, namespace name, error text).
package secrets

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"golang.org/x/crypto/hkdf"
)

// MasterKeyLen is the length in bytes of the master key (AES-256).
const MasterKeyLen = 32

// SubKeyLen is the length in bytes of a derived per-namespace sub-key.
const SubKeyLen = 32

// NonceLen is the length in bytes of the AES-GCM nonce.
const NonceLen = 12

// gcmTagLen is the length in bytes of the GCM authentication tag appended
// to the ciphertext by crypto/cipher's GCM Seal.
const gcmTagLen = 16

// MasterKeyMode is the required file mode for master.key. LoadMasterKey
// refuses anything looser.
const MasterKeyMode os.FileMode = 0o600

// GenerateMasterKey returns MasterKeyLen cryptographically random bytes
// from crypto/rand. Used by `orca init`.
func GenerateMasterKey() ([]byte, error) {
	key := make([]byte, MasterKeyLen)
	if _, err := io.ReadFull(rand.Reader, key); err != nil {
		return nil, fmt.Errorf("secrets: generate master key: %w", err)
	}
	return key, nil
}

// LoadMasterKey reads the master key from path and enforces the 0600
// permission requirement (REQ-080). A missing file is an error; a file
// with permissions looser than 0600 is refused to prevent accidental
// exposure via group/world-readable key files.
func LoadMasterKey(path string) ([]byte, error) {
	info, err := os.Stat(path)
	if err != nil {
		return nil, fmt.Errorf("secrets: stat master key %s: %w", path, err)
	}
	if info.Mode().Perm() != MasterKeyMode {
		return nil, fmt.Errorf("secrets: master key %s has mode %04o, want %04o (REFUSE)", path, info.Mode().Perm(), MasterKeyMode)
	}
	key, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("secrets: read master key %s: %w", path, err)
	}
	if len(key) != MasterKeyLen {
		return nil, fmt.Errorf("secrets: master key %s is %d bytes, want %d", path, len(key), MasterKeyLen)
	}
	return key, nil
}

// SaveMasterKey persists the master key to path with mode 0600 using an
// atomic write (temp file + rename). The parent directory is created if
// missing.
func SaveMasterKey(path string, key []byte) error {
	if len(key) != MasterKeyLen {
		return fmt.Errorf("secrets: master key is %d bytes, want %d", len(key), MasterKeyLen)
	}
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("secrets: mkdir %s: %w", dir, err)
	}
	tmp, err := os.CreateTemp(dir, ".master-key-*")
	if err != nil {
		return fmt.Errorf("secrets: create temp: %w", err)
	}
	tmpName := tmp.Name()
	defer func() { _ = os.Remove(tmpName) }()
	if _, err := tmp.Write(key); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("secrets: write master key: %w", err)
	}
	if err := tmp.Chmod(MasterKeyMode); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("secrets: chmod master key: %w", err)
	}
	if err := tmp.Sync(); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("secrets: sync master key: %w", err)
	}
	if err := tmp.Close(); err != nil {
		return fmt.Errorf("secrets: close master key: %w", err)
	}
	if err := os.Rename(tmpName, path); err != nil {
		return fmt.Errorf("secrets: rename master key: %w", err)
	}
	return nil
}

// DeriveNamespaceKey derives a per-namespace sub-key from the master key
// using HKDF-SHA256 with the namespace name as the `info` parameter. The
// same (master, namespace) pair always yields the same sub-key; a
// different namespace yields a different sub-key. The salt is empty
// (the master key is already high-entropy).
func DeriveNamespaceKey(masterKey []byte, namespace string) ([]byte, error) {
	if len(masterKey) != MasterKeyLen {
		return nil, fmt.Errorf("secrets: derive: master key is %d bytes, want %d", len(masterKey), MasterKeyLen)
	}
	if namespace == "" {
		return nil, errors.New("secrets: derive: namespace is empty")
	}
	out := make([]byte, SubKeyLen)
	r := hkdf.New(sha256.New, masterKey, nil, []byte(namespace))
	if _, err := io.ReadFull(r, out); err != nil {
		return nil, fmt.Errorf("secrets: hkdf: %w", err)
	}
	return out, nil
}

// gcmFor returns an AES-256-GCM AEAD keyed by nsKey.
func gcmFor(nsKey []byte) (cipher.AEAD, error) {
	block, err := aes.NewCipher(nsKey)
	if err != nil {
		return nil, fmt.Errorf("secrets: aes new: %w", err)
	}
	g, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("secrets: gcm new: %w", err)
	}
	return g, nil
}

// lineAAD returns the AAD for a line number: a 4-byte big-endian uint32.
// A non-positive line number yields an empty AAD (callers that do not
// care about position-binding pass 0).
func lineAAD(lineNumber int) []byte {
	if lineNumber <= 0 {
		return nil
	}
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[:], uint32(lineNumber))
	return buf[:]
}

// EncryptLine encrypts a single plaintext line with AES-256-GCM using
// nsKey, binding the ciphertext to lineNumber via AAD (defeats line-swap
// attacks). The returned string is base64(nonce || ciphertext || tag).
func EncryptLine(nsKey []byte, lineNumber int, plaintext []byte) (string, error) {
	g, err := gcmFor(nsKey)
	if err != nil {
		return "", err
	}
	nonce := make([]byte, NonceLen)
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return "", fmt.Errorf("secrets: nonce: %w", err)
	}
	ct := g.Seal(nil, nonce, plaintext, lineAAD(lineNumber))
	blob := make([]byte, 0, len(nonce)+len(ct))
	blob = append(blob, nonce...)
	blob = append(blob, ct...)
	return base64.StdEncoding.EncodeToString(blob), nil
}

// DecryptLine decrypts a single base64(nonce||ciphertext||tag) line
// produced by EncryptLine. lineNumber MUST match the value used at
// encryption time or the GCM tag check fails.
func DecryptLine(nsKey []byte, lineNumber int, encoded string) ([]byte, error) {
	g, err := gcmFor(nsKey)
	if err != nil {
		return nil, err
	}
	blob, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded))
	if err != nil {
		return nil, fmt.Errorf("secrets: base64 decode: %w", err)
	}
	if len(blob) < NonceLen+gcmTagLen {
		return nil, fmt.Errorf("secrets: ciphertext too short: %d bytes", len(blob))
	}
	nonce := blob[:NonceLen]
	ct := blob[NonceLen:]
	pt, err := g.Open(nil, nonce, ct, lineAAD(lineNumber))
	if err != nil {
		return nil, fmt.Errorf("secrets: gcm open: %w", err)
	}
	return pt, nil
}

// EncryptEnvFile encrypts each plaintext line independently with a fresh
// nonce. Line numbers are 1-based. The returned string is one base64 blob
// per line (newline-separated). Blank lines are preserved as empty lines
// so a re-decrypt round-trips to the same line count.
func EncryptEnvFile(nsKey []byte, plaintextLines []string) (string, error) {
	var b strings.Builder
	for i, line := range plaintextLines {
		if line == "" {
			b.WriteString("\n")
			continue
		}
		enc, err := EncryptLine(nsKey, i+1, []byte(line))
		if err != nil {
			return "", fmt.Errorf("secrets: encrypt line %d: %w", i+1, err)
		}
		b.WriteString(enc)
		b.WriteString("\n")
	}
	return b.String(), nil
}

// DecryptEnvFile decrypts the content produced by EncryptEnvFile. Each
// non-empty line is decrypted at its 1-based position; empty lines are
// preserved as empty strings in the output slice.
func DecryptEnvFile(nsKey []byte, encryptedContent string) ([]string, error) {
	encryptedContent = strings.TrimRight(encryptedContent, "\n")
	if encryptedContent == "" {
		return nil, nil
	}
	rawLines := strings.Split(encryptedContent, "\n")
	out := make([]string, 0, len(rawLines))
	for i, line := range rawLines {
		if line == "" {
			out = append(out, "")
			continue
		}
		pt, err := DecryptLine(nsKey, i+1, line)
		if err != nil {
			return nil, fmt.Errorf("secrets: decrypt line %d: %w", i+1, err)
		}
		out = append(out, string(pt))
	}
	return out, nil
}

// GenerateLoadCredentialFile decrypts the encrypted lines and returns a
// map of KEY->value for use with systemd LoadCredential=. The plaintext
// lines are expected to be in `KEY=value` form; lines that do not match
// are skipped.
func GenerateLoadCredentialFile(nsKey []byte, encryptedLines []string) (map[string]string, error) {
	out := make(map[string]string)
	for i, line := range encryptedLines {
		if line == "" {
			continue
		}
		pt, err := DecryptLine(nsKey, i+1, line)
		if err != nil {
			return nil, fmt.Errorf("secrets: loadcredential decrypt line %d: %w", i+1, err)
		}
		s := string(pt)
		idx := strings.IndexByte(s, '=')
		if idx <= 0 {
			continue
		}
		out[s[:idx]] = s[idx+1:]
	}
	return out, nil
}

// hmacSHA256 is a tiny helper retained for parity with manual HKDF
// fallback implementations; the package uses golang.org/x/crypto/hkdf
// directly, so this is unused in production but keeps the import set
// stable if a future refactor drops the x/crypto dependency.
func hmacSHA256(key, msg []byte) []byte {
	h := hmac.New(sha256.New, key)
	h.Write(msg)
	return h.Sum(nil)
}

var _ = hmacSHA256
