package checkers

import (
	"context"
	"database/sql"
	"fmt"
)

//go:generate counterfeiter -o ../fakes/isqlpinger.go . SQLPinger
//go:generate counterfeiter -o ../fakes/isqlqueryer.go . SQLQueryer
//go:generate counterfeiter -o ../fakes/isqlexecer.go . SQLExecer

// SQLPinger is an interface that allows direct pinging of the database
type SQLPinger interface {
	PingContext(ctx context.Context) error
}

// SQLQueryer is an interface that allows querying of the database
type SQLQueryer interface {
	QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
}

// SQLExecer is an interface that allows executing of queries in the database
type SQLExecer interface {
	ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
}

// SQLQueryerResultHandler is the BYO function to
// handle the result of an SQL SELECT query
type SQLQueryerResultHandler func(rows *sql.Rows) (bool, error)

// SQLExecerResultHandler is the BYO function
// to handle a database exec result
type SQLExecerResultHandler func(result sql.Result) (bool, error)

// SQLConfig is used for configuring a database check.
// One of the Pinger, Queryer, or Execer fields is required.
//
//
// If Execer is set, it will take precedence over Queryer and Pinger,
// Execer implements the SQLExecer interface in this package.
// The sql.DB and sql.TX structs both implement this interface.
//
// Note that if the Execer is set, then the ExecerResultHandler
// and Query values MUST also be set
//
//
// If Queryer is set, it will take precedence over Pinger.
// SQLQueryer implements the SQLQueryer interface in this package.
// The sql.DB and sql.TX structs both implement this interface.
//
// Note that if the Queryer is set, then the QueryerResultHandler
// and Query values MUST also be set
//
//
// Pinger implements the SQLPinger interface in this package.
// The sql.DB struct implements this interface.
type SQLConfig struct {
	// Pinger is the value implementing SQLPinger
	Pinger SQLPinger

	// Queryer is the value implementing SQLQueryer
	Queryer SQLQueryer

	// Execer is the value implementing SQLExecer
	Execer SQLExecer

	// Query is the parameterized SQL query required
	// with both Queryer and Execer
	Query string

	// Params are the SQL query parameters, if any
	Params []interface{}

	// QueryerResultHandler handles the result of
	// the QueryContext function
	QueryerResultHandler SQLQueryerResultHandler

	// ExecerResultHandler handles the result of
	// the ExecContext function
	ExecerResultHandler SQLExecerResultHandler
}

// SQL implements the "ICheckable" interface
type SQL struct {
	Config *SQLConfig
}

// NewSQL creates a new database checker that can be used for ".AddCheck(s)".
func NewSQL(cfg *SQLConfig) (*SQL, error) {
	if err := validateSQLConfig(cfg); err != nil {
		return nil, err
	}

	return &SQL{
		Config: cfg,
	}, nil
}

// DefaultQueryHandler is the default SQLQueryer result handler
// that assumes one row was returned from the passed query
func DefaultQueryHandler(rows *sql.Rows) (bool, error) {
	defer rows.Close()

	numRows := 0
	for rows.Next() {
		numRows++
	}

	return numRows == 1, nil
}

// DefaultExecHandler is the default SQLExecer result handler
// that assumes one row was affected in the passed query
func DefaultExecHandler(result sql.Result) (bool, error) {
	affectedRows, err := result.RowsAffected()
	if err != nil {
		return false, err
	}

	return affectedRows == int64(1), nil
}

// this makes sure the sql check is properly configured
func validateSQLConfig(cfg *SQLConfig) error {
	if cfg == nil {
		return fmt.Errorf("config is required")
	}

	if cfg.Execer == nil && cfg.Queryer == nil && cfg.Pinger == nil {
		return fmt.Errorf("one of Execer, Queryer, or Pinger is required in SQLConfig")
	}

	if (cfg.Execer != nil || cfg.Queryer != nil) && len(cfg.Query) == 0 {
		return fmt.Errorf("SQLConfig.Query is required")
	}

	return nil
}

// Status is used for performing a database ping against a dependency; it satisfies
// the "ICheckable" interface.
func (s *SQL) Status() (interface{}, error) {
	if err := validateSQLConfig(s.Config); err != nil {
		return nil, err
	}

	switch {
	// check for SQLExecer first
	case s.Config.Execer != nil:
		// if the result handler is nil, use the default
		if s.Config.ExecerResultHandler == nil {
			s.Config.ExecerResultHandler = DefaultExecHandler
		}
		// run the execer
		return s.runExecer()
	// check for SQLQueryer next
	case s.Config.Queryer != nil:
		// if the result handler is nil, use the default
		if s.Config.QueryerResultHandler == nil {
			s.Config.QueryerResultHandler = DefaultQueryHandler
		}
		// run the queryer
		return s.runQueryer()
	// finally, must be a pinger
	default:
		ctx := context.Background()
		return nil, s.Config.Pinger.PingContext(ctx)
	}
}

// This will run the execer from the Status func
func (s *SQL) runExecer() (interface{}, error) {
	ctx := context.Background()
	result, err := s.Config.Execer.ExecContext(ctx, s.Config.Query, s.Config.Params...)
	if err != nil {
		return nil, err
	}

	ok, err := s.Config.ExecerResultHandler(result)
	if err != nil {
		return nil, err
	}

	if !ok {
		return nil, fmt.Errorf("userland exec result handler returned false")
	}

	return nil, nil
}

// This will run the queryer from the Status func
func (s *SQL) runQueryer() (interface{}, error) {
	ctx := context.Background()
	rows, err := s.Config.Queryer.QueryContext(ctx, s.Config.Query, s.Config.Params...)
	if err != nil {
		return nil, err
	}

	// the BYO result handler is responsible for closing the rows

	ok, err := s.Config.QueryerResultHandler(rows)
	if err != nil {
		return nil, err
	}

	if !ok {
		return nil, fmt.Errorf("userland query result handler returned false")
	}

	return nil, nil
}
