package jet

import (
	"context"
	"database/sql"
	"errors"
	"github.com/go-jet/jet/execution"
	"github.com/go-jet/jet/internal/utils"
)

// InsertStatement is interface for SQL INSERT statements
type InsertStatement interface {
	Statement

	// Insert row of values
	VALUES(value interface{}, values ...interface{}) InsertStatement
	// Insert row of values, where value for each column is extracted from filed of structure data.
	// If data is not struct or there is no field for every column selected, this method will panic.
	MODEL(data interface{}) InsertStatement

	MODELS(data interface{}) InsertStatement

	QUERY(selectStatement SelectStatement) InsertStatement

	RETURNING(projections ...projection) InsertStatement
}

func newInsertStatement(t WritableTable, columns []column) InsertStatement {
	return &insertStatementImpl{
		table:   t,
		columns: columns,
	}
}

type insertStatementImpl struct {
	table     WritableTable
	columns   []column
	rows      [][]clause
	query     SelectStatement
	returning []projection
}

func (i *insertStatementImpl) VALUES(value interface{}, values ...interface{}) InsertStatement {
	i.rows = append(i.rows, unwindRowFromValues(value, values))
	return i
}

func (i *insertStatementImpl) MODEL(data interface{}) InsertStatement {
	i.rows = append(i.rows, unwindRowFromModel(i.getColumns(), data))
	return i
}

func (i *insertStatementImpl) MODELS(data interface{}) InsertStatement {
	i.rows = append(i.rows, unwindRowsFromModels(i.getColumns(), data)...)
	return i
}

func (i *insertStatementImpl) RETURNING(projections ...projection) InsertStatement {
	i.returning = projections
	return i
}

func (i *insertStatementImpl) QUERY(selectStatement SelectStatement) InsertStatement {
	i.query = selectStatement
	return i
}

func (i *insertStatementImpl) getColumns() []column {
	if len(i.columns) > 0 {
		return i.columns
	}

	return i.table.columns()
}

func (i *insertStatementImpl) DebugSql() (query string, err error) {
	return debugSql(i)
}

func (i *insertStatementImpl) Sql() (sql string, args []interface{}, err error) {
	queryData := &sqlBuilder{}

	queryData.newLine()
	queryData.writeString("INSERT INTO")

	if utils.IsNil(i.table) {
		return "", nil, errors.New("jet: table is nil")
	}

	err = i.table.serialize(insertStatement, queryData)

	if err != nil {
		return
	}

	if len(i.columns) > 0 {
		queryData.writeString("(")

		err = serializeColumnNames(i.columns, queryData)

		if err != nil {
			return
		}

		queryData.writeString(")")
	}

	if len(i.rows) == 0 && i.query == nil {
		return "", nil, errors.New("jet: no row values or query specified")
	}

	if len(i.rows) > 0 && i.query != nil {
		return "", nil, errors.New("jet: only row values or query has to be specified")
	}

	if len(i.rows) > 0 {
		queryData.writeString("VALUES")

		for rowIndex, row := range i.rows {
			if rowIndex > 0 {
				queryData.writeString(",")
			}

			queryData.increaseIdent()
			queryData.newLine()
			queryData.writeString("(")

			err = serializeClauseList(insertStatement, row, queryData)

			if err != nil {
				return "", nil, err
			}

			queryData.writeByte(')')
			queryData.decreaseIdent()
		}
	}

	if i.query != nil {
		err = i.query.serialize(insertStatement, queryData)

		if err != nil {
			return
		}
	}

	if err = queryData.writeReturning(insertStatement, i.returning); err != nil {
		return
	}

	sql, args = queryData.finalize()

	return
}

func (i *insertStatementImpl) Query(db execution.DB, destination interface{}) error {
	return query(i, db, destination)
}

func (i *insertStatementImpl) QueryContext(context context.Context, db execution.DB, destination interface{}) error {
	return queryContext(context, i, db, destination)
}

func (i *insertStatementImpl) Exec(db execution.DB) (res sql.Result, err error) {
	return exec(i, db)
}

func (i *insertStatementImpl) ExecContext(context context.Context, db execution.DB) (res sql.Result, err error) {
	return execContext(context, i, db)
}
