package services

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

// CalDAV password encryption — AES-256-GCM with random per-message nonce.
//
// Storage layout (bytea column password_encrypted):
//   [12-byte nonce][ciphertext + 16-byte GCM tag]
//
// The key is read once at server startup from CALDAV_ENCRYPTION_KEY (a
// 32-byte AES-256 key, base64-encoded). The server refuses to start if
// the env var is set but malformed; if the var is absent the CalDAV
// service is left disabled and all CalDAV endpoints return 501.
//
// Security posture (audit §1.3 fix):
//   - Plaintext passwords never enter the database
//   - Plaintext passwords never appear in API responses (json:"-" on the model)
//   - The frontend only writes; it can never read the password back
//   - Rotating the key requires re-encrypting all rows (out of scope for v1)

// ErrCalDAVNoKey is returned when an encrypt/decrypt is attempted with no
// CALDAV_ENCRYPTION_KEY configured. Handlers map this to 501.
var ErrCalDAVNoKey = errors.New("CALDAV_ENCRYPTION_KEY not configured")

// CalDAVCipher wraps an AES-GCM AEAD with the server's CalDAV key.
type CalDAVCipher struct {
	gcm cipher.AEAD
}

// LoadCalDAVCipher reads the key from CALDAV_ENCRYPTION_KEY and returns a
// ready-to-use cipher. Returns (nil, nil) when the env var is unset — the
// caller treats this as "CalDAV disabled".
func LoadCalDAVCipher() (*CalDAVCipher, error) {
	raw := os.Getenv("CALDAV_ENCRYPTION_KEY")
	if raw == "" {
		return nil, nil
	}
	key, err := base64.StdEncoding.DecodeString(raw)
	if err != nil {
		return nil, fmt.Errorf("CALDAV_ENCRYPTION_KEY must be base64: %w", err)
	}
	if len(key) != 32 {
		return nil, fmt.Errorf("CALDAV_ENCRYPTION_KEY must decode to 32 bytes (AES-256), got %d", len(key))
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("aes.NewCipher: %w", err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("cipher.NewGCM: %w", err)
	}
	return &CalDAVCipher{gcm: gcm}, nil
}

// Encrypt seals plaintext with a fresh random nonce and returns
// nonce || ciphertext || GCM-tag.
func (c *CalDAVCipher) Encrypt(plaintext string) ([]byte, error) {
	if c == nil {
		return nil, ErrCalDAVNoKey
	}
	nonce := make([]byte, c.gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, fmt.Errorf("nonce: %w", err)
	}
	ct := c.gcm.Seal(nil, nonce, []byte(plaintext), nil)
	out := make([]byte, 0, len(nonce)+len(ct))
	out = append(out, nonce...)
	out = append(out, ct...)
	return out, nil
}

// Decrypt reverses Encrypt. Returns an error on tampering / wrong key /
// short input.
func (c *CalDAVCipher) Decrypt(blob []byte) (string, error) {
	if c == nil {
		return "", ErrCalDAVNoKey
	}
	ns := c.gcm.NonceSize()
	if len(blob) < ns+c.gcm.Overhead() {
		return "", errors.New("ciphertext too short")
	}
	nonce := blob[:ns]
	ct := blob[ns:]
	pt, err := c.gcm.Open(nil, nonce, ct, nil)
	if err != nil {
		return "", fmt.Errorf("gcm.Open: %w", err)
	}
	return string(pt), nil
}
