package main

import (
	"database/sql"
	"fmt"
	"net"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"syscall"

	embeddedpostgres "github.com/fergusstrange/embedded-postgres"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/coderd/database/dbtestutil"
	"github.com/coder/coder/v2/coderd/database/migrations"
	"github.com/coder/coder/v2/scripts/atomicwrite"
)

var preamble = []byte("-- Code generated by 'make coderd/database/generate'. DO NOT EDIT.")

type mockTB struct {
	cleanup []func()
}

func (*mockTB) Name() string {
	return "mockTB"
}

func (t *mockTB) Cleanup(f func()) {
	t.cleanup = append(t.cleanup, f)
}

func (*mockTB) Helper() {
	// noop
}

func (*mockTB) Logf(format string, args ...any) {
	_, _ = fmt.Printf(format, args...)
}

func (*mockTB) TempDir() string {
	panic("not implemented")
}

func main() {
	t := &mockTB{}

	// Ensure cleanups run on both normal exit and SIGINT/SIGTERM.
	// Go's default signal handlers call os.Exit, which skips deferred
	// funcs and would leave an embedded-postgres daemon orphaned.
	var cleanupOnce sync.Once
	runCleanup := func() {
		cleanupOnce.Do(func() {
			for _, f := range t.cleanup {
				f()
			}
		})
	}
	defer runCleanup()

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
	go func() {
		<-sigCh
		runCleanup()
		os.Exit(130)
	}()

	connection := os.Getenv("DB_DUMP_CONNECTION_URL")
	if connection == "" {
		var cleanup func()
		var err error
		connection, cleanup, err = dbtestutil.OpenContainerized(t, dbtestutil.DBContainerOptions{})
		if err != nil {
			_, _ = fmt.Fprintf(os.Stderr, "containerized postgres unavailable (%s); falling back to embedded postgres\n", err)
			connection, cleanup, err = openEmbeddedPostgres()
			if err != nil {
				panic(err)
			}
		}
		t.Cleanup(cleanup)
	}

	db, err := sql.Open("postgres", connection)
	if err != nil {
		err = xerrors.Errorf("open database failed: %w", err)
		panic(err)
	}
	defer db.Close()

	err = migrations.Up(db)
	if err != nil {
		err = xerrors.Errorf("run migrations failed: %w", err)
		panic(err)
	}

	dumpBytes, err := dbtestutil.PGDumpSchemaOnly(connection)
	if err != nil {
		if !pgDumpUsable() {
			_, _ = fmt.Fprintf(os.Stderr,
				"\nThis step needs pg_dump (PostgreSQL v13 or later) on PATH OR a Docker-compatible daemon.\n"+
					"Install pg_dump locally to avoid Docker:\n"+
					"  mise:  mise use -g postgres@13\n"+
					"  brew:  brew install libpq && brew link --force libpq\n"+
					"  apt:   sudo apt-get install -y postgresql-client\n\n")
		}
		err = xerrors.Errorf("dump schema failed: %w", err)
		panic(err)
	}

	_, mainPath, _, ok := runtime.Caller(0)
	if !ok {
		panic("couldn't get caller path")
	}
	err = atomicwrite.File(filepath.Join(mainPath, "..", "..", "..", "dump.sql"), append(preamble, dumpBytes...))
	if err != nil {
		err = xerrors.Errorf("write dump failed: %w", err)
		panic(err)
	}
}

// pgDumpUsable mirrors PGDumpSchemaOnly's requirement (pg_dump on PATH at
// v13 or later). PGDumpSchemaOnly silently falls back to `docker run` when
// either condition fails, so we only show the install hint here when the
// local pg_dump is genuinely unusable. Otherwise an old pg_dump would
// produce a misleading Docker-not-found message.
func pgDumpUsable() bool {
	path, err := exec.LookPath("pg_dump")
	if err != nil {
		return false
	}
	out, err := exec.Command(path, "--version").Output()
	if err != nil {
		return false
	}
	// Output format: "pg_dump (PostgreSQL) 14.5 ..."
	parts := strings.Fields(string(out))
	if len(parts) < 3 {
		return false
	}
	major, err := strconv.Atoi(strings.SplitN(parts[2], ".", 2)[0])
	if err != nil {
		return false
	}
	return major >= 13
}

func openEmbeddedPostgres() (string, func(), error) {
	listener, err := net.Listen("tcp4", "127.0.0.1:0")
	if err != nil {
		return "", nil, xerrors.Errorf("find ephemeral port: %w", err)
	}
	tcpAddr, ok := listener.Addr().(*net.TCPAddr)
	if !ok {
		_ = listener.Close()
		return "", nil, xerrors.New("listener returned non-TCP addr")
	}
	port := tcpAddr.Port
	_ = listener.Close()

	cacheRoot, err := os.UserCacheDir()
	if err != nil {
		cacheRoot = os.TempDir()
	}
	cacheDir := filepath.Join(cacheRoot, "coder", "dbdump-postgres")

	runtimeDir, err := os.MkdirTemp("", "coder-dbdump-postgres-")
	if err != nil {
		return "", nil, xerrors.Errorf("create runtime dir: %w", err)
	}

	const password = "postgres"
	ep := embeddedpostgres.NewDatabase(
		embeddedpostgres.DefaultConfig().
			Version(embeddedpostgres.V13).
			// repo1.maven.org is flaky; matches cli/server.go and scripts/embedded-pg/main.go.
			BinaryRepositoryURL("https://repo.maven.apache.org/maven2").
			BinariesPath(filepath.Join(cacheDir, "bin")).
			CachePath(filepath.Join(cacheDir, "cache")).
			DataPath(filepath.Join(runtimeDir, "data")).
			RuntimePath(filepath.Join(runtimeDir, "runtime")).
			Port(uint32(port)). //nolint:gosec // port from listener, fits uint32.
			Username("postgres").
			Password(password).
			Database("postgres").
			// Postgres canonicalizes timestamptz DEFAULT expressions at
			// parse time using the server timezone GUC, then stores the
			// canonical form in pg_attrdef. Without UTC, the host's TZ
			// leaks into dump.sql as values like '0001-12-31 23:06:32+00 BC'.
			StartParameters(map[string]string{"timezone": "UTC"}).
			Logger(nil),
	)

	_, _ = fmt.Fprintln(os.Stderr, "starting embedded postgres (first run may download binaries)...")
	if err := ep.Start(); err != nil {
		_ = os.RemoveAll(runtimeDir)
		return "", nil, xerrors.Errorf("start embedded postgres: %w", err)
	}

	dsn := dbtestutil.ConnectionParams{
		Username: "postgres",
		Password: password,
		Host:     "127.0.0.1",
		Port:     strconv.Itoa(port),
		DBName:   "postgres",
	}.DSN()

	cleanup := func() {
		if stopErr := ep.Stop(); stopErr != nil {
			_, _ = fmt.Fprintf(os.Stderr, "failed to stop embedded postgres: %s\n", stopErr)
		}
		_ = os.RemoveAll(runtimeDir)
	}
	return dsn, cleanup, nil
}
