// Package smtpoutconfig persists the outbound SMTP relay configuration in
// PostgreSQL. Exactly one record may exist (id=1). The SMTP password is
// encrypted with AES-256-GCM using the same scheme as internal/ldapconfig.
package smtpoutconfig

import (
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"io"
	"time"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

// SMTPOutConfig is the persisted outbound SMTP relay configuration.
type SMTPOutConfig struct {
	ID        int64     `json:"id"`
	Enabled   bool      `json:"enabled"`
	Host      string    `json:"host"`
	Port      int       `json:"port"`
	User      string    `json:"user"`
	Password  string    `json:"password"` // masked as "••••••" in GET responses
	TLS       bool      `json:"tls"`
	From      string    `json:"from"`
	UpdatedAt time.Time `json:"updated_at"`
	UpdatedBy string    `json:"updated_by"`
}

const createTableSQL = `
CREATE TABLE IF NOT EXISTS smtp_out_config (
    id         BIGSERIAL PRIMARY KEY,
    enabled    BOOLEAN NOT NULL DEFAULT false,
    host       TEXT NOT NULL DEFAULT '',
    port       INT  NOT NULL DEFAULT 587,
    usr        TEXT NOT NULL DEFAULT '',
    password   BYTEA,
    tls        BOOLEAN NOT NULL DEFAULT false,
    from_addr  TEXT NOT NULL DEFAULT '',
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_by TEXT NOT NULL DEFAULT ''
);
`

// Store manages SMTP-Out configuration persistence.
type Store struct {
	pool   *pgxpool.Pool
	encKey [32]byte
}

// New connects to PostgreSQL, creates the table if needed, and returns a Store.
// secret is the HKDF-derived aesKey (see main.go, SEC-08) — not the raw
// cfg.API.Secret — so this store shares the same key-derivation lineage as
// internal/ldapconfig and internal/imap/store.go. The "-smtpout" suffix keeps
// this store's encryption key domain-separated from those.
func New(dsn, secret string) (*Store, error) {
	ctx := context.Background()
	pool, err := pgxpool.New(ctx, dsn)
	if err != nil {
		return nil, fmt.Errorf("smtpoutconfig: connect: %w", err)
	}

	key := sha256.Sum256([]byte(secret + "-smtpout"))
	s := &Store{pool: pool, encKey: key}

	if _, err := pool.Exec(ctx, createTableSQL); err != nil {
		pool.Close()
		return nil, fmt.Errorf("smtpoutconfig: init schema: %w", err)
	}
	return s, nil
}

// Close releases the connection pool.
func (s *Store) Close() { s.pool.Close() }

// Get returns the SMTP-Out configuration with the password masked.
// Returns nil, nil when no configuration has been saved yet.
func (s *Store) Get(ctx context.Context) (*SMTPOutConfig, error) {
	cfg, err := s.query(ctx)
	if err != nil {
		return nil, err
	}
	if cfg == nil {
		return nil, nil
	}
	if cfg.Password != "" {
		cfg.Password = "••••••"
	}
	return cfg, nil
}

// GetWithPassword returns the configuration including the decrypted password.
func (s *Store) GetWithPassword(ctx context.Context) (*SMTPOutConfig, error) {
	return s.query(ctx)
}

// Save upserts the SMTP-Out configuration (always uses id=1).
// When password is empty the existing stored password is preserved.
func (s *Store) Save(ctx context.Context, cfg SMTPOutConfig, updatedBy string) error {
	var encPw []byte
	var err error

	if cfg.Password != "" {
		encPw, err = s.encrypt(cfg.Password)
		if err != nil {
			return fmt.Errorf("smtpoutconfig: encrypt password: %w", err)
		}
	} else {
		// Preserve existing password
		existing, qErr := s.query(ctx)
		if qErr == nil && existing != nil && existing.Password != "" {
			encPw, err = s.encrypt(existing.Password)
			if err != nil {
				return fmt.Errorf("smtpoutconfig: re-encrypt: %w", err)
			}
		}
	}

	_, err = s.pool.Exec(ctx, `
		INSERT INTO smtp_out_config (id, enabled, host, port, usr, password, tls, from_addr, updated_at, updated_by)
		VALUES (1, $1, $2, $3, $4, $5, $6, $7, NOW(), $8)
		ON CONFLICT (id) DO UPDATE SET
			enabled    = EXCLUDED.enabled,
			host       = EXCLUDED.host,
			port       = EXCLUDED.port,
			usr        = EXCLUDED.usr,
			password   = CASE WHEN EXCLUDED.password IS NULL THEN smtp_out_config.password ELSE EXCLUDED.password END,
			tls        = EXCLUDED.tls,
			from_addr  = EXCLUDED.from_addr,
			updated_at = NOW(),
			updated_by = EXCLUDED.updated_by
	`, cfg.Enabled, cfg.Host, cfg.Port, cfg.User, encPw, cfg.TLS, cfg.From, updatedBy)
	if err != nil {
		return fmt.Errorf("smtpoutconfig: upsert: %w", err)
	}
	return nil
}

// Delete removes the SMTP-Out configuration.
func (s *Store) Delete(ctx context.Context) error {
	_, err := s.pool.Exec(ctx, `DELETE FROM smtp_out_config WHERE id = 1`)
	return err
}

func (s *Store) query(ctx context.Context) (*SMTPOutConfig, error) {
	row := s.pool.QueryRow(ctx,
		`SELECT id, enabled, host, port, usr, password, tls, from_addr, updated_at, updated_by
		 FROM smtp_out_config WHERE id = 1`,
	)
	var cfg SMTPOutConfig
	var encPw []byte
	err := row.Scan(&cfg.ID, &cfg.Enabled, &cfg.Host, &cfg.Port, &cfg.User, &encPw, &cfg.TLS, &cfg.From, &cfg.UpdatedAt, &cfg.UpdatedBy)
	if err == pgx.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("smtpoutconfig: query: %w", err)
	}
	if len(encPw) > 0 {
		pw, err := s.decrypt(encPw)
		if err != nil {
			return nil, fmt.Errorf("smtpoutconfig: decrypt: %w", err)
		}
		cfg.Password = pw
	}
	return &cfg, nil
}

// ── AES-256-GCM helpers ───────────────────────────────────────────────────────

func (s *Store) encrypt(plain string) ([]byte, error) {
	block, err := aes.NewCipher(s.encKey[:])
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, err
	}
	return gcm.Seal(nonce, nonce, []byte(plain), nil), nil
}

func (s *Store) decrypt(data []byte) (string, error) {
	block, err := aes.NewCipher(s.encKey[:])
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}
	ns := gcm.NonceSize()
	if len(data) < ns {
		return "", fmt.Errorf("smtpoutconfig: ciphertext too short")
	}
	plain, err := gcm.Open(nil, data[:ns], data[ns:], nil)
	if err != nil {
		return "", fmt.Errorf("smtpoutconfig: decrypt: %w", err)
	}
	return string(plain), nil
}
