// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package db

import (
	"context"
	"fmt"
	"time"

	"github.com/hashicorp/boundary/internal/errors"
	"github.com/hashicorp/boundary/internal/oplog"
	"github.com/hashicorp/go-dbw"
	"github.com/hashicorp/go-hclog"
	wrapping "github.com/hashicorp/go-kms-wrapping/v2"
)

// GetOpts - iterate the inbound Options and return a struct.
func GetOpts(opt ...Option) Options {
	opts := getDefaultOptions()
	for _, o := range opt {
		o(&opts)
	}
	return opts
}

func getDbwOptions(ctx context.Context, rw *Db, i any, opType OpType, opt ...Option) ([]dbw.Option, error) {
	const op = "db.getDbwOptions"
	opts := GetOpts(opt...)
	dbwOpts := make([]dbw.Option, 0, len(opt))
	oplogBefore, after, err := rw.generateOplogBeforeAfterOpts(ctx, i, opType, opts)
	if err != nil {
		return nil, errors.Wrap(ctx, err, op)
	}
	before := oplogBefore
	if !opts.withSkipVetForWrite && (opType != DeleteOp && opType != DeleteItemsOp) {
		if vetter, ok := i.(VetForWriter); ok {
			before = func(i any) error {
				if err := vetter.VetForWrite(ctx, rw, opType, opt...); err != nil {
					return err
				}
				if oplogBefore != nil {
					if err := oplogBefore(i); err != nil {
						return err
					}
				}
				return nil
			}
		}
	}
	if before != nil {
		dbwOpts = append(dbwOpts, dbw.WithBeforeWrite(before))
	}
	if after != nil {
		dbwOpts = append(dbwOpts, dbw.WithAfterWrite(after))
	}
	if opts.withOnConflict != nil {
		c := &dbw.OnConflict{}
		switch target := opts.withOnConflict.Target.(type) {
		case Constraint:
			c.Target = dbw.Constraint(target)
		case Columns:
			c.Target = dbw.Columns(target)
		default:
			return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("not a supported target type: %T", target))
		}
		switch action := opts.withOnConflict.Action.(type) {
		case DoNothing:
			c.Action = dbw.DoNothing(bool(action))
		case UpdateAll:
			c.Action = dbw.UpdateAll(bool(action))
		case []ColumnValue:
			colVals := make([]dbw.ColumnValue, 0, len(action))
			for _, cv := range action {
				dbwColVal := dbw.ColumnValue{
					Column: cv.column,
				}
				switch cvVal := cv.value.(type) {
				case ExprValue:
					dbwColVal.Value = dbw.ExprValue{
						Sql:  cvVal.sql,
						Vars: cvVal.vars,
					}
				case column:
					dbwColVal.Column = cvVal.name
					dbwColVal.Value = dbw.Column{
						Table: cvVal.table,
						Name:  cvVal.name,
					}
				default:
					dbwColVal.Value = cv.value
				}
				colVals = append(colVals, dbwColVal)
			}
			c.Action = colVals
		default:
			return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("not a valid supported action: %T", action))
		}
		dbwOpts = append(dbwOpts, dbw.WithOnConflict(c))
	}
	if opts.withLookup {
		dbwOpts = append(dbwOpts, dbw.WithLookup(opts.withLookup))
	}
	if opts.WithLimit != 0 {
		dbwOpts = append(dbwOpts, dbw.WithLimit(opts.WithLimit))
	}
	if len(opts.WithFieldMaskPaths) > 0 {
		dbwOpts = append(dbwOpts, dbw.WithFieldMaskPaths(opts.WithFieldMaskPaths))
	}
	if len(opts.WithNullPaths) > 0 {
		dbwOpts = append(dbwOpts, dbw.WithNullPaths(opts.WithNullPaths))
	}
	if opts.WithVersion != nil {
		dbwOpts = append(dbwOpts, dbw.WithVersion(opts.WithVersion))
	}
	if opts.withSkipVetForWrite {
		dbwOpts = append(dbwOpts, dbw.WithSkipVetForWrite(opts.withSkipVetForWrite))
	}
	if opts.withWhereClause != "" {
		dbwOpts = append(dbwOpts, dbw.WithWhere(opts.withWhereClause, opts.withWhereClauseArgs...))
	}
	if opts.withOrder != "" {
		dbwOpts = append(dbwOpts, dbw.WithOrder(opts.withOrder))
	}
	if len(opts.withPrngValues) > 0 {
		dbwOpts = append(dbwOpts, dbw.WithPrngValues(opts.withPrngValues))
	}
	if opts.withGormFormatter != nil {
		dbwOpts = append(dbwOpts, dbw.WithLogger(opts.withGormFormatter))
	}
	if opts.withMaxOpenConnections > 0 {
		dbwOpts = append(dbwOpts, dbw.WithMaxOpenConnections(opts.withMaxOpenConnections))
	}
	if opts.withDebug {
		dbwOpts = append(dbwOpts, dbw.WithDebug(opts.withDebug))
	}
	if opts.withRowsAffected != nil {
		dbwOpts = append(dbwOpts, dbw.WithReturnRowsAffected(opts.withRowsAffected))
	}
	if opts.withTable != "" {
		dbwOpts = append(dbwOpts, dbw.WithTable(opts.withTable))
	}
	return dbwOpts, nil
}

// Option - how Options are passed as arguments.
type Option func(*Options)

// Options - how Options are represented.
type Options struct {
	withOplog  bool
	oplogOpts  oplogOpts
	withLookup bool
	// WithLimit must be accessible in other packages.
	WithLimit int
	// WithFieldMaskPaths must be accessible from other packages.
	WithFieldMaskPaths []string
	// WithNullPaths must be accessible from other packages.
	WithNullPaths []string

	newOplogMsg  *oplog.Message
	newOplogMsgs *[]*oplog.Message

	// WithVersion must be accessible from other packages.
	WithVersion *uint32

	withSkipVetForWrite bool

	withWhereClause     string
	withWhereClauseArgs []any
	withOrder           string

	// withPrngValues is used to switch the ID generation to a pseudo-random mode
	withPrngValues []string

	withGormFormatter           hclog.Logger
	withMaxOpenConnections      int
	withMaxIdleConnections      *int
	withConnMaxIdleTimeDuration *time.Duration

	// withDebug indicates that the given operation should invoke Gorm's debug
	// mode
	withDebug bool

	withOnConflict   *OnConflict
	withRowsAffected *int64

	withTable string
}

type oplogOpts struct {
	wrapper  wrapping.Wrapper
	metadata oplog.Metadata
}

func getDefaultOptions() Options {
	return Options{
		withOplog: false,
		oplogOpts: oplogOpts{
			wrapper:  nil,
			metadata: oplog.Metadata{},
		},
		withLookup:                  false,
		WithFieldMaskPaths:          []string{},
		WithNullPaths:               []string{},
		WithLimit:                   0,
		WithVersion:                 nil,
		withMaxIdleConnections:      nil,
		withConnMaxIdleTimeDuration: nil,
	}
}

// WithTable provides an optional table name for the operation.
func WithTable(name string) Option {
	return func(o *Options) {
		o.withTable = name
	}
}

// WithLookup enables a lookup.
func WithLookup(enable bool) Option {
	return func(o *Options) {
		o.withLookup = enable
	}
}

// WithOplog provides an option to write an oplog entry. WithOplog and
// NewOplogMsg cannot be used together.
func WithOplog(wrapper wrapping.Wrapper, md oplog.Metadata) Option {
	return func(o *Options) {
		o.withOplog = true
		o.oplogOpts = oplogOpts{
			wrapper:  wrapper,
			metadata: md,
		}
	}
}

// NewOplogMsg provides an option to ask for a new in-memory oplog message.  The
// new msg will be returned in the provided *oplog.Message parameter. WithOplog
// and NewOplogMsg cannot be used together.
func NewOplogMsg(msg *oplog.Message) Option {
	return func(o *Options) {
		o.newOplogMsg = msg
	}
}

// NewOplogMsgs provides an option to ask for multiple new in-memory oplog
// messages.  The new msgs will be returned in the provided *[]oplog.Message
// parameter. NewOplogMsgs can only be used with write functions that operate on
// multiple items(CreateItems, DeleteItems). WithOplog and NewOplogMsgs cannot
// be used together.
func NewOplogMsgs(msgs *[]*oplog.Message) Option {
	return func(o *Options) {
		o.newOplogMsgs = msgs
	}
}

// WithFieldMaskPaths provides an option to provide field mask paths.
func WithFieldMaskPaths(paths []string) Option {
	return func(o *Options) {
		o.WithFieldMaskPaths = paths
	}
}

// WithNullPaths provides an option to provide null paths.
func WithNullPaths(paths []string) Option {
	return func(o *Options) {
		o.WithNullPaths = paths
	}
}

// WithLimit provides an option to provide a limit.  Intentionally allowing
// negative integers.   If WithLimit < 0, then unlimited results are returned.
// If WithLimit == 0, then default limits are used for results.
func WithLimit(limit int) Option {
	return func(o *Options) {
		o.WithLimit = limit
	}
}

// WithVersion provides an option version number for update operations.
func WithVersion(version *uint32) Option {
	return func(o *Options) {
		o.WithVersion = version
	}
}

// WithSkipVetForWrite provides an option to allow skipping vet checks to allow
// testing lower-level SQL triggers and constraints
func WithSkipVetForWrite(enable bool) Option {
	return func(o *Options) {
		o.withSkipVetForWrite = enable
	}
}

// WithWhere provides an option to provide a where clause with arguments for an
// operation.
func WithWhere(whereClause string, args ...any) Option {
	return func(o *Options) {
		o.withWhereClause = whereClause
		o.withWhereClauseArgs = append(o.withWhereClauseArgs, args...)
	}
}

// WithOrder provides an option to provide an order when searching and looking
// up.
func WithOrder(withOrder string) Option {
	return func(o *Options) {
		o.withOrder = withOrder
	}
}

// WithPrngValues provides an option to provide values to seed an PRNG when generating IDs
func WithPrngValues(withPrngValues []string) Option {
	return func(o *Options) {
		o.withPrngValues = withPrngValues
	}
}

// WithGormFormatter specifies an optional hclog to use for gorm's log
// formmater
func WithGormFormatter(l hclog.Logger) Option {
	return func(o *Options) {
		o.withGormFormatter = l
	}
}

// WithMaxOpenConnections specifies an optional max open connections for the
// database
func WithMaxOpenConnections(max int) Option {
	return func(o *Options) {
		o.withMaxOpenConnections = max
	}
}

// WithMaxIdleConnections specifies an optional max idle connections for the
// database.
// This corresponds with: https://pkg.go.dev/database/sql#DB.SetMaxIdleConns
func WithMaxIdleConnections(max *int) Option {
	return func(o *Options) {
		o.withMaxIdleConnections = max
	}
}

// WithConnMaxIdleTimeDuration specifies an optional connection max idle time
// for the database.
// This corresponds with: https://pkg.go.dev/database/sql#DB.SetConnMaxIdleTime
func WithConnMaxIdleTimeDuration(max *time.Duration) Option {
	return func(o *Options) {
		o.withConnMaxIdleTimeDuration = max
	}
}

// WithDebug specifies the given operation should invoke debug mode in Gorm
func WithDebug(with bool) Option {
	return func(o *Options) {
		o.withDebug = with
	}
}

// WithOnConflict specifies an optional on conflict criteria which specify
// alternative actions to take when an insert results in a unique constraint or
// exclusion constraint error
func WithOnConflict(onConflict *OnConflict) Option {
	return func(o *Options) {
		o.withOnConflict = onConflict
	}
}

// WithReturnRowsAffected specifies an option for returning the rows affected
func WithReturnRowsAffected(rowsAffected *int64) Option {
	return func(o *Options) {
		o.withRowsAffected = rowsAffected
	}
}
