// Copyright 2021 Molecula Corp. All rights reserved.

package planner

import (
	"strconv"
	"strings"
	"time"

	pilosa "github.com/featurebasedb/featurebase/v3"
	"github.com/featurebasedb/featurebase/v3/dax"
	"github.com/featurebasedb/featurebase/v3/pql"
	"github.com/featurebasedb/featurebase/v3/sql3"
	"github.com/featurebasedb/featurebase/v3/sql3/parser"
	"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
)

type createTableField struct {
	planner  *ExecutionPlanner
	name     string
	typeName string
	fos      []pilosa.FieldOption
}

// compileCreateTableStatement compiles a CREATE TABLE statement into a
// PlanOperator.
func (p *ExecutionPlanner) compileCreateTableStatement(stmt *parser.CreateTableStatement) (_ types.PlanOperator, err error) {
	tableName := parser.IdentName(stmt.Name)
	failIfExists := !stmt.IfNotExists.IsValid()

	// apply table options
	keyPartitions := 0
	description := ""
	for _, option := range stmt.Options {
		switch o := option.(type) {
		case *parser.KeyPartitionsOption:
			e := o.Expr.(*parser.IntegerLit)
			i, err := strconv.ParseInt(e.Value, 10, 64)
			if err != nil {
				return nil, err
			}
			keyPartitions = int(i)
		case *parser.CommentOption:
			e := o.Expr.(*parser.StringLit)
			description = e.Value
		}
	}

	isKeyed := false

	var columns = []*createTableField{}
	for _, col := range stmt.Columns {
		columnName := parser.IdentName(col.Name)
		typeName := parser.IdentName(col.Type.Name)

		if strings.ToLower(columnName) == "_id" {
			if strings.EqualFold(typeName, dax.BaseTypeString) {
				isKeyed = true
			}
			continue
		}

		column, err := p.compileColumn(col)
		if err != nil {
			return nil, err
		}

		columns = append(columns, column)
	}
	cop := NewPlanOpCreateTable(p, tableName, failIfExists, isKeyed, keyPartitions, description, columns)
	if keyPartitions > 0 {
		cop.AddWarning("The value of KEYPARTITIONS is currently ignored")
	}
	return NewPlanOpQuery(p, cop, p.sql), nil
}

// compiles a column def
func (p *ExecutionPlanner) compileColumn(col *parser.ColumnDefinition) (*createTableField, error) {
	var err error
	columnName := parser.IdentName(col.Name)
	typeName := parser.IdentName(col.Type.Name)

	column := &createTableField{
		planner:  p,
		name:     columnName,
		typeName: typeName,
	}
	// Possible FieldOptions. We define these here, but the contraints below
	// can set them to the values provided in the create table statement.
	// And finally, the correct pilosa.FieldOption functional option will be
	// created after that.
	var cacheType string = pilosa.DefaultCacheType
	var cacheSize uint32 = pilosa.DefaultCacheSize
	var scale int64
	min, max := pql.MinMax(0)
	var epoch = pilosa.DefaultEpoch
	var timeUnit string = pilosa.TimeUnitSeconds
	var timeQuantum pilosa.TimeQuantum
	var ttl = "0"

	for _, con := range col.Constraints {
		switch c := con.(type) {
		case *parser.CacheTypeConstraint:
			cacheType = c.CacheTypeValue

			if c.Size.IsValid() {
				e := c.SizeExpr.(*parser.IntegerLit)
				i, err := strconv.ParseInt(e.Value, 10, 64)
				if err != nil {
					return nil, err
				}
				cacheSize = uint32(i)
			}

		case *parser.MinConstraint:
			var val string
			switch e := c.Expr.(type) {
			case *parser.IntegerLit:
				val = e.Value

			case *parser.UnaryExpr:
				// Call analyzeUnaryExpression() in order to set the value on
				// expr.ResultDataType so that we can rely on the DataType()
				// method. There is a case where a BITNOT expression could get
				// through, but that value as a string will fail in
				// strconv.ParseInt() conversion.
				if _, err := p.analyzeUnaryExpression(e, nil); err != nil {
					return nil, err
				}

				if e.IsLiteral() && typeIsInteger(e.DataType()) {
					val = e.String()
				}
			}

			i, err := strconv.ParseInt(val, 10, 64)
			if err != nil {
				return nil, err
			}
			min = pql.NewDecimal(i, 0)

		case *parser.MaxConstraint:
			var val string
			switch e := c.Expr.(type) {
			case *parser.IntegerLit:
				val = e.Value

			case *parser.UnaryExpr:
				// Call analyzeUnaryExpression() in order to set the value on
				// expr.ResultDataType so that we can rely on the DataType()
				// method. There is a case where a BITNOT expression could get
				// through, but that value as a string will fail in
				// strconv.ParseInt() conversion.
				if _, err := p.analyzeUnaryExpression(e, nil); err != nil {
					return nil, err
				}

				if e.IsLiteral() && typeIsInteger(e.DataType()) {
					val = e.String()
				}
			}

			i, err := strconv.ParseInt(val, 10, 64)
			if err != nil {
				return nil, err
			}
			max = pql.NewDecimal(i, 0)

		case *parser.TimeUnitConstraint:
			unit := c.Expr.(*parser.StringLit)
			timeUnit = unit.Value

			if c.EpochExpr != nil {
				epochString := c.EpochExpr.(*parser.StringLit)
				tm, err := time.ParseInLocation(time.RFC3339, epochString.Value, time.UTC)
				if err != nil {
					return nil, sql3.NewErrInvalidTimeEpoch(c.EpochExpr.Pos().Line, c.EpochExpr.Pos().Line, epochString.Value)
				}
				epoch = tm
			}

		case *parser.TimeQuantumConstraint:
			unit := c.Expr.(*parser.StringLit)
			timeQuantum = pilosa.TimeQuantum(unit.Value)
			if c.TtlExpr != nil {
				e := c.TtlExpr.(*parser.StringLit)
				ttl = e.Value
			}

		default:
			return nil, sql3.NewErrInternalf("unhandled column constraint type '%T'", c)
		}
	}

	switch strings.ToLower(typeName) {
	case dax.BaseTypeBool:
		column.fos = append(column.fos, pilosa.OptFieldTypeBool())
	case dax.BaseTypeDecimal:

		// if we don't have a scale, it's an error
		if col.Type.Scale == nil {
			return nil, sql3.NewErrDecimalScaleExpected(col.Type.Name.NamePos.Line, col.Type.Name.NamePos.Column)
		}

		// get the scale value
		scale, err = strconv.ParseInt(col.Type.Scale.Value, 10, 64)
		if err != nil {
			return nil, err
		}

		// Adjust min/max to fit within the scaled min/max.
		scaledMin, scaledMax := pql.MinMax(scale)
		if scaledMax.LessThan(max) {
			max = scaledMax
		}
		if scaledMin.GreaterThan(min) {
			min = scaledMin
		}

		column.fos = append(column.fos, pilosa.OptFieldTypeDecimal(scale, min, max))
	case dax.BaseTypeID:
		column.fos = append(column.fos, pilosa.OptFieldTypeMutex(cacheType, cacheSize))
	case dax.BaseTypeIDSet:
		if timeQuantum != "" {
			column.fos = append(column.fos, pilosa.OptFieldTypeTime(timeQuantum, ttl))
		} else {
			column.fos = append(column.fos, pilosa.OptFieldTypeSet(cacheType, cacheSize))
		}
	case dax.BaseTypeInt:
		column.fos = append(column.fos, pilosa.OptFieldTypeInt(min.ToInt64(0), max.ToInt64(0)))
	case dax.BaseTypeString:
		column.fos = append(column.fos, pilosa.OptFieldTypeMutex(cacheType, cacheSize))
		column.fos = append(column.fos, pilosa.OptFieldKeys())
	case dax.BaseTypeStringSet:
		if timeQuantum != "" {
			column.fos = append(column.fos, pilosa.OptFieldTypeTime(timeQuantum, ttl))
		} else {
			column.fos = append(column.fos, pilosa.OptFieldTypeSet(cacheType, cacheSize))
		}
		column.fos = append(column.fos, pilosa.OptFieldKeys())
	case dax.BaseTypeTimestamp:
		column.fos = append(column.fos, pilosa.OptFieldTypeTimestamp(epoch, timeUnit))
	}
	return column, nil
}

// analyzeCreateTableStatement analyzes a CREATE TABLE statement and returns an
// error if anything is invalid.
func (p *ExecutionPlanner) analyzeCreateTableStatement(stmt *parser.CreateTableStatement) error {
	//iterate columns, check types, check constraints, ensure we have no dupe names and make sure there is an _id column
	checkedColumns := make(map[string]string)
	for _, col := range stmt.Columns {
		columnName := parser.IdentName(col.Name)
		_, ok := checkedColumns[strings.ToLower(columnName)]
		if ok {
			return sql3.NewErrDuplicateColumn(col.Name.NamePos.Line, col.Name.NamePos.Column, columnName)
		}

		typeName := parser.IdentName(col.Type.Name)
		if !parser.IsValidTypeName(typeName) {
			return sql3.NewErrUnknownType(col.Type.Name.NamePos.Line, col.Type.Name.NamePos.Column, typeName)
		}

		if strings.ToLower(columnName) == "_id" {
			//check the type
			if !(strings.EqualFold(typeName, dax.BaseTypeID) || strings.EqualFold(typeName, dax.BaseTypeString)) {
				return sql3.NewErrTableIDColumnType(col.Type.Name.NamePos.Line, col.Type.Name.NamePos.Column)
			}
			//make sure we have no constraints
			if len(col.Constraints) > 0 {
				return sql3.NewErrTableIDColumnConstraints(col.Type.Name.NamePos.Line, col.Type.Name.NamePos.Column)
			}
		}
		checkedColumns[columnName] = strings.ToLower(columnName)

		err := p.analyzeColumn(typeName, col)
		if err != nil {
			return err
		}
	}
	_, ok := checkedColumns["_id"]
	if !ok {
		return sql3.NewErrTableMustHaveIDColumn(stmt.Create.Line, stmt.Create.Column)
	}
	//check table options
	for _, option := range stmt.Options {

		switch o := option.(type) {
		case *parser.KeyPartitionsOption:
			//check the type of the expression
			literal, ok := o.Expr.(*parser.IntegerLit)
			if !ok {
				return sql3.NewErrIntegerLiteral(o.Expr.Pos().Line, o.Expr.Pos().Column)
			}
			//key partittions needs to be >=1 and we'll cap conservatively at 10000
			i, err := strconv.ParseInt(literal.Value, 10, 64)
			if err != nil {
				return err
			}
			if i < 1 || i > 10000 {
				return sql3.NewErrInvalidKeyPartitionsValue(o.Expr.Pos().Line, o.Expr.Pos().Column, i)
			}

		case *parser.CommentOption:

			_, ok := o.Expr.(*parser.StringLit)
			if !ok {
				return sql3.NewErrStringLiteral(o.Expr.Pos().Line, o.Expr.Pos().Column)
			}

		default:
			return sql3.NewErrInternalf("unhandled table option type '%T'", option)
		}
	}

	return nil
}

// analyze the column def for a CREATE or ALTER TABLE
func (p *ExecutionPlanner) analyzeColumn(typeName string, col *parser.ColumnDefinition) error {
	// handledConstraints keeps track of the constraints which have been
	// analyzed in the for loop below. This allows us to verify that two
	// different, incompatible constraints aren't included. For now, that
	// really only applies to TIMEQUANTUM and CACHETYPE. The other
	// constraints which may be incompatible are checked against the field
	// type. It may make sense, in the future, to add some logic which
	// analyzes all the constraints in a more flexible way, but this
	// addresses the immediate issue.
	handledConstraints := make(map[parser.Token]struct{})

	for _, con := range col.Constraints {
		switch c := con.(type) {
		case *parser.CacheTypeConstraint:
			//make sure we have a set or mutex type
			if !(strings.EqualFold(typeName, dax.BaseTypeString) || strings.EqualFold(typeName, dax.BaseTypeStringSet) || strings.EqualFold(typeName, dax.BaseTypeID) || strings.EqualFold(typeName, dax.BaseTypeIDSet)) {
				return sql3.NewErrBadColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, "CACHETYPE", typeName)
			}
			//check the type of the expression for SIZE
			if c.Size.IsValid() {
				if _, ok := c.SizeExpr.(*parser.IntegerLit); !ok {
					return sql3.NewErrIntegerLiteral(c.SizeExpr.Pos().Line, c.SizeExpr.Pos().Column)
				}
			}
			// Make sure a TIMEQUANTUM constraint (which is incompatible with
			// CACHETYPE) hasn't been specified.
			if _, ok := handledConstraints[parser.TIMEQUANTUM]; ok {
				return sql3.NewErrConflictingColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, parser.CACHETYPE, parser.TIMEQUANTUM)
			}
			handledConstraints[parser.CACHETYPE] = struct{}{}

		case *parser.MinConstraint:
			// Make sure we have either an integer or unary type.
			switch c.Expr.(type) {
			case *parser.IntegerLit, *parser.UnaryExpr:
				// pass
			default:
				return sql3.NewErrBadColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, "MIN", typeName)
			}
			handledConstraints[parser.MIN] = struct{}{}

		case *parser.MaxConstraint:
			// Make sure we have either an integer or unary type.
			switch c.Expr.(type) {
			case *parser.IntegerLit, *parser.UnaryExpr:
				// pass
			default:
				return sql3.NewErrBadColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, "MAX", typeName)
			}
			handledConstraints[parser.MAX] = struct{}{}

		case *parser.TimeUnitConstraint:
			//make sure we have an timestamp type
			if !strings.EqualFold(typeName, dax.BaseTypeTimestamp) {
				return sql3.NewErrBadColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, "TIMEUNIT", typeName)
			}
			//check the type of the expression
			unit, ok := c.Expr.(*parser.StringLit)
			if !ok {
				return sql3.NewErrStringLiteral(c.Expr.Pos().Line, c.Expr.Pos().Column)
			}
			if !pilosa.IsValidTimeUnit(unit.Value) {
				return sql3.NewErrInvalidTimeUnit(c.Expr.Pos().Line, c.Expr.Pos().Column, unit.Value)
			}
			if c.EpochExpr != nil {
				//check the type of the expression
				_, ok := c.EpochExpr.(*parser.StringLit)
				if !ok {
					return sql3.NewErrStringLiteral(c.EpochExpr.Pos().Line, c.EpochExpr.Pos().Column)
				}
			}
			handledConstraints[parser.TIMEUNIT] = struct{}{}

		case *parser.TimeQuantumConstraint:
			//make sure we have a set type
			if !(strings.EqualFold(typeName, dax.BaseTypeStringSet) || strings.EqualFold(typeName, dax.BaseTypeIDSet)) {
				return sql3.NewErrBadColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, "TIMEQUANTUM", typeName)
			}
			//check the type of the expression
			unit, ok := c.Expr.(*parser.StringLit)
			if !ok {
				return sql3.NewErrStringLiteral(c.Expr.Pos().Line, c.Expr.Pos().Column)
			}
			quantum := pilosa.TimeQuantum(strings.ToUpper(unit.Value))
			if !quantum.Valid() {
				return sql3.NewErrInvalidTimeQuantum(c.Expr.Pos().Line, c.Expr.Pos().Column, unit.Value)
			}
			if c.TtlExpr != nil {
				//check the type of the expression
				ttl, ok := c.TtlExpr.(*parser.StringLit)
				if !ok {
					return sql3.NewErrStringLiteral(c.Expr.Pos().Line, c.Expr.Pos().Column)
				}
				_, err := time.ParseDuration(ttl.Value)
				if err != nil {
					return sql3.NewErrInvalidDuration(c.Expr.Pos().Line, c.Expr.Pos().Column, ttl.Value)
				}
			}

			// Make sure a CACHETYPE constraint (which is incompatible with
			// TIMEQUANTUM) hasn't been specified.
			if _, ok := handledConstraints[parser.CACHETYPE]; ok {
				return sql3.NewErrConflictingColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, parser.CACHETYPE, parser.TIMEQUANTUM)
			}

			handledConstraints[parser.TIMEQUANTUM] = struct{}{}

		default:
			return sql3.NewErrInternalf("unhandled column constraint type '%T'", c)
		}
	}
	return nil
}
