// Command jetgen regenerates the go-jet/v2 query-builder code under
// backend/internal/postgres/jet against a transient PostgreSQL instance.
//
// Invoke as `go run ./cmd/jetgen` from inside the backend module. The tool is
// not part of the runtime binary and requires a reachable Docker daemon.
//
// Steps:
//
//  1. start a postgres:17-alpine container via testcontainers-go
//  2. open it with search_path=backend and apply the embedded goose migrations
//  3. drop goose's bookkeeping table so jet does not generate a model for it
//  4. run jet's PostgreSQL generator for the backend and payments schemas into
//     internal/postgres/jet (one subdirectory per schema)
package main

import (
	"context"
	"fmt"
	"log"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"time"

	jetpostgres "github.com/go-jet/jet/v2/generator/postgres"
	testcontainers "github.com/testcontainers/testcontainers-go"
	tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
	"github.com/testcontainers/testcontainers-go/wait"

	"scrabble/backend/internal/postgres"
)

const (
	postgresImage      = "postgres:17-alpine"
	superuserName      = "scrabble"
	superuserPassword  = "scrabble"
	superuserDatabase  = "scrabble_backend"
	backendSchema      = "backend"
	paymentsSchema     = "payments"
	containerStartup   = 90 * time.Second
	jetOutputDirSuffix = "internal/postgres/jet"
)

func main() {
	if err := run(context.Background()); err != nil {
		log.Fatalf("jetgen: %v", err)
	}
}

func run(ctx context.Context) error {
	outputDir, err := jetOutputDir()
	if err != nil {
		return err
	}

	container, err := tcpostgres.Run(ctx, postgresImage,
		tcpostgres.WithDatabase(superuserDatabase),
		tcpostgres.WithUsername(superuserName),
		tcpostgres.WithPassword(superuserPassword),
		testcontainers.WithWaitStrategy(
			wait.ForLog("database system is ready to accept connections").
				WithOccurrence(2).
				WithStartupTimeout(containerStartup),
		),
	)
	if err != nil {
		return fmt.Errorf("start postgres container: %w", err)
	}
	defer func() {
		if termErr := testcontainers.TerminateContainer(container); termErr != nil {
			log.Printf("jetgen: terminate container: %v", termErr)
		}
	}()

	baseDSN, err := container.ConnectionString(ctx, "sslmode=disable")
	if err != nil {
		return fmt.Errorf("resolve container dsn: %w", err)
	}
	scopedDSN, err := dsnWithSearchPath(baseDSN, backendSchema)
	if err != nil {
		return err
	}

	cfg := postgres.DefaultConfig()
	cfg.DSN = scopedDSN

	db, err := postgres.Open(ctx, cfg)
	if err != nil {
		return fmt.Errorf("open scoped pool: %w", err)
	}
	defer func() { _ = db.Close() }()

	if err := postgres.ApplyMigrations(ctx, db); err != nil {
		return fmt.Errorf("apply migrations: %w", err)
	}

	// jet's generator wipes <outputDir>/<schema> on every run; ensure the
	// parent exists so the first run on a fresh checkout does not fail.
	if err := os.MkdirAll(outputDir, 0o755); err != nil {
		return fmt.Errorf("ensure jet output dir: %w", err)
	}

	// Drop goose's bookkeeping table so jet does not generate code for it. The
	// container is never reused, so this only affects generation.
	if _, err := db.ExecContext(ctx, "DROP TABLE IF EXISTS goose_db_version"); err != nil {
		return fmt.Errorf("drop goose_db_version: %w", err)
	}

	// Each schema generates into its own jet/<schema>/ subdirectory (the
	// generator wipes only that subtree), so the two calls do not clobber each
	// other. GenerateDB takes the schema explicitly, so the connection's
	// search_path=backend does not affect the payments introspection.
	for _, schema := range []string{backendSchema, paymentsSchema} {
		if err := jetpostgres.GenerateDB(db, schema, outputDir); err != nil {
			return fmt.Errorf("jet generate schema=%s: %w", schema, err)
		}
		log.Printf("jetgen: generated jet code into %s (schema=%s)", outputDir, schema)
	}
	return nil
}

// dsnWithSearchPath rewrites the connection string so every new connection pins
// search_path to the named schema and disables TLS for the local container.
func dsnWithSearchPath(baseDSN, schema string) (string, error) {
	parsed, err := url.Parse(baseDSN)
	if err != nil {
		return "", fmt.Errorf("parse base dsn: %w", err)
	}
	values := parsed.Query()
	values.Set("search_path", schema)
	if values.Get("sslmode") == "" {
		values.Set("sslmode", "disable")
	}
	parsed.RawQuery = values.Encode()
	return parsed.String(), nil
}

// jetOutputDir returns the absolute path jet writes into, anchored to the
// backend module via runtime.Caller so the tool runs from any directory.
func jetOutputDir() (string, error) {
	_, file, _, ok := runtime.Caller(0)
	if !ok {
		return "", fmt.Errorf("resolve runtime caller for jet output path")
	}
	// file = .../backend/cmd/jetgen/main.go
	moduleRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", ".."))
	return filepath.Join(moduleRoot, jetOutputDirSuffix), nil
}
