// Package crypto provides AES-256-GCM encryption for sensitive app config values.
package crypto

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"log"
	"os"
	"strings"
)

const (
	keySize = 32 // AES-256
	prefix  = "ENC:"
)

// LoadOrCreateKey reads a 32-byte encryption key from path, or generates one if it doesn't exist.
func LoadOrCreateKey(path string) ([]byte, error) {
	data, err := os.ReadFile(path)
	if err == nil {
		if len(data) != keySize {
			return nil, fmt.Errorf("encryption key file %s has invalid size %d (expected %d)", path, len(data), keySize)
		}
		return data, nil
	}
	if !os.IsNotExist(err) {
		return nil, fmt.Errorf("reading encryption key %s: %w", path, err)
	}

	// Generate new key
	key := make([]byte, keySize)
	if _, err := rand.Read(key); err != nil {
		return nil, fmt.Errorf("generating encryption key: %w", err)
	}
	if err := os.WriteFile(path, key, 0600); err != nil {
		return nil, fmt.Errorf("writing encryption key %s: %w", path, err)
	}
	return key, nil
}

// Encrypt encrypts plaintext with AES-256-GCM and returns "ENC:<base64(nonce+ciphertext)>".
func Encrypt(key []byte, plaintext string) (string, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return "", err
	}

	ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) // nonce prepended
	return prefix + base64.StdEncoding.EncodeToString(ciphertext), nil
}

// Decrypt decrypts a value produced by Encrypt. Returns error if value is not encrypted or decryption fails.
func Decrypt(key []byte, value string) (string, error) {
	if !IsEncrypted(value) {
		return "", fmt.Errorf("value is not encrypted")
	}

	data, err := base64.StdEncoding.DecodeString(value[len(prefix):])
	if err != nil {
		return "", fmt.Errorf("base64 decode: %w", err)
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	nonceSize := gcm.NonceSize()
	if len(data) < nonceSize {
		return "", fmt.Errorf("ciphertext too short")
	}

	plaintext, err := gcm.Open(nil, data[:nonceSize], data[nonceSize:], nil)
	if err != nil {
		return "", fmt.Errorf("decrypt: %w", err)
	}
	return string(plaintext), nil
}

// IsEncrypted returns true if the value has the encryption prefix.
func IsEncrypted(value string) bool {
	return strings.HasPrefix(value, prefix)
}

// DecryptMap decrypts all encrypted values in a map, returning a new map with plaintext values.
// Logs a warning for any value that fails to decrypt (key rotation, data corruption).
func DecryptMap(key []byte, env map[string]string) map[string]string {
	if key == nil || env == nil {
		return env
	}
	result := make(map[string]string, len(env))
	for k, v := range env {
		if IsEncrypted(v) {
			dec, err := Decrypt(key, v)
			if err != nil {
				log.Printf("[WARN] Failed to decrypt env var %q: %v — passing through encrypted value", k, err)
				result[k] = v
				continue
			}
			result[k] = dec
			continue
		}
		result[k] = v
	}
	return result
}
