// Copyright 2022 Molecula Corp. All rights reserved.

package planner

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

	pilosa "github.com/featurebasedb/featurebase/v3"
	"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"
)

// PlanOpCopy is a copy operator
type PlanOpCopy struct {
	planner     *ExecutionPlanner
	targetTable string
	url         string
	apiKey      string
	ddl         string
	ChildOp     types.PlanOperator

	warnings []string
}

func NewPlanOpCopy(planner *ExecutionPlanner, targetName string, url string, apiKey string, ddl string, child types.PlanOperator) *PlanOpCopy {
	return &PlanOpCopy{
		planner:     planner,
		targetTable: targetName,
		url:         url,
		apiKey:      apiKey,
		ddl:         ddl,
		ChildOp:     child,
		warnings:    make([]string, 0),
	}
}

func (p *PlanOpCopy) Schema() types.Schema {
	return types.Schema{}
}

func (p *PlanOpCopy) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) {
	child, err := p.ChildOp.Iterator(ctx, row)
	if err != nil {
		return nil, err
	}
	if p.url != "" {
		return newRemoteCopyIterator(p.planner, p.targetTable, p.url, p.apiKey, p.ddl, p.ChildOp.Schema(), child), nil
	}
	return newCopyIterator(p.planner, p.targetTable, p.ddl, p.ChildOp.Schema(), child), nil
}

func (p *PlanOpCopy) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) {
	if len(children) != 1 {
		return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
	}
	return NewPlanOpCopy(p.planner, p.targetTable, p.url, p.apiKey, p.ddl, children[0]), nil
}

func (p *PlanOpCopy) Children() []types.PlanOperator {
	return []types.PlanOperator{
		p.ChildOp,
	}
}

func (p *PlanOpCopy) Plan() map[string]interface{} {
	result := make(map[string]interface{})
	result["_op"] = fmt.Sprintf("%T", p)
	result["_schema"] = p.Schema().Plan()
	result["child"] = p.ChildOp.Plan()
	result["child"] = p.ChildOp.Plan()
	return result
}

func (p *PlanOpCopy) String() string {
	return ""
}

func (p *PlanOpCopy) AddWarning(warning string) {
	p.warnings = append(p.warnings, warning)
}

func (p *PlanOpCopy) Warnings() []string {
	return p.warnings
}

func (p *PlanOpCopy) Expressions() []types.PlanExpression {
	return []types.PlanExpression{}
}

func (p *PlanOpCopy) WithUpdatedExpressions(exprs ...types.PlanExpression) (types.PlanOperator, error) {
	if len(exprs) > 0 {
		return nil, sql3.NewErrInternalf("unexpected number of exprs '%d'", len(exprs))
	}
	return p, nil
}

type copyIterator struct {
	planner         *ExecutionPlanner
	targetTableName string
	copySchema      types.Schema
	ddl             string
	child           types.RowIterator
	hasStarted      *struct{}
}

func newCopyIterator(planner *ExecutionPlanner, targetTableName string, ddl string, copySchema types.Schema, childIter types.RowIterator) *copyIterator {
	return &copyIterator{
		planner:         planner,
		targetTableName: targetTableName,
		ddl:             ddl,
		copySchema:      copySchema,
		child:           childIter,
	}
}

func (i *copyIterator) Next(ctx context.Context) (types.Row, error) {
	if i.hasStarted == nil {
		// parse and execute the ddl to create the table
		ast, err := parser.NewParser(strings.NewReader(i.ddl)).ParseStatement()
		if err != nil {
			return nil, err
		}
		ct, ok := ast.(*parser.CreateTableStatement)
		if !ok {
			return nil, sql3.NewErrInternalf("unexpected ast type")
		}
		// analyze
		err = i.planner.analyzeCreateTableStatement(ct)
		if err != nil {
			return nil, err
		}
		ctOp, err := i.planner.compileCreateTableStatement(ctx, ct)
		if err != nil {
			return nil, err
		}
		ctIter, err := ctOp.Iterator(context.Background(), nil)
		if err != nil {
			return nil, err
		}
		_, err = ctIter.Next(ctx)
		if err != nil && err != types.ErrNoMoreRows {
			return nil, err
		}

		targetColumns := make([]*qualifiedRefPlanExpression, 0)

		for _, s := range i.copySchema {
			targetColumns = append(targetColumns, newQualifiedRefPlanExpression(i.targetTableName, s.ColumnName, 0, s.Type))
		}

		// build an insert iterator for the target table
		insertIter := &insertRowIter{
			planner:       i.planner,
			tableName:     i.targetTableName,
			targetColumns: targetColumns,
		}

		batchCount := 0
		insertBatch := make([][]types.PlanExpression, 0)

		for {
			// get a source row
			row, err := i.child.Next(ctx)
			if err != nil {
				if err == types.ErrNoMoreRows {
					break
				}
				return nil, err
			}

			// add it to target batch

			irow := make([]types.PlanExpression, len(row))
			for i, s := range i.copySchema {
				switch ty := s.Type.(type) {
				case *parser.DataTypeID, *parser.DataTypeInt:
					val, ok := row[i].(int64)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					irow[i] = newIntLiteralPlanExpression(val)

				case *parser.DataTypeDecimal:
					val, ok := row[i].(pql.Decimal)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					irow[i] = newFloatLiteralPlanExpression(val.String())

				case *parser.DataTypeString:
					val, ok := row[i].(string)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					irow[i] = newStringLiteralPlanExpression(val)

				case *parser.DataTypeBool:
					val, ok := row[i].(bool)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					irow[i] = newBoolLiteralPlanExpression(val)

				case *parser.DataTypeTimestamp:
					val, ok := row[i].(time.Time)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					irow[i] = newTimestampLiteralPlanExpression(val)

				case *parser.DataTypeStringSet:
					val, ok := row[i].([]string)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}

					members := make([]types.PlanExpression, 0)
					for _, m := range val {
						members = append(members, newStringLiteralPlanExpression(m))
					}
					irow[i] = newExprSetLiteralPlanExpression(members, parser.NewDataTypeStringSet())

				case *parser.DataTypeIDSet:
					val, ok := row[i].([]int64)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}

					members := make([]types.PlanExpression, 0)
					for _, m := range val {
						members = append(members, newIntLiteralPlanExpression(m))
					}
					irow[i] = newExprSetLiteralPlanExpression(members, parser.NewDataTypeIDSet())

				default:
					return nil, sql3.NewErrInternalf("unhandled type '%T'", ty)
				}
			}
			insertBatch = append(insertBatch, irow)

			// inc batch count
			batchCount += 1
			if batchCount > 1000 {
				// do the insert
				insertIter.insertValues = insertBatch
				_, err = insertIter.Next(context.Background())
				if err != nil && err != types.ErrNoMoreRows {
					return nil, err
				}
				// reset
				batchCount = 0
				insertBatch = make([][]types.PlanExpression, 0)
			}
		}
		if len(insertBatch) > 0 {
			// do the insert
			insertIter.insertValues = insertBatch
			_, err = insertIter.Next(context.Background())
			if err != nil && err != types.ErrNoMoreRows {
				return nil, err
			}
		}

		i.hasStarted = &struct{}{}
	}
	return nil, types.ErrNoMoreRows
}

type remoteCopyIterator struct {
	planner         *ExecutionPlanner
	targetTableName string
	copySchema      types.Schema
	url             string
	apiKey          string
	ddl             string
	child           types.RowIterator
	hasStarted      *struct{}
}

func newRemoteCopyIterator(planner *ExecutionPlanner, targetTableName string, url string, apiKey string, ddl string, copySchema types.Schema, childIter types.RowIterator) *remoteCopyIterator {
	return &remoteCopyIterator{
		planner:         planner,
		targetTableName: targetTableName,
		url:             url,
		apiKey:          apiKey,
		ddl:             ddl,
		copySchema:      copySchema,
		child:           childIter,
	}
}

func (i *remoteCopyIterator) remoteExec(ctx context.Context, sql string) (*pilosa.WireQueryResponse, error) {
	// Create HTTP request.
	req, err := http.NewRequest("POST", i.url, strings.NewReader(sql))
	if err != nil {
		return nil, sql3.NewErrInternalf("error executing remotely: %s", err.Error())
	}

	req.Header.Set("Content-Length", strconv.Itoa(len(sql)))
	req.Header.Set("Content-Type", "text/plain")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("User-Agent", "pilosa/"+i.planner.systemAPI.Version())
	if len(i.apiKey) > 0 {
		req.Header.Set("X-API-Key", i.apiKey)
	}

	// Execute request against the host.
	resp, err := http.DefaultClient.Do(req.WithContext(ctx))
	if err != nil {
		return nil, sql3.NewErrInternalf("error executing remotely: %s", err.Error())
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, sql3.NewErrInternalf("error executing remotely: %s", err.Error())
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		if resp.StatusCode == 401 {
			return nil, sql3.NewErrRemoteUnauthorized(0, 0, i.url)
		}
		// we have an error
		return nil, sql3.NewErrInternalf("error  executing remotely: %d, %s", resp.StatusCode, string(body))
	}

	sqlResponse := &pilosa.WireQueryResponse{}
	err = sqlResponse.UnmarshalJSONTyped([]byte(body), true)
	if err != nil {
		return nil, sql3.NewErrInternalf("error executing remotely: %s", err.Error())
	}

	if len(sqlResponse.Error) > 0 {
		return nil, sql3.NewErrInternalf("error executing remotely: %s", sqlResponse.Error)
	}

	return sqlResponse, nil
}

func (i *remoteCopyIterator) Next(ctx context.Context) (types.Row, error) {
	if i.hasStarted == nil {
		// execute the ddl to create the table
		_, err := i.remoteExec(ctx, i.ddl)
		if err != nil {
			return nil, err
		}

		// build bulk insert statement
		var buf bytes.Buffer
		buf.WriteString("bulk insert into ")
		fmt.Fprintf(&buf, "%s", i.targetTableName)
		buf.WriteString(" (")

		for i, s := range i.copySchema {
			if i > 0 {
				buf.WriteString(", ")
			}
			fmt.Fprintf(&buf, "%s", s.ColumnName)
		}
		buf.WriteString(") map (")
		for i, s := range i.copySchema {
			if i > 0 {
				buf.WriteString(", ")
			}
			fmt.Fprintf(&buf, "'$._%d' %s", i, s.Type.TypeDescription())
		}
		buf.WriteString(") from x'")
		header := buf.String()

		batchCount := 0
		var batchBuf bytes.Buffer

		for {
			// get a source row
			row, err := i.child.Next(ctx)
			if err != nil {
				if err == types.ErrNoMoreRows {
					break
				}
				return nil, err
			}

			// add it to target batch
			var rowBuf bytes.Buffer
			rowBuf.WriteString("{")
			for i, s := range i.copySchema {
				if i > 0 {
					rowBuf.WriteString(",")
				}
				fmt.Fprintf(&rowBuf, `"_%d":`, i)

				if row[i] == nil {
					rowBuf.WriteString("null")
					continue
				}

				switch ty := s.Type.(type) {
				case *parser.DataTypeID, *parser.DataTypeInt:
					val, ok := row[i].(int64)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					fmt.Fprintf(&rowBuf, "%d", val)

				case *parser.DataTypeString:
					val, ok := row[i].(string)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					// escape single quotes
					val = strings.ReplaceAll(val, `'`, `''`)
					// and double quotes
					val = strings.ReplaceAll(val, `"`, `\"`)
					// and line feeds
					if strings.Contains(val, "\n") {
						val = strings.ReplaceAll(val, "\n", "\\n")
					}
					fmt.Fprintf(&rowBuf, `"%s"`, val)

				case *parser.DataTypeBool:
					val, ok := row[i].(bool)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					fmt.Fprintf(&rowBuf, "%v", val)

				case *parser.DataTypeTimestamp:
					val, ok := row[i].(time.Time)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					fmt.Fprintf(&rowBuf, `"%s"`, val.Format(time.RFC3339Nano))

				case *parser.DataTypeStringSet:
					val, ok := row[i].([]string)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					rowBuf.WriteString("[")
					for j, s := range val {
						if j > 0 {
							rowBuf.WriteString(",")
						}
						fmt.Fprintf(&rowBuf, `"%s"`, s)
					}
					rowBuf.WriteString("]")

				case *parser.DataTypeIDSet:
					val, ok := row[i].([]int64)
					if !ok {
						return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
					}
					rowBuf.WriteString("[")
					for j, s := range val {
						if j > 0 {
							rowBuf.WriteString(",")
						}
						fmt.Fprintf(&rowBuf, `%d`, s)
					}
					rowBuf.WriteString("]")

				default:
					return nil, sql3.NewErrInternalf("unhandled type '%T'", ty)
				}
			}
			rowBuf.WriteString("}\n")
			batchBuf.Write(rowBuf.Bytes())

			// inc batch count
			batchCount += 1
			if batchCount > 10000 {
				// do the insert

				var reqBuf bytes.Buffer
				reqBuf.WriteString(header)
				reqBuf.Write(batchBuf.Bytes())
				reqBuf.WriteString("' with batchsize 10000 input 'STREAM' format 'NDJSON'")

				_, err := i.remoteExec(ctx, reqBuf.String())
				if err != nil {
					return nil, err
				}

				// reset
				batchCount = 0
				batchBuf.Reset()
			}
		}
		if batchCount > 0 {
			// do the insert

			var reqBuf bytes.Buffer
			reqBuf.WriteString(header)
			reqBuf.Write(batchBuf.Bytes())
			reqBuf.WriteString("' with batchsize 10000 input 'STREAM' format 'NDJSON'")

			_, err := i.remoteExec(ctx, reqBuf.String())
			if err != nil {
				return nil, err
			}
		}

		i.hasStarted = &struct{}{}
	}
	return nil, types.ErrNoMoreRows
}
