/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package executor

import (
	"fmt"
	"strings"

	"github.com/seata/seata-go/pkg/datasource/sql/datasource"
	"github.com/seata/seata-go/pkg/datasource/sql/types"
	"github.com/seata/seata-go/pkg/util/log"
)

// IsRecordsEquals check before record and after record if equal
func IsRecordsEquals(beforeImage *types.RecordImage, afterImage *types.RecordImage) (bool, error) {
	if beforeImage == nil && afterImage == nil {
		return true, nil
	}
	if beforeImage == nil || afterImage == nil {
		return false, nil
	}

	if !strings.EqualFold(beforeImage.TableName, afterImage.TableName) || len(beforeImage.Rows) != len(afterImage.Rows) {
		return false, nil
	}
	if len(beforeImage.Rows) == 0 {
		return true, nil
	}

	return compareRows(*beforeImage.TableMeta, beforeImage.Rows, afterImage.Rows)
}

func compareRows(tableMeta types.TableMeta, oldRows []types.RowImage, newRows []types.RowImage) (bool, error) {
	oldRowMap := rowListToMap(oldRows, tableMeta.GetPrimaryKeyOnlyName())
	newRowMap := rowListToMap(newRows, tableMeta.GetPrimaryKeyOnlyName())

	for key, oldRow := range oldRowMap {
		newRow := newRowMap[key]
		if newRow == nil {
			log.Errorf("compare row failed, rowKey %s, reason new field is null", key)
			return false, fmt.Errorf("compare image failed for new row is null")
		}
		for fieldName, oldValue := range oldRow {
			newValue := newRow[fieldName]
			if newValue == nil {
				log.Errorf("compare row failed, rowKey %s, fieldName %s, reason new value is null", key, fieldName)
				return false, fmt.Errorf("compare image failed for new value is null")
			}
			if !datasource.DeepEqual(newValue, oldValue) {
				return false, nil
			}
		}
	}
	return true, nil
}

func rowListToMap(rows []types.RowImage, primaryKeyList []string) map[string]map[string]interface{} {
	rowMap := make(map[string]map[string]interface{}, 0)
	for _, row := range rows {
		fieldMap := make(map[string]interface{}, 0)
		var rowKey string
		var firstUnderline bool

		for _, column := range row.Columns {
			for i, key := range primaryKeyList {
				if column.ColumnName == key {
					if firstUnderline && i > 0 {
						rowKey += "_##$$_"
					}
					// todo make value more accurate
					rowKey = fmt.Sprintf("%v%v", rowKey, column.GetActualValue())
					firstUnderline = true
				}
			}
			fieldMap[strings.ToUpper(column.ColumnName)] = column.Value
		}
		rowMap[rowKey] = fieldMap
	}
	return rowMap
}

// buildWhereConditionByPKs build where condition by primary keys
// each pk is a condition.the result will like :" (id,userCode) in ((?,?),(?,?)) or (id,userCode) in ((?,?),(?,?) ) or (id,userCode) in ((?,?))"
func buildWhereConditionByPKs(pkNameList []string, rowSize int, maxInSize int) string {
	var (
		whereStr  = &strings.Builder{}
		batchSize = rowSize/maxInSize + 1
	)

	if rowSize%maxInSize == 0 {
		batchSize = rowSize / maxInSize
	}

	for batch := 0; batch < batchSize; batch++ {
		if batch > 0 {
			whereStr.WriteString(" OR ")
		}
		whereStr.WriteString("(")

		for i := 0; i < len(pkNameList); i++ {
			if i > 0 {
				whereStr.WriteString(",")
			}
			// todo add escape
			whereStr.WriteString(fmt.Sprintf("`%s`", pkNameList[i]))
		}
		whereStr.WriteString(") IN (")

		var eachSize int

		if batch == batchSize-1 {
			if rowSize%maxInSize == 0 {
				eachSize = maxInSize
			} else {
				eachSize = rowSize % maxInSize
			}
		} else {
			eachSize = maxInSize
		}

		for i := 0; i < eachSize; i++ {
			if i > 0 {
				whereStr.WriteString(",")
			}
			whereStr.WriteString("(")
			for j := 0; j < len(pkNameList); j++ {
				if j > 0 {
					whereStr.WriteString(",")
				}
				whereStr.WriteString("?")
			}
			whereStr.WriteString(")")
		}
		whereStr.WriteString(")")
	}
	return whereStr.String()
}

func buildPKParams(rows []types.RowImage, pkNameList []string) []interface{} {
	params := make([]interface{}, 0)
	for _, row := range rows {
		coumnMap := row.GetColumnMap()
		for _, pk := range pkNameList {
			col := coumnMap[pk]
			if col != nil {
				params = append(params, col.Value)
			}
		}
	}
	return params
}
