package main

import (
	"context"
	"crypto/rand"
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"time"

	"github.com/google/uuid"
	"github.com/spf13/cobra"

	"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"
	pgdumpadapter "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"
	"git.tswf.io/infra/go-synapse-backupper/pkg/domain"
	"git.tswf.io/infra/go-synapse-backupper/pkg/domain/backup"
	"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
	"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
)

var (
	newKeyManager           = keymanager.NewKeyManager
	newLocalSink            = local.NewLocalSink
	newRunner               = defaultNewRunner
	outputWriter  io.Writer = os.Stderr
)

func defaultNewRunner(options ...pipeline.Option) pipeline.Runner {
	return pipeline.NewRunner(options...)
}

func init() {
	config.RegisterFlags(backupCmd)
}

var backupCmd = &cobra.Command{
	Use:   "backup",
	Short: "Run a one-off backup",
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := cmd.Context()
		if ctx == nil {
			ctx = context.Background()
		}

		startTime := time.Now().UTC()
		runID, err := uuid.NewRandom()
		if err != nil {
			return fmt.Errorf("generate run-id: %w", err)
		}

		logger := slog.New(slog.NewTextHandler(outputWriter, nil))
		logger.Info(
			"backup started",
			slog.String("run_id", runID.String()),
			slog.Time("start_time", startTime),
		)

		cfg, err := config.Load(cmd)
		if err != nil {
			return fmt.Errorf("load config: %w", err)
		}

		keyManager := newKeyManager(crypto.NewRegistry())
		pqPub, err := keyManager.LoadPub(cfg.PQPublicKeyPath, cfg.PQScheme)
		if err != nil {
			return fmt.Errorf("load PQ public key: %w", err)
		}

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

		recipients := make([]crypto.RecipientPub, 0, 2)
		recipients = append(recipients, pqPub, classicalPub)

		pgDumpOpts := pgdump.Options{
			Host:          cfg.PG.Host,
			Port:          cfg.PG.Port,
			Database:      cfg.PG.Database,
			User:          cfg.PG.User,
			Password:      cfg.PG.Password,
			Key:           backup.ArtifactKey(startTime),
			ExcludeTables: cfg.PG.ExcludeTables,
		}

		finalPath := filepath.Join(cfg.Backup.Dir, pgDumpOpts.Key)
		logger.Info(
			"backup destination",
			slog.String("backup_dir", cfg.Backup.Dir),
			slog.String("output_path", finalPath),
		)

		var sink domain.Sink = newLocalSink(cfg.Backup.Dir)

		registry := crypto.NewRegistry()
		_ = registry.Register(0x0006, func() crypto.KEM { return mlkem768.New() })
		_ = registry.Register(0x0007, func() crypto.KEM { return x25519.New() })
		runner := newRunner(
			pipeline.WithDumper(pgdumpadapter.New()),
			pipeline.WithEncryptor(composite.NewEncryptor(registry)),
		)

		err = runner.Run(ctx, pgDumpOpts, recipients, sink, rand.Reader)
		endTime := time.Now().UTC()

		var byteCount int64
		if info, statErr := os.Stat(finalPath); statErr == nil {
			byteCount = info.Size()
		}

		if err != nil {
			logger.Error(
				"backup failed",
				slog.String("run_id", runID.String()),
				slog.String("output_path", finalPath),
				slog.Time("start_time", startTime),
				slog.Time("end_time", endTime),
				slog.Int64("byte_count", byteCount),
				slog.String("error", err.Error()),
			)
			return err
		}

		logger.Info(
			"backup completed",
			slog.String("run_id", runID.String()),
			slog.String("output_path", finalPath),
			slog.Time("start_time", startTime),
			slog.Time("end_time", endTime),
			slog.Int64("byte_count", byteCount),
		)

		if _, pruneErr := retention.PruneByAge(
			ctx,
			cfg.Backup.Dir,
			cfg.Backup.RetentionDays,
			time.Now(),
		); pruneErr != nil {
			logger.Error(
				"retention pruning failed",
				slog.String("error", pruneErr.Error()),
			)
			return pruneErr
		}

		return nil
	},
}
