// Command mciassrv is the MCIAS authentication server.
//
// It reads a TOML configuration file, derives the master encryption key,
// loads or generates the Ed25519 signing key, opens the SQLite database,
// runs migrations, and starts an HTTPS listener.
// If [server] grpc_addr is set in the config, a gRPC/TLS listener is also
// started on that address. Both listeners share the same signing key, DB,
// and config. Graceful shutdown drains both within the configured window.
//
// Usage:
//
//	mciassrv -config /srv/mcias/mcias.toml
package main

import (
	"context"
	"crypto/ed25519"
	"crypto/tls"
	"errors"
	"flag"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	"git.wntrmute.dev/mc/mcias/internal/config"
	"git.wntrmute.dev/mc/mcias/internal/crypto"
	"git.wntrmute.dev/mc/mcias/internal/db"
	"git.wntrmute.dev/mc/mcias/internal/grpcserver"
	"git.wntrmute.dev/mc/mcias/internal/server"
	"git.wntrmute.dev/mc/mcias/internal/vault"
)

func main() {
	configPath := flag.String("config", "/srv/mcias/mcias.toml", "path to TOML configuration file")
	flag.Parse()

	logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))

	if err := run(*configPath, logger); err != nil {
		logger.Error("fatal", "error", err)
		os.Exit(1)
	}
}

func run(configPath string, logger *slog.Logger) error {
	// Load and validate configuration.
	cfg, err := config.Load(configPath)
	if err != nil {
		return fmt.Errorf("load config: %w", err)
	}
	logger.Info("configuration loaded", "listen_addr", cfg.Server.ListenAddr, "grpc_addr", cfg.Server.GRPCAddr)

	// Open and migrate the database first — we need it to load the master key salt.
	database, err := db.Open(cfg.Database.Path)
	if err != nil {
		return fmt.Errorf("open database: %w", err)
	}
	defer func() { _ = database.Close() }()

	if err := db.Migrate(database); err != nil {
		return fmt.Errorf("migrate database: %w", err)
	}
	logger.Info("database ready", "path", cfg.Database.Path)

	// Derive or load the master encryption key and build the vault.
	// Security: The master key encrypts TOTP secrets, Postgres passwords, and
	// the signing key at rest. It is derived from a passphrase via Argon2id
	// (or loaded directly from a key file). The KDF salt is stored in the DB
	// for stability across restarts. The passphrase env var is cleared after use.
	//
	// When the passphrase is not available (empty env var in passphrase mode
	// with no key file), the server starts in sealed state. The operator must
	// provide the passphrase via the /v1/vault/unseal API or the /unseal UI page.
	// First run (no signing key in DB) still requires the passphrase at startup.
	var v *vault.Vault
	masterKey, mkErr := loadMasterKey(cfg, database)
	if mkErr != nil {
		// Check if we can start sealed (passphrase mode, empty env var).
		if cfg.MasterKey.KeyFile != "" || os.Getenv(cfg.MasterKey.PassphraseEnv) != "" {
			return fmt.Errorf("load master key: %w", mkErr)
		}
		// Verify that this is not a first run — the signing key must already exist.
		enc, nonce, scErr := database.ReadServerConfig()
		if scErr != nil || enc == nil || nonce == nil {
			return fmt.Errorf("first run requires passphrase: %w", mkErr)
		}
		v = vault.NewSealed()
		logger.Info("vault starting in sealed state")
	} else {
		// Load or generate the Ed25519 signing key.
		// Security: The private signing key is stored AES-256-GCM encrypted in the
		// database. On first run it is generated and stored. The key is decrypted
		// with the master key each startup.
		privKey, pubKey, err := loadOrGenerateSigningKey(database, masterKey, logger)
		if err != nil {
			// Zero master key on failure.
			for i := range masterKey {
				masterKey[i] = 0
			}
			return fmt.Errorf("signing key: %w", err)
		}
		v = vault.NewUnsealed(masterKey, privKey, pubKey)
		logger.Info("vault unsealed at startup")
	}

	// Configure TLS. We require TLS 1.2+ and prefer TLS 1.3.
	// Security: HTTPS/gRPC-TLS is mandatory; no plaintext listener is provided.
	// The same TLS certificate is used for both REST and gRPC listeners.
	tlsCfg := &tls.Config{
		MinVersion: tls.VersionTLS12,
		CurvePreferences: []tls.CurveID{
			tls.X25519,
			tls.CurveP256,
		},
	}

	// Build the REST handler. All servers share the same vault by pointer.
	restSrv := server.New(database, cfg, v, logger)
	httpServer := &http.Server{
		Addr:              cfg.Server.ListenAddr,
		Handler:           restSrv.Handler(),
		TLSConfig:         tlsCfg,
		ReadTimeout:       30 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       120 * time.Second,
		ReadHeaderTimeout: 5 * time.Second,
	}

	// Build the gRPC server if grpc_addr is configured.
	var grpcSrv *grpc.Server
	var grpcListener net.Listener
	if cfg.Server.GRPCAddr != "" {
		// Load TLS credentials for gRPC using the same cert/key as REST.
		// Security: TLS 1.2 minimum is enforced via tls.Config; no h2c is offered.
		grpcTLSCreds, err := credentials.NewServerTLSFromFile(cfg.Server.TLSCert, cfg.Server.TLSKey)
		if err != nil {
			return fmt.Errorf("load gRPC TLS credentials: %w", err)
		}

		grpcSrvImpl := grpcserver.New(database, cfg, v, logger)
		// Build server directly with TLS credentials. GRPCServerWithCreds builds
		// the server with transport credentials at construction time per gRPC idiom.
		grpcSrv = rebuildGRPCServerWithTLS(grpcSrvImpl, grpcTLSCreds)

		grpcListener, err = net.Listen("tcp", cfg.Server.GRPCAddr)
		if err != nil {
			return fmt.Errorf("gRPC listen: %w", err)
		}
		logger.Info("gRPC listener ready", "addr", cfg.Server.GRPCAddr)
	}

	// Graceful shutdown on SIGINT/SIGTERM.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	var wg sync.WaitGroup
	errCh := make(chan error, 2)

	// Start REST listener.
	wg.Add(1)
	go func() {
		defer wg.Done()
		logger.Info("REST server starting", "addr", cfg.Server.ListenAddr)
		if err := httpServer.ListenAndServeTLS(cfg.Server.TLSCert, cfg.Server.TLSKey); err != nil {
			if !errors.Is(err, http.ErrServerClosed) {
				errCh <- fmt.Errorf("REST server: %w", err)
			}
		}
	}()

	// Start gRPC listener if configured.
	if grpcSrv != nil && grpcListener != nil {
		wg.Add(1)
		go func() {
			defer wg.Done()
			logger.Info("gRPC server starting", "addr", cfg.Server.GRPCAddr)
			if err := grpcSrv.Serve(grpcListener); err != nil {
				errCh <- fmt.Errorf("gRPC server: %w", err)
			}
		}()
	}

	// Wait for shutdown signal or a server error.
	select {
	case <-ctx.Done():
		logger.Info("shutdown signal received")
	case err := <-errCh:
		return err
	}

	// Graceful drain: give servers up to 15s to finish in-flight requests.
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	if err := httpServer.Shutdown(shutdownCtx); err != nil {
		logger.Error("REST shutdown error", "error", err)
	}
	if grpcSrv != nil {
		grpcSrv.GracefulStop()
	}

	wg.Wait()

	// Drain any remaining error from startup goroutines.
	select {
	case err := <-errCh:
		return err
	default:
	}
	return nil
}

// rebuildGRPCServerWithTLS creates a new *grpc.Server with TLS credentials
// and re-registers all services from the implementation.
// This is needed because grpc.NewServer accepts credentials as an option at
// construction time, not after the fact.
func rebuildGRPCServerWithTLS(impl *grpcserver.Server, creds credentials.TransportCredentials) *grpc.Server {
	return impl.GRPCServerWithCreds(creds)
}

// loadMasterKey derives or loads the AES-256-GCM master key from the config.
//
// Key file mode: reads exactly 32 bytes from a file.
//
// Passphrase mode: reads the passphrase from the named environment variable,
// then immediately clears it from the environment. The Argon2id KDF salt is
// stored in the database on first run and retrieved on subsequent runs so that
// the same passphrase always yields the same master key.
//
// Security: The Argon2id parameters used by crypto.DeriveKey exceed OWASP 2023
// minimums (time=3, memory=128MiB, threads=4). The salt is 32 random bytes.
func loadMasterKey(cfg *config.Config, database *db.DB) ([]byte, error) {
	if cfg.MasterKey.KeyFile != "" {
		// Key file mode: file must contain exactly 32 bytes (AES-256).
		data, err := os.ReadFile(cfg.MasterKey.KeyFile) //nolint:gosec // G304: operator-supplied path
		if err != nil {
			return nil, fmt.Errorf("read key file: %w", err)
		}
		if len(data) != 32 {
			return nil, fmt.Errorf("key file must be exactly 32 bytes, got %d", len(data))
		}
		key := make([]byte, 32)
		copy(key, data)
		// Zero the file buffer before it can be GC'd.
		for i := range data {
			data[i] = 0
		}
		return key, nil
	}

	// Passphrase mode.
	passphrase := os.Getenv(cfg.MasterKey.PassphraseEnv)
	if passphrase == "" {
		return nil, fmt.Errorf("environment variable %q is not set or empty", cfg.MasterKey.PassphraseEnv)
	}
	// Immediately unset the env var so child processes cannot read it.
	_ = os.Unsetenv(cfg.MasterKey.PassphraseEnv)

	// Retrieve or create the KDF salt.
	salt, err := database.ReadMasterKeySalt()
	if errors.Is(err, db.ErrNotFound) {
		// First run: generate and persist a new salt.
		salt, err = crypto.NewSalt()
		if err != nil {
			return nil, fmt.Errorf("generate master key salt: %w", err)
		}
		if err := database.WriteMasterKeySalt(salt); err != nil {
			return nil, fmt.Errorf("store master key salt: %w", err)
		}
	} else if err != nil {
		return nil, fmt.Errorf("read master key salt: %w", err)
	}

	key, err := crypto.DeriveKey(passphrase, salt)
	if err != nil {
		return nil, fmt.Errorf("derive master key: %w", err)
	}
	return key, nil
}

// loadOrGenerateSigningKey loads the Ed25519 signing key from the database
// (decrypted with masterKey), or generates and stores a new one on first run.
//
// Security: The private key is stored AES-256-GCM encrypted. A fresh random
// nonce is used for each encryption. The plaintext key only exists in memory
// during the process lifetime.
func loadOrGenerateSigningKey(database *db.DB, masterKey []byte, logger *slog.Logger) (ed25519.PrivateKey, ed25519.PublicKey, error) {
	// Try to load existing key.
	enc, nonce, err := database.ReadServerConfig()
	if err == nil && enc != nil && nonce != nil {
		privPEM, err := crypto.OpenAESGCM(masterKey, nonce, enc)
		if err != nil {
			return nil, nil, fmt.Errorf("decrypt signing key: %w", err)
		}

		priv, err := crypto.ParsePrivateKeyPEM(privPEM)
		if err != nil {
			return nil, nil, fmt.Errorf("parse signing key PEM: %w", err)
		}

		// Security: ed25519.PrivateKey.Public() always returns ed25519.PublicKey,
		// but we use the ok form to make the type assertion explicit and safe.
		pub, ok := priv.Public().(ed25519.PublicKey)
		if !ok {
			return nil, nil, fmt.Errorf("signing key has unexpected public key type")
		}
		logger.Info("signing key loaded from database")
		return priv, pub, nil
	}

	// First run: generate and store a new signing key.
	logger.Info("generating new Ed25519 signing key")
	pub, priv, err := crypto.GenerateEd25519KeyPair()
	if err != nil {
		return nil, nil, fmt.Errorf("generate signing key: %w", err)
	}

	privPEM, err := crypto.MarshalPrivateKeyPEM(priv)
	if err != nil {
		return nil, nil, fmt.Errorf("marshal signing key: %w", err)
	}

	encKey, encNonce, err := crypto.SealAESGCM(masterKey, privPEM)
	if err != nil {
		return nil, nil, fmt.Errorf("encrypt signing key: %w", err)
	}

	if err := database.WriteServerConfig(encKey, encNonce); err != nil {
		return nil, nil, fmt.Errorf("store signing key: %w", err)
	}

	logger.Info("signing key generated and stored")
	return priv, pub, nil
}
