package stargate

import (
	"bytes"
	"fmt"
	"strconv"
	"strings"

	"github.com/juju/errors"
	"github.com/ngaut/log"
	"github.com/pingcap/wormhole/pkg/mysql"
	"github.com/pingcap/wormhole/pkg/whpb"
)

func genInsertSQLs(data *whpb.DMLData) ([]string, [][]interface{}, error) {
	schema := data.GetSchema()
	table := data.GetTable()
	rows := data.GetRows()
	columns := data.GetColumns()

	sqls := make([]string, 0, len(rows))
	args := make([][]interface{}, 0, len(rows))
	columnList := genColumnList(columns)
	columnPlaceholders := genColumnPlaceholders(len(columns))
	columnPlaceholdersList := make([]string, 0, 1000)
	arg := make([]interface{}, 0, 1000)

	for _, row := range rows {
		rowData := row.GetRow()
		if len(rowData) != len(columns) {
			return nil, nil, errors.Errorf("[stargate] insert columns and data mismatch in length: %d vs %d", len(columns), len(rowData))
		}
		columnDatas := make([]interface{}, 0, len(rowData))
		for i := range rowData {
			d, err := castColumnData(rowData[i], columns[i])
			if err != nil {
				return nil, nil, errors.Trace(err)
			}
			columnDatas = append(columnDatas, d)
		}

		columnPlaceholdersList = append(columnPlaceholdersList, columnPlaceholders)
		arg = append(arg, columnDatas...)
	}
	sql := fmt.Sprintf("REPLACE INTO `%s`.`%s` (%s) VALUES %s;", schema, table, columnList, strings.Join(columnPlaceholdersList, ","))
	sqls = append(sqls, sql)
	args = append(args, arg)

	return sqls, args, nil
}

func genUpdateSQLs(data *whpb.DMLData) ([]string, [][]interface{}, error) {
	schema := data.GetSchema()
	table := data.GetTable()
	rows := data.GetRows()
	columns := data.GetColumns()

	sqls := make([]string, 0, len(rows)/2)
	args := make([][]interface{}, 0, len(rows)/2)
	keyColumns := genKeyColumns(columns)

	log.Debugf("[stargate] rows data: %+v, column data: %+v", rows, columns)

	for i := 0; i < len(rows); i += 2 {
		oldData := rows[i].GetRow()
		changedData := rows[i+1].GetRow()
		if len(oldData) != len(changedData) {
			return nil, nil, errors.Errorf("[stargate] update data mismatch in length: %d vs %d", len(oldData), len(changedData))
		}

		if len(oldData) != len(columns) {
			return nil, nil, errors.Errorf("[stargate] update columns and data mismatch in length: %d vs %d", len(columns), len(oldData))
		}

		log.Debugf("[stargate] old data %v, changed data %v", oldData, changedData)

		oldValues := make([]interface{}, 0, len(oldData))
		changedValues := make([]interface{}, 0, len(oldData))

		for i := range oldData {
			oldValue, err := castColumnData(oldData[i], columns[i])
			if err != nil {
				return nil, nil, errors.Trace(err)
			}
			changedValue, err := castColumnData(changedData[i], columns[i])
			if err != nil {
				return nil, nil, errors.Trace(err)
			}
			oldValues = append(oldValues, oldValue)
			changedValues = append(changedValues, changedValue)
		}

		// gennerate updated column and values
		updateColumns := make([]*whpb.Column, 0, len(changedData))
		updateValues := make([]interface{}, 0, len(changedData))
		for i := range oldData {
			if oldData[i] == changedData[i] {
				continue
			}
			updateColumns = append(updateColumns, columns[i])
			updateValues = append(updateValues, changedValues[i])
		}

		// ignore no changed sql
		if len(updateColumns) == 0 {
			continue
		}

		kvs := genKVs(updateColumns)
		value := make([]interface{}, 0, len(oldData))
		value = append(value, updateValues...)

		whereColumns, whereValues := columns, oldValues
		if len(keyColumns) > 0 {
			whereColumns, whereValues = getColumnData(columns, oldValues, keyColumns)
		}
		log.Debugf("[stargate] where_columns %+v, where_values %+v", whereColumns, whereValues)
		// todo: if columnData == NULL?
		where := genWhere(whereColumns, whereValues)
		value = append(value, whereValues...)
		log.Debugf("[stargate] where %v, value %v, kvs %+v", where, value, kvs)
		sql := fmt.Sprintf("UPDATE `%s`.`%s` SET %s WHERE %s LIMIT 1;", schema, table, kvs, where)
		log.Debugf("[stargate] update sql %+v, args %+v", sql, value)
		sqls = append(sqls, sql)
		args = append(args, value)
	}

	return sqls, args, nil
}

func genDeleteSQLs(data *whpb.DMLData) ([]string, [][]interface{}, error) {
	schema := data.GetSchema()
	table := data.GetTable()
	rows := data.GetRows()
	columns := data.GetColumns()

	sqls := make([]string, 0, len(rows))
	args := make([][]interface{}, 0, len(rows))
	keyColumns := genKeyColumns(columns)
	for _, row := range rows {
		rowData := row.GetRow()
		if len(rowData) != len(columns) {
			return nil, nil, errors.Errorf("[stargate] delete columns and data mismatch in length: %d vs %d", len(columns), len(rowData))
		}

		columnDatas := make([]interface{}, 0, len(columns))
		for i := range rowData {
			columnData, err := castColumnData(rowData[i], columns[i])
			if err != nil {
				return nil, nil, errors.Trace(err)
			}
			columnDatas = append(columnDatas, columnData)
		}

		whereColumns, whereValues := columns, columnDatas
		if len(keyColumns) > 0 {
			whereColumns, whereValues = getColumnData(columns, columnDatas, keyColumns)
		}

		where := genWhere(whereColumns, whereValues)
		sql := fmt.Sprintf("DELETE FROM `%s`.`%s` WHERE %s LIMIT 1;", schema, table, where)
		sqls = append(sqls, sql)
		args = append(args, whereValues)
	}

	return sqls, args, nil
}

func genColumnList(columns []*whpb.Column) string {
	var columnList []byte
	for i, column := range columns {
		name := fmt.Sprintf("`%s`", column.Name)
		columnList = append(columnList, []byte(name)...)

		if i != len(columns)-1 {
			columnList = append(columnList, ',')
		}
	}

	return string(columnList)
}

func genColumnPlaceholders(length int) string {
	values := make([]string, length, length)
	for i := 0; i < length; i++ {
		values[i] = "?"
	}
	return fmt.Sprintf("(%s)", strings.Join(values, ","))
}

func getColumnData(columns []*whpb.Column, rows []interface{}, keyColumns []*whpb.Column) ([]*whpb.Column, []interface{}) {
	cols := make([]*whpb.Column, 0, len(columns))
	values := make([]interface{}, 0, len(columns))
	for _, column := range keyColumns {
		cols = append(cols, column)
		values = append(values, rows[column.Idx])
	}
	return cols, values
}

func genWhere(columns []*whpb.Column, rows []interface{}) string {
	var kvs bytes.Buffer
	for i := range columns {
		kvSplit := "="
		// we have convert NULL to nil in castColumnData.
		if rows[i] == nil || rows[i] == "null" || rows[i] == "NULL" {
			kvSplit = "IS"
		}

		//TODO: use unique index use where condition.
		if i == len(columns)-1 {
			fmt.Fprintf(&kvs, "`%s` %s ?", columns[i].Name, kvSplit)
		} else {
			fmt.Fprintf(&kvs, "`%s` %s ? AND ", columns[i].Name, kvSplit)
		}
	}

	return kvs.String()
}

func genKVs(columns []*whpb.Column) string {
	var kvs bytes.Buffer
	for i := range columns {
		if i == len(columns)-1 {
			fmt.Fprintf(&kvs, "`%s` = ?", columns[i].Name)
		} else {
			fmt.Fprintf(&kvs, "`%s` = ?, ", columns[i].Name)
		}
	}
	return kvs.String()
}

func genKeyColumns(columns []*whpb.Column) []*whpb.Column {
	keyCols := make([]*whpb.Column, 0, len(columns))
	for _, col := range columns {
		if col.IsKey {
			keyCols = append(keyCols, col)
		}
	}
	return keyCols
}

func castColumnData(columnData string, column *whpb.Column) (interface{}, error) {
	if strings.ToLower(columnData) == "null" {
		// let go package convert `nil`` to `NULL`.
		return nil, nil
	}
	// binlog's bit type data should convert to int
	switch column.Type {
	case mysql.TypeBit:
		intData, err := strconv.ParseUint(columnData, 10, 64)
		if err == nil {
			return intData, nil
		}
		log.Warnf("convert string %s to uint failed!", columnData)
	}

	return columnData, nil
}
