// Command jetgen regenerates the go-jet/v2 query-builder code under
// galaxy/backend/internal/postgres/jet/ against a transient PostgreSQL
// instance.
//
// Invoke as `go run ./cmd/jetgen` (or via the `make jet` target) from inside
// `galaxy/backend`. The tool is not part of the runtime binary.
//
// Steps:
//
//  1. start a postgres:16-alpine container via testcontainers-go
//  2. open it through galaxy/postgres with search_path=backend
//  3. ensure the backend schema exists, then apply the embedded goose
//     migrations
//  4. run jet's PostgreSQL generator against schema=backend, writing into
//     ../internal/postgres/jet
package main

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"galaxy/backend/internal/postgres/migrations"
	"galaxy/postgres"

	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"
)

const (
	postgresImage      = "postgres:16-alpine"
	superuserName      = "galaxy"
	superuserPassword  = "galaxy"
	superuserDatabase  = "galaxy_backend"
	backendSchema      = "backend"
	containerStartup   = 90 * time.Second
	defaultOpTimeout   = 10 * 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
	}

	if err := applyMigrations(ctx, scopedDSN); err != nil {
		return err
	}

	// jet's ProcessSchema wipes <outputDir>/<schema> on every run, so package
	// metadata kept directly under outputDir (e.g. jet.go) survives. We only
	// ensure the parent directory exists so the first run on a fresh
	// checkout does not fail with ENOENT.
	if err := os.MkdirAll(outputDir, 0o755); err != nil {
		return fmt.Errorf("ensure jet output dir: %w", err)
	}

	jetDB, err := openScoped(ctx, scopedDSN)
	if err != nil {
		return fmt.Errorf("open scoped pool for jet generation: %w", err)
	}
	defer func() { _ = jetDB.Close() }()

	// Drop goose's bookkeeping table inside the schema-scoped connection so
	// jet does not generate code for it. The table is recreated on the next
	// migration run; jetgen never reuses the container.
	if _, err := jetDB.ExecContext(ctx, "DROP TABLE IF EXISTS goose_db_version"); err != nil {
		return fmt.Errorf("drop goose_db_version: %w", err)
	}

	if err := jetpostgres.GenerateDB(jetDB, backendSchema, outputDir); err != nil {
		return fmt.Errorf("jet generate: %w", err)
	}

	log.Printf("jetgen: generated jet code into %s (schema=%s)", outputDir, backendSchema)
	return nil
}

// dsnWithSearchPath rewrites the connection string so each new connection
// pins search_path to the named schema. The schema must exist before the
// first query that depends on search_path resolution; ensureSchema handles
// that on the migration path.
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
}

func applyMigrations(ctx context.Context, dsn string) error {
	db, err := openScoped(ctx, dsn)
	if err != nil {
		return fmt.Errorf("open scoped pool: %w", err)
	}
	defer func() { _ = db.Close() }()

	if err := postgres.Ping(ctx, db, defaultOpTimeout); err != nil {
		return err
	}
	if err := ensureSchema(ctx, db, backendSchema); err != nil {
		return err
	}
	if err := postgres.RunMigrations(ctx, db, migrations.Migrations(), "."); err != nil {
		return fmt.Errorf("run migrations: %w", err)
	}
	return nil
}

// ensureSchema creates the named schema when it is absent. The statement is
// idempotent and unaffected by search_path, so it must run before goose
// creates its bookkeeping table inside the schema-scoped connection.
func ensureSchema(ctx context.Context, db *sql.DB, schema string) error {
	stmt := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", quoteIdent(schema))
	if _, err := db.ExecContext(ctx, stmt); err != nil {
		return fmt.Errorf("ensure schema %q: %w", schema, err)
	}
	return nil
}

func openScoped(ctx context.Context, dsn string) (*sql.DB, error) {
	cfg := postgres.DefaultConfig()
	cfg.PrimaryDSN = dsn
	cfg.OperationTimeout = defaultOpTimeout
	return postgres.OpenPrimary(ctx, cfg)
}

// jetOutputDir returns the absolute path that jet should write into. The path
// is anchored to galaxy/backend via runtime.Caller so the tool can be
// invoked from any working directory.
func jetOutputDir() (string, error) {
	_, file, _, ok := runtime.Caller(0)
	if !ok {
		return "", errors.New("resolve runtime caller for jet output path")
	}
	dir := filepath.Dir(file)
	// dir = .../galaxy/backend/cmd/jetgen
	moduleRoot := filepath.Clean(filepath.Join(dir, "..", ".."))
	return filepath.Join(moduleRoot, jetOutputDirSuffix), nil
}

// quoteIdent quotes a SQL identifier by doubling embedded quote characters.
// jetgen uses a fixed schema name, but quoting keeps the helper safe to reuse
// if the constant ever changes to a configurable value.
func quoteIdent(name string) string {
	return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
}
