package mynalog

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
)

// KeyLength is the AES-256 key size the stored refresh token is sealed with.
const KeyLength = 32

// ErrUndecryptable is returned when a stored secret cannot be opened with the configured key —
// typically because the key was rotated or lost. Callers treat it as "there is no session" and ask
// the operator to sign in again, which is a nuisance rather than a failure.
var ErrUndecryptable = errors.New("mynalog: stored secret cannot be decrypted")

// ParseKey decodes the configured encryption key. It accepts standard base64 with or without
// padding (so the output of `openssl rand -base64 32` works as-is) and plain hex.
func ParseKey(s string) ([]byte, error) {
	for _, decode := range []func(string) ([]byte, error){
		base64.StdEncoding.DecodeString,
		base64.RawStdEncoding.DecodeString,
		hex.DecodeString,
	} {
		if key, err := decode(s); err == nil && len(key) == KeyLength {
			return key, nil
		}
	}
	return nil, fmt.Errorf("mynalog: encryption key must decode to %d bytes (base64 or hex)", KeyLength)
}

// Seal encrypts plaintext with key, returning the nonce followed by the AES-GCM ciphertext.
//
// This exists because the refresh token is a long-lived credential to the operator's personal tax
// cabinet, and it is the one part of the rail that has to sit on disk. Encrypting it at the
// application layer keeps it out of database dumps, which travel further than the database does.
func Seal(key []byte, plaintext string) ([]byte, error) {
	gcm, err := newGCM(key)
	if err != nil {
		return nil, err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return nil, fmt.Errorf("mynalog: nonce: %w", err)
	}
	return gcm.Seal(nonce, nonce, []byte(plaintext), nil), nil
}

// Open decrypts a blob produced by Seal. Any failure — a wrong key, a truncated value, a tampered
// one — comes back as ErrUndecryptable, because the caller's response is the same in every case.
func Open(key, blob []byte) (string, error) {
	gcm, err := newGCM(key)
	if err != nil {
		return "", err
	}
	if len(blob) < gcm.NonceSize() {
		return "", ErrUndecryptable
	}
	nonce, ciphertext := blob[:gcm.NonceSize()], blob[gcm.NonceSize():]
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return "", ErrUndecryptable
	}
	return string(plaintext), nil
}

// newGCM builds the AEAD both directions share.
func newGCM(key []byte) (cipher.AEAD, error) {
	if len(key) != KeyLength {
		return nil, fmt.Errorf("mynalog: encryption key must be %d bytes, got %d", KeyLength, len(key))
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("mynalog: cipher: %w", err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("mynalog: gcm: %w", err)
	}
	return gcm, nil
}
