package httpgateway

import (
	"context"
	"database/sql"
	"encoding/hex"
	"reflect"
	"strconv"
	"strings"

	"github.com/pingcap/tidb/pkg/parser"
	"github.com/pingcap/tidb/pkg/parser/ast"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
)

func parseResult(ctx context.Context, result sql.Result) (*ExecuteResponse, error) {
	logger := log.WithContextL(ctx)

	lastInsertID, err := result.LastInsertId()
	if err != nil {
		logger.Error("failed to get last insert id")
		return nil, err
	}
	rowsAffected, err := result.RowsAffected()
	if err != nil {
		logger.Error("failed to get rows affected")
		return nil, err
	}
	sLastInsertID := strconv.FormatInt(lastInsertID, 10)
	return &ExecuteResponse{
		LastInsertID:  &lastInsertID,
		SLastInsertID: &sLastInsertID,
		RowsAffected:  &rowsAffected,
	}, nil
}

func parseRows(ctx context.Context, rows *sql.Rows) (*ExecuteResponse, error) {
	logger := log.WithContextL(ctx)

	types, err := rows.ColumnTypes()
	if err != nil {
		logger.Error("failed to get columns")
		return nil, err
	}

	// get types
	tp := make([]*FieldType, 0, len(types))
	for _, t := range types {
		var nullable = false
		if n, ok := t.Nullable(); ok {
			nullable = n
		}
		tp = append(tp, &FieldType{
			Name:     t.Name(),
			Type:     t.DatabaseTypeName(),
			Nullable: nullable,
		})
	}

	// get data
	values := make([]sql.RawBytes, len(types))
	valuePtrs := make([]any, len(types))
	for i := range types {
		valuePtrs[i] = &values[i]
	}
	data := make([][]*string, 0)
	for rows.Next() {
		if err := rows.Scan(valuePtrs...); err != nil {
			logger.Error("failed to scan value")
			return nil, err
		}
		row := make([]*string, len(types))
		for i := range types {
			if values[i] == nil {
				row[i] = nil
			} else {
				value := encodeBytes(values[i], types[i].DatabaseTypeName())
				row[i] = &value
			}
		}
		data = append(data, row)
	}

	// build resp
	resp := &ExecuteResponse{
		Types: tp,
		Rows:  data,
	}

	return resp, nil
}

func encodeBytes(b []byte, sqlType string) string {
	switch sqlType {
	case "BLOB", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB", "BINARY", "VARBINARY", "BIT":
		// encode as hex string
		return hex.EncodeToString(b)
	default:
		// encode with utf-8
		return string(b)
	}
}

func getTransactionOptions(isolation string) (*sql.TxOptions, bizerr.BizError) {
	if isolation == "" {
		return nil, nil
	}
	switch isolation {
	case "READ UNCOMMITTED":
		return &sql.TxOptions{Isolation: sql.LevelReadUncommitted}, nil
	case "READ COMMITTED":
		return &sql.TxOptions{Isolation: sql.LevelReadCommitted}, nil
	case "REPEATABLE READ":
		return &sql.TxOptions{Isolation: sql.LevelRepeatableRead}, nil
	case "SERIALIZABLE":
		return &sql.TxOptions{Isolation: sql.LevelSerializable}, nil
	default:
		return nil, bizerr.New(ErrCodeUnsupportedIsolation, "%s: %s", UnsupportedIsolationMessage, isolation)
	}
}

func parseSqlType(ctx context.Context, sql string) (SqlType, error) {
	logger := log.WithContextL(ctx)
	if sql == "" {
		return Undefined, nil
	}
	stmtNode, err := parser.New().ParseOneStmt(sql, "", "")
	if err != nil {
		logger.Error("parse sql failed")
		return Undefined, err
	}

	switch stmtNode.(type) {
	case *ast.SelectStmt, *ast.ShowStmt, *ast.ExplainStmt, *ast.SetOprStmt:
		return Query, nil
	case *ast.UseStmt, ast.DDLNode, *ast.SetStmt, *ast.CreateUserStmt,
		*ast.GrantStmt, *ast.SavepointStmt, *ast.ReleaseSavepointStmt:
		return Execute, nil
	case *ast.InsertStmt, *ast.UpdateStmt, *ast.DeleteStmt:
		return DML, nil
	case *ast.CommitStmt:
		return Commit, nil
	case *ast.BeginStmt:
		return Begin, nil
	case *ast.RollbackStmt:
		if strings.Contains(strings.ToLower(sql), "savepoint") {
			return Execute, nil
		}
		return Rollback, nil
	default:
		logger.Warn("unsupported sql type", zap.String("type", reflect.TypeOf(stmtNode).String()))
		return Unsupported, nil
	}
}
