package sqldb

import (
	"context"
	"strings"

	"database/sql"

	"github.com/featurebasedb/featurebase/v3/dax"
	"github.com/featurebasedb/featurebase/v3/dax/controller"
	"github.com/featurebasedb/featurebase/v3/errors"
	"github.com/featurebasedb/featurebase/v3/logger"
	"github.com/gobuffalo/pop/v6"
)

// Transactor wraps a pop Connection to make it into a dax.Transactor
// which can be used by the controller agnostic of implementation.
type Transactor struct {
	*pop.Connection

	logger logger.Logger
}

func NewTransactor(cfg *controller.SQLDBConfig, log logger.Logger) (Transactor, error) {
	conn, err := pop.NewConnection(&pop.ConnectionDetails{
		Dialect:         cfg.Dialect,
		Database:        cfg.Database,
		Host:            cfg.Host,
		Port:            cfg.Port,
		User:            cfg.User,
		Password:        cfg.Password,
		URL:             cfg.URL,
		Pool:            cfg.Pool,
		IdlePool:        cfg.IdlePool,
		ConnMaxLifetime: cfg.ConnMaxLifetime,
		ConnMaxIdleTime: cfg.ConnMaxIdleTime,
	})
	if err != nil {
		return Transactor{Connection: nil}, errors.Wrap(err, "creating new connection")
	}

	return Transactor{
		Connection: conn,
		logger:     log,
	}, nil
}

// Start creates the database specified in the database connection, then runs
// any outstanding migrations.
func (t Transactor) Start() error {
	conn := t.Connection

	// Create the database if it doesn't exist.
	if err := pop.CreateDB(conn); err != nil {
		t.logger.Warnf("auto-creating database, got error '%v'", err)
	}

	// Open a connection to the database.
	if err := conn.Open(); err != nil {
		return errors.Wrap(err, "opening connection")
	}

	// Run migrations.
	if mig, err := NewEmbedMigrator(dax.MigrationsFS, conn, t.logger); err != nil {
		return errors.Wrap(err, "getting embedded migrator")
	} else if err = mig.Up(); err != nil {
		return errors.Wrap(err, "migrating DB")
	}

	err := t.RawQuery("INSERT INTO directive_versions (id, version, created_at, updated_at) VALUES (1, 0, '1970-01-01T00:00', '1970-01-01T00:00')").Exec()
	if err != nil && !strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
		return errors.Wrap(err, "unexpected error (re)inserting directive_version record")
	}

	return nil
}

func (t Transactor) BeginTx(ctx context.Context, writable bool) (dax.Transaction, error) {
	cn, err := t.NewTransactionContextOptions(ctx, &sql.TxOptions{ReadOnly: !writable})
	if err != nil {
		return nil, errors.Wrap(err, "getting SQL transaction")
	}
	return &DaxTransaction{C: cn}, nil
}

func (t Transactor) Close() error {
	return t.Connection.Close()
}

// DaxTransaction is a thin wrapper to create a dax.Transaction from a
// pop Transaction/Connection.
type DaxTransaction struct {
	C *pop.Connection
}

func (w *DaxTransaction) Commit() error {
	return w.C.TX.Commit()
}

func (w *DaxTransaction) Context() context.Context {
	return w.C.Context()
}
func (w *DaxTransaction) Rollback() error {
	return w.C.TX.Rollback()
}

// DropDatabase drops the database associated with the given
// Transactor (which embeds a live database connection). This is
// destructive, you will lose data.
func DropDatabase(trans Transactor) error {
	conn := trans.Connection
	return pop.DropDB(conn)
}
