package backup

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"time"

	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/config"
	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/composite"
	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/keymanager"
	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/mlkem768"
	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/x25519"
	adapterpgdump "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pgdump"
	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pipeline"
	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/retention"
	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/storage/local"
	domaincrypto "git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
	domainpgdump "git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
)

func RunOnce(ctx context.Context, cfg *config.Config) error {
	registry := domaincrypto.NewRegistry()
	if err := registry.Register(0x0006, func() domaincrypto.KEM { return mlkem768.New() }); err != nil {
		return fmt.Errorf("register mlkem768: %w", err)
	}
	if err := registry.Register(0x0007, func() domaincrypto.KEM { return x25519.New() }); err != nil {
		return fmt.Errorf("register x25519: %w", err)
	}

	keyMgr := keymanager.NewKeyManager(registry)

	pqPub, err := keyMgr.LoadPub(cfg.PQPublicKeyPath, cfg.PQScheme)
	if err != nil {
		return fmt.Errorf("load pq public key: %w", err)
	}

	classicalPub, err := keyMgr.LoadPub(cfg.ClassicalPublicKeyPath, cfg.ClassicalScheme)
	if err != nil {
		return fmt.Errorf("load classical public key: %w", err)
	}

	recipients := []domaincrypto.RecipientPub{pqPub, classicalPub}

	encryptor := composite.NewEncryptor(registry)
	dumper := adapterpgdump.New()
	sink := local.NewLocalSink(cfg.Backup.Dir)

	runner := pipeline.NewRunner(
		pipeline.WithDumper(dumper),
		pipeline.WithEncryptor(encryptor),
	)

	now := time.Now()
	pgDumpOpts := domainpgdump.Options{
		Host:          cfg.PG.Host,
		Port:          cfg.PG.Port,
		Database:      cfg.PG.Database,
		User:          cfg.PG.User,
		Password:      cfg.PG.Password,
		ExcludeTables: cfg.PG.ExcludeTables,
		Key:           ArtifactKey(now),
	}

	if err := runner.Run(ctx, pgDumpOpts, recipients, sink, rand.Reader); err != nil {
		return fmt.Errorf("backup pipeline failed: %w", err)
	}

	if _, err := retention.PruneByAge(ctx, cfg.Backup.Dir, cfg.Backup.RetentionDays, now); err != nil {
		return fmt.Errorf("retention pruning failed: %w", err)
	}

	return nil
}

// ArtifactKey returns a backup file name that includes a UTC timestamp plus a
// short random suffix so that two backups started in the same second do not
// collide.
func ArtifactKey(timestamp time.Time) string {
	var randomBytes [3]byte
	if _, err := rand.Read(randomBytes[:]); err != nil {
		return fmt.Sprintf(
			"synapse-%s-%09d.dump.pqenc",
			timestamp.UTC().Format("20060102-150405"),
			timestamp.Nanosecond(),
		)
	}
	return fmt.Sprintf(
		"synapse-%s-%s.dump.pqenc",
		timestamp.UTC().Format("20060102-150405"),
		hex.EncodeToString(randomBytes[:]),
	)
}
