// Package queryer provides the core query-related structs.
package queryer

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync"
	"time"

	featurebase "github.com/featurebasedb/featurebase/v3"
	fbcontext "github.com/featurebasedb/featurebase/v3/context"
	"github.com/featurebasedb/featurebase/v3/dax"
	"github.com/featurebasedb/featurebase/v3/encoding/proto"
	"github.com/featurebasedb/featurebase/v3/errors"
	idkserverless "github.com/featurebasedb/featurebase/v3/idk/serverless"
	"github.com/featurebasedb/featurebase/v3/logger"
	featurebase_pql "github.com/featurebasedb/featurebase/v3/pql"
	fbproto "github.com/featurebasedb/featurebase/v3/proto"
	"github.com/featurebasedb/featurebase/v3/server"
	"github.com/featurebasedb/featurebase/v3/sql3/parser"
	"github.com/featurebasedb/featurebase/v3/sql3/planner"
	plannertypes "github.com/featurebasedb/featurebase/v3/sql3/planner/types"
	"github.com/featurebasedb/featurebase/v3/systemlayer"
	uuid "github.com/satori/go.uuid"
)

// Queryer represents the query layer in a Molecula implementation. The idea is
// that the externally-facing Molecula API would proxy query requests to a pool
// of "Queryer" nodes, which handle incoming query requests.
type Queryer struct {
	mu            sync.RWMutex
	orchestrators map[dax.QualifiedDatabaseID]*qualifiedOrchestrator

	fbClient *featurebase.InternalClient

	controller dax.Controller

	systemLayer *systemlayer.SystemLayer

	logger logger.Logger
}

// New returns a new instance of Queryer.
func New(cfg Config) *Queryer {
	q := &Queryer{
		controller:    dax.NewNopController(),
		orchestrators: make(map[dax.QualifiedDatabaseID]*qualifiedOrchestrator),
		systemLayer:   systemlayer.NewSystemLayer(),
		logger:        logger.NopLogger,
	}

	if cfg.Logger != nil {
		q.logger = cfg.Logger
	}

	return q
}

func (q *Queryer) Logger() logger.Logger {
	return q.logger
}

// Orchestrator gets (or creates) an instance of qualifiedOrchestrator based on
// the provided dax.QualifiedDatabaseID.
func (q *Queryer) Orchestrator(qdbid dax.QualifiedDatabaseID) *qualifiedOrchestrator {
	// Try to get orchestrator under a read lock first.
	if orch := func() *qualifiedOrchestrator {
		q.mu.RLock()
		defer q.mu.RUnlock()
		if orch, ok := q.orchestrators[qdbid]; ok {
			return orch
		}
		return nil
	}(); orch != nil {
		return orch
	}

	// Since we didn't find an orchestrator under a read lock, obtain a write
	// lock and try a read/write.
	q.mu.Lock()
	defer q.mu.Unlock()
	if orch, ok := q.orchestrators[qdbid]; ok {
		return orch
	}

	sapi := newQualifiedSchemaAPI(qdbid, q.controller)

	orch := &orchestrator{
		schema:   sapi,
		trans:    NewServerlessTranslator(q.controller),
		topology: &ServerlessTopology{controller: q.controller},
		// TODO(jaffee) using default http.Client probably bad... need to set some timeouts.
		client: q.fbClient,
		logger: q.logger,
	}

	qorch := newQualifiedOrchestrator(orch, qdbid)
	q.orchestrators[qdbid] = qorch

	return qorch
}

func (q *Queryer) SetController(controller dax.Controller) error {
	q.controller = controller
	return nil
}

func (q *Queryer) Start() error {
	if q.controller == nil {
		return errors.New(errors.ErrUncoded, "queryer requires controller to be configured")
	}

	// fbClient is an instance of internal client. It's used in one place in the
	// orchestrator (o.client.QueryNode()), but in that case, the host is
	// replaces with the actual host (another computer node) to connect to.
	// That's why we set it up with a dummy host here.
	fbClient, err := featurebase.NewInternalClient("fakehostname:8080",
		&http.Client{},
		featurebase.WithSerializer(proto.Serializer{}),
		featurebase.WithPathPrefix("should-not-be-used"),
	)
	if err != nil {
		return errors.Wrap(err, "setting up internal client")
	}
	q.fbClient = fbClient

	return nil
}

func (q *Queryer) QuerySQL(ctx context.Context, qdbid dax.QualifiedDatabaseID, sql io.Reader) (*featurebase.WireQueryResponse, error) {
	start := time.Now()

	ret := &featurebase.WireQueryResponse{}

	applyExecutionTime := func() {
		ret.ExecutionTime = time.Since(start).Microseconds()
	}

	applyError := func(e error) {
		ret.Error = e.Error()
		applyExecutionTime()
	}

	// Peek at the first character of sql. If it's "[", then handle this as PQL.
	var isPQL bool
	peekSize := 1
	peeker := io.LimitReader(sql, int64(peekSize))
	peek, err := io.ReadAll(peeker)
	if err != nil {
		return nil, errors.Wrap(err, "reading peeker")
	} else if len(peek) == 1 && peek[0] == byte('[') {
		isPQL = true
	}

	// Since we already read peekSize bytes from r, we need to prepend that peek
	// data to what's left to read from r. To do that we stitch them back
	// together with an io.MultiReader.
	peekReader := bytes.NewReader(peek)
	multiReader := io.MultiReader(peekReader, sql)

	// If PQL, run that instead.
	if isPQL {
		pql, err := io.ReadAll(multiReader)
		if err != nil {
			applyError(errors.Wrap(err, "reading pql"))
			return ret, nil
		}
		if pqlResp, err := q.parseAndQueryPQL(ctx, qdbid, string(pql)); err != nil {
			applyError(errors.Wrap(err, "querying pql"))
			return ret, nil
		} else {
			ret = pqlResp
		}
		applyExecutionTime()

		return ret, nil
	}

	// Create a requestID and add it to the context.
	requestID, err := uuid.NewV4()
	if err != nil {
		applyError(errors.Wrap(err, "creating requestID"))
		return ret, nil
	}
	// put the requestId in the context
	ctx = fbcontext.WithRequestID(ctx, requestID.String())

	st, err := parser.NewParser(multiReader).ParseStatement()
	if err != nil {
		applyError(errors.Wrap(err, "parsing sql"))
		return ret, nil
	}

	// SchemaAPI
	sapi := newQualifiedSchemaAPI(qdbid, q.controller)

	// Importer
	imp := idkserverless.NewImporter(q.controller, qdbid, nil)

	// SystemAPI.
	sysapi := newSystemAPI(q.controller, qdbid)

	// We intentionally don't pass the sql argument here because we're working
	// with an io.Reader rather than a string and it's just not necessary to
	// send it as a string to this method. Also, what happens if the sql is a
	// large BULK INSERT?
	pl := planner.NewExecutionPlanner(q.Orchestrator(qdbid), sapi, sysapi, q.systemLayer, imp, q.logger, "")

	planOp, err := pl.CompilePlan(ctx, st)
	if err != nil {
		applyError(errors.Wrap(err, "compiling plan"))
		return ret, nil
	}

	// Get a query iterator.
	iter, err := planOp.Iterator(ctx, nil)
	if err != nil {
		applyError(errors.Wrap(err, "getting iterator"))
		return ret, nil
	}

	// Read schema.
	columns := planOp.Schema()
	schema := featurebase.WireQuerySchema{
		Fields: make([]*featurebase.WireQueryField, len(columns)),
	}
	for i, col := range columns {
		btype, err := dax.BaseTypeFromString(col.Type.BaseTypeName())
		if err != nil {
			applyError(errors.Wrap(err, "getting fieldtype from string"))
			return ret, nil
		}
		schema.Fields[i] = &featurebase.WireQueryField{
			Name:     dax.FieldName(col.ColumnName),
			Type:     strings.ToLower(col.Type.TypeDescription()), // TODO(tlt): remove this once sql3 uses BaseTypes.
			BaseType: btype,
			TypeInfo: col.Type.TypeInfo(),
		}
	}

	// Read rows.
	data := make([][]interface{}, 0)
	var currentRow plannertypes.Row
	for currentRow, err = iter.Next(ctx); err == nil; currentRow, err = iter.Next(ctx) {
		data = append(data, currentRow)
	}
	if err != nil && err != plannertypes.ErrNoMoreRows {
		applyError(errors.Wrap(err, "getting row"))
		return ret, nil
	}

	ret.Schema = schema
	ret.Data = data
	applyExecutionTime()

	return ret, nil
}

func (q *Queryer) parseAndQueryPQL(ctx context.Context, qdbid dax.QualifiedDatabaseID, sql string) (*featurebase.WireQueryResponse, error) {
	var i int
	for i = 1; sql[i] != ']'; i++ {
		if i == len(sql)-1 {
			return nil, errors.Errorf("couldn't parse table name out of '%s'", sql)
		}
	}
	table := sql[1:i]
	query := sql[i+1:]

	return q.queryPQL(ctx, qdbid, dax.TableName(table), query)
}

// convertIndex tries to covert any "index" specified in the call.Args map to a
// TableKeyer. Note, since the Call.CallIndex() method currently only looks for
// strings, we can't just set the value to a TableKeyer; we have to set it to
// the equivalent string and then parse it back out later. A TODO would be to
// modify Call.CallIndex() to be TableKeyer aware. I didn't do that along with
// these changes because I'm not sure if we want to introduce dax types into the
// pql package.
func (q *Queryer) convertIndex(ctx context.Context, qdbid dax.QualifiedDatabaseID, call *featurebase_pql.Call) {
	if index := call.CallIndex(); index != "" {
		qtbl, err := q.controller.TableByName(ctx, qdbid, dax.TableName(index))
		if err != nil {
			return
		}
		call.Args["index"] = string(qtbl.Key())
	}

	// Apply to children.
	for _, child := range call.Children {
		q.convertIndex(ctx, qdbid, child)
	}
}

func (q *Queryer) QueryPQL(ctx context.Context, qdbid dax.QualifiedDatabaseID, table dax.TableName, pql string) (*featurebase.WireQueryResponse, error) {
	start := time.Now()

	ret := &featurebase.WireQueryResponse{}

	applyExecutionTime := func() {
		ret.ExecutionTime = time.Since(start).Microseconds()
	}

	applyError := func(e error) {
		ret.Error = e.Error()
		applyExecutionTime()
	}

	if pqlResp, err := q.queryPQL(ctx, qdbid, table, pql); err != nil {
		applyError(errors.Wrap(err, "querying pql"))
		return ret, nil
	} else {
		ret = pqlResp
	}

	applyExecutionTime()

	return ret, nil
}

func (q *Queryer) queryPQL(ctx context.Context, qdbid dax.QualifiedDatabaseID, table dax.TableName, pql string) (*featurebase.WireQueryResponse, error) {
	// Parse the pql into a pql.Query containing []pql.Call.
	qry, err := featurebase_pql.NewParser(strings.NewReader(pql)).Parse()
	if err != nil {
		return nil, errors.Wrap(err, "parsing pql")
	}
	if len(qry.Calls) != 1 {
		return nil, errors.Errorf("must have exactly 1 query, but got: %+v", qry.Calls)
	}

	// Replace any "index" arguments within the PQL with a TableKey.
	q.convertIndex(ctx, qdbid, qry.Calls[0])

	qtbl, err := q.controller.TableByName(ctx, qdbid, dax.TableName(table))
	if err != nil {
		return nil, errors.Wrap(err, "converting index to qualified table")
	}

	results, err := q.Orchestrator(qdbid).Execute(ctx, qtbl, qry, nil, &featurebase.ExecOptions{})
	if err != nil {
		return nil, errors.Wrap(err, "orchestrator.Execute")
	}
	if len(results.Results) != 1 {
		return nil, errors.Errorf("expected single result but got %+v", results.Results)
	}

	return pqlResultToQueryResult(results.Results[0])
}

func pqlResultToQueryResult(pqlResult interface{}) (*featurebase.WireQueryResponse, error) {
	toTabler, err := server.ToTablerWrapper(pqlResult)
	if err != nil {
		return nil, errors.Wrap(err, "wrapping as type ToTabler")
	}
	table, err := toTabler.ToTable()
	if err != nil {
		return nil, errors.Wrap(err, "ToTable")
	}

	return tableResponseToQueryResult(table)
}

func tableResponseToQueryResult(t *fbproto.TableResponse) (*featurebase.WireQueryResponse, error) {
	qr := &featurebase.WireQueryResponse{
		Schema: featurebase.WireQuerySchema{Fields: make([]*featurebase.WireQueryField, len(t.Headers))},
		Data:   make([][]interface{}, len(t.Rows)),
	}
	for i, ci := range t.Headers {
		qr.Schema.Fields[i] = &featurebase.WireQueryField{
			Name:     dax.FieldName(ci.Name),
			Type:     string(datatypeToBaseType(ci.Datatype)), // TODO(tlt): this doesn't contain typeInfo
			BaseType: datatypeToBaseType(ci.Datatype),
		}
	}

	for i, row := range t.Rows {
		qr.Data[i] = rowToSliceInterface(t.Headers, row)
	}

	return qr, nil
}

func datatypeToBaseType(ciDatatype string) dax.BaseType {
	switch ciDatatype {
	case "string":
		return dax.BaseTypeString
	case "uint64":
		return dax.BaseTypeID
	case "float64":
		// ??
		panic("float64 doesn't have sql3 field type?")
	case "int64":
		return dax.BaseTypeInt
	case "bool":
		return dax.BaseTypeBool
	case "decimal":
		return dax.BaseTypeDecimal
	case "timestamp":
		return dax.BaseTypeTimestamp
	case "[]string":
		return dax.BaseTypeStringSet
	case "[]uint64":
		return dax.BaseTypeIDSet
	// TODO []byte??
	default:
		panic(fmt.Sprintf("unknown ColumnInfo Datatype: %s", ciDatatype))
	}
}

func rowToSliceInterface(header []*fbproto.ColumnInfo, row *fbproto.Row) []interface{} {
	ret := make([]interface{}, len(row.Columns))
	for i, col := range row.Columns {
		switch header[i].Datatype {
		case "string":
			ret[i] = col.GetStringVal()
		case "uint64":
			ret[i] = col.GetUint64Val()
		case "int64":
			ret[i] = col.GetInt64Val()
		case "bool":
			ret[i] = col.GetBoolVal()
		case "[]byte":
			ret[i] = col.GetBlobVal()
		case "[]uint64":
			ret[i] = col.GetUint64ArrayVal()
		case "[]string":
			ret[i] = col.GetStringArrayVal()
		case "float64":
			ret[i] = col.GetFloat64Val()
		case "decimal":
			dec := col.GetDecimalVal()
			ret[i] = featurebase_pql.NewDecimal(dec.Value, dec.Scale)
		case "timestamp":
			ret[i] = col.GetTimestampVal()
		default:
			panic(fmt.Sprintf("don't know how to get value for columninfo datatype %s, val: %+v, type: %[2]T", header[i].Datatype, col.ColumnVal))
		}
	}
	return ret
}
