package statement

import (
	"context"

	"github.com/chaisql/chai/internal/database"
	"github.com/chaisql/chai/internal/environment"
	"github.com/chaisql/chai/internal/expr"
	"github.com/cockroachdb/errors"
)

// A Statement represents a unique action that can be executed against the database.
type Statement interface {
	Run(*Context) (*Result, error)
}

// Optional interface that allows a statement to specify if it is read-only.
// Defaults to false if not implemented.
type ReadOnly interface {
	IsReadOnly() bool
}

// Optional interface that allows a statement to specify if they need a transaction.
// Defaults to true if not implemented.
// If true, the engine will auto-commit.
type Transactional interface {
	NeedsTransaction() bool
}

// Optional interface that allows a statement to specify if they need to be bound to database
// objects.
type Bindable interface {
	Bind(*Context) error
}

type Context struct {
	DB     *database.Database
	Conn   *database.Connection
	Params []environment.Param
}

type Preparer interface {
	Prepare(*Context) (Statement, error)
}

// Result of a query.
type Result struct {
	Result database.Result
	Tx     *database.Transaction
	closed bool
	err    error
}

func (r *Result) Iterator() (database.Iterator, error) {
	if r.Result == nil {
		return nil, nil
	}

	return r.Result.Iterator()
}

func (r *Result) Iterate(fn func(r database.Row) error) (err error) {
	if r.Result == nil {
		return nil
	}
	defer func() {
		if err != nil {
			r.err = err
		}
	}()

	it, err := r.Result.Iterator()
	if err != nil {
		return err
	}
	defer it.Close()

	for it.Next() {
		rr, err := it.Row()
		if err != nil {
			return err
		}
		if err := fn(rr); err != nil {
			return err
		}
	}
	return it.Error()
}

// Skip iterates over the result and skips all rows.
// It is useful when you need the query to be executed
// but don't care about the results.
func (r *Result) Skip(ctx context.Context) (err error) {
	if r == nil {
		return nil
	}
	if r.Result == nil {
		return nil
	}
	defer func() {
		if err != nil {
			r.err = err
		}
	}()

	it, err := r.Result.Iterator()
	if err != nil {
		return err
	}
	defer it.Close()

	for it.Next() {
		if err := ctx.Err(); err != nil {
			return err
		}
	}

	return it.Error()
}

func (r *Result) Columns() ([]string, error) {
	if r.Result == nil {
		return nil, nil
	}

	stmt, ok := r.Result.(*StreamStmtResult)
	if !ok || stmt.Stream.Op == nil {
		return nil, nil
	}

	env := environment.New(stmt.Context.DB, stmt.Context.Conn.GetTx(), stmt.Context.Params, nil)
	return stmt.Stream.Columns(env)
}

// Close the result stream.
// After closing the result, Stream is not supposed to be used.
// If the result stream was already closed, it returns an error.
func (r *Result) Close() (err error) {
	if r == nil {
		return nil
	}

	if r.closed {
		return errors.New("result already closed")
	}

	r.closed = true

	if r.Tx != nil {
		if r.Tx.Writable && r.err == nil {
			err = r.Tx.Commit()
		} else {
			err = r.Tx.Rollback()
		}
	}

	return err
}

func BindExpr(ctx *Context, tableName string, e expr.Expr) (err error) {
	if e == nil {
		return nil
	}

	var info *database.TableInfo
	if tableName != "" {
		info, err = ctx.Conn.GetTx().Catalog.GetTableInfo(tableName)
		if err != nil {
			return err
		}
	}

	expr.Walk(e, func(e expr.Expr) bool {
		switch t := e.(type) {
		case *expr.Column:
			if t == nil {
				return true
			}

			if info == nil {
				err = errors.New("no table specified")
				return false
			}

			cc := info.ColumnConstraints.GetColumnConstraint(t.Name)
			if cc == nil {
				err = errors.Newf("column %s does not exist", t)
				return false
			}
			t.Table = tableName
			t.Type = cc.Type
		}

		return true
	})

	return err
}
