package widetable

import (
	"context"
	"errors"
	"fmt"
	"log"
	"strconv"
	"strings"

	"github.com/huandu/xstrings"
	"gorm.io/gorm"
)

type UpdateFnType func(ctx context.Context, agg *AggregationKey)

type AggregationKey struct {
	Key              string
	PrimaryKey       string
	AggKeyMapForObjs map[string]string `json:"AggKeyMapForObjs"`
	ColumnsMap       map[string]string
	UpdateFuncName   string
	UpdateFunc       UpdateFnType `json:"-"`
	data             map[string]map[string]interface{}
}

func (agg *AggregationKey) Init(table map[string]UpdateFnType) error {
	agg.data = make(map[string]map[string]interface{})
	if agg.UpdateFuncName == "" {
		return nil
	}

	if fn, ok := table[agg.UpdateFuncName]; ok {
		agg.UpdateFunc = fn
		return nil
	} else {
		return errors.New(fmt.Sprintf("aggregationKey update function not found :%s\n",
			agg.UpdateFuncName))
	}
}

// salesforce_recrev | fxxk_recrev
type ObjectMeta struct {
	HierarchyName string
	QuerySQL      string // select a, b, c from obj;
	// it seems that PrimaryColKey is unnessary because we have define it on AggregationKey.AggKeyMapForObjs
	PrimaryColKey string
	ParentColKey  string
	// deal with same table on different platform has different column name for one index
	// columns in thes map will be insert into target table
	ColumnsMap map[string]string
	// column -> transform function name
	TransformMap map[string]string
}

type HierarchyInfo struct {
	Name              string
	PrimaryColumnName string
}

type Hierarchy map[string]HierarchyInfo

func (h Hierarchy) get(name string) *HierarchyInfo {
	if v, ok := h[name]; ok {
		return &v
	}
	return nil
}

type Transformation struct {
}

type Widetable struct {
	AggKey       AggregationKey        `json:"AggKey"`
	AggObjs      map[string]ObjectMeta `json:"AggObjs"`
	TableName    string                `json:"TableName"`
	DataBaseName string                `json:"DataBaseName"`
	ObjHierarchy Hierarchy             `json:"ObjHierarchy"`
	Gdb          *gorm.DB              `json:"-"`
	Wbg          *WidetableManager     `json:"-"`
	// key = object primary id
	// value = parent object primary id
	exists         map[string]string
	ch             chan []string
	transformRoute map[string]func(context.Context, interface{}) interface{}
}

//// todo cjl load data from specific table & init exists map
//func (wb *Widetable) InitFromDB() error {
//	return nil
//}

// data flow comes in, we need to aggregate it, varify the exist data through exists
func (wb *Widetable) Flow(objName string, rows []map[string]interface{}) {
	objMeta, ok := wb.AggObjs[objName]
	if !ok {
		log.Println("[error] object not found ", objName)
		return
	}

	if rows == nil || len(rows) == 0 {
		return
	}

	sqls := make([]string, 0, len(rows))
	for _, row := range rows {
		insertKV := make(map[string]interface{}, len(row))

		AggKeyMappingOfObj := wb.AggKey.AggKeyMapForObjs[objName]
		aggKeyPrimayId, ok := row[AggKeyMappingOfObj]
		if !ok {
			log.Println("[error] row AggKeyMappingOfObj not found ", AggKeyMappingOfObj, row)
			continue
		}
		if aggKeyPrimayId == nil {
			log.Println("[error] 数据异常 ", row)
			continue
		}
		insertKV[wb.AggKey.Key] = aggKeyPrimayId

		if objMeta.PrimaryColKey != "" {
			primaryId, ok := row[objMeta.PrimaryColKey]
			if !ok {
				log.Println("[error] row primary key not found ", objMeta.PrimaryColKey)
				continue
			}
			if primaryId == nil {
				log.Println("[error] 数据异常 primaryId = nil", row)
				primaryId = ""
			}
			insertKV[objMeta.PrimaryColKey] = primaryId

			parentPrimaryId, ok := row[objMeta.ParentColKey]
			if ok && parentPrimaryId != nil {
				wb.exists[primaryId.(string)] = parentPrimaryId.(string)
			} else {
				parentPrimaryId = ""
			}

			hierarchyName := objMeta.HierarchyName
			for {
				parentHierarchyInfo := wb.ObjHierarchy.get(hierarchyName)
				if parentHierarchyInfo == nil {
					break
				}

				insertKV[parentHierarchyInfo.PrimaryColumnName] = parentPrimaryId
				hierarchyName = parentHierarchyInfo.Name

				parentPrimaryId, ok = wb.exists[parentPrimaryId.(string)]
				if !ok {
					parentPrimaryId = ""
				}
			}
		}

		// delete unnessary columns (primay key, parent key, agg key)
		delete(row, objMeta.PrimaryColKey)
		delete(row, objMeta.ParentColKey)
		delete(row, AggKeyMappingOfObj)

		sqls = append(sqls, wb.genInsertOrUpdateSQL(objName, insertKV, row, wb.AggKey.data[aggKeyPrimayId.(string)]))
	}

	wb.ch <- sqls
}

// generate insert on update key sql
func (wb *Widetable) genInsertOrUpdateSQL(objName string, index, row, aggkv map[string]interface{}) string {
	objMeta, _ := wb.AggObjs[objName]
	keys := make([]string, 0, len(index)+len(row))
	values := make([]string, 0, len(index)+len(row))
	for k, v := range index {
		if newColName, ok := objMeta.ColumnsMap[k]; ok {
			k = newColName
		}
		keys = append(keys, xstrings.ToSnakeCase(k))
		if v == nil {
			values = append(values, "")
		} else {
			values = append(values, strconv.Quote(v.(string)))
		}
	}

	for k, v := range aggkv {
		row[k] = v
	}

	updates := strings.Builder{}
	var newColName string
	var ok bool
	for k, v := range row {
		if v == nil {
			continue
		}
		newColName = ""
		if newColName, ok = wb.AggKey.ColumnsMap[k]; ok {
			newColName = xstrings.ToSnakeCase(newColName)
			keys = append(keys, newColName)
			updates.WriteString(newColName)
			updates.WriteString("=")
		} else {
			if newColName, ok = objMeta.ColumnsMap[k]; ok {
				newColName = xstrings.ToSnakeCase(newColName)
				keys = append(keys, newColName)
				updates.WriteString(newColName)
				updates.WriteString("=")
			}
		}

		if newColName == "" {
			continue
		}

		// do transform if needed
		if fnName, ok := objMeta.TransformMap[objMeta.ColumnsMap[k]]; ok {
			if fn, ok := wb.transformRoute[fnName]; ok {
				if fnName == "currencyTransform" {
					currency := "USD"
					if strings.HasPrefix(objName, "Fxxk") {
						currency = "CNY"
					} else {
						tmpCurrency, _ := row["Currency"]
						currency = tmpCurrency.(string)
					}

					v = fn(context.WithValue(
						context.WithValue(wb.Wbg.ctx, "currency", currency),
						"key", k),
						row)
				} else {
					v = fn(context.WithValue(wb.Wbg.ctx, "key", k), row)
				}
			} else {
				log.Println("[error] transform function not exist : ", fnName)
			}
		}
		switch v := v.(type) {
		case string:
			values = append(values, strconv.Quote(v))
			updates.WriteString(strconv.Quote(v))
		case int:
			values = append(values, strconv.FormatInt(int64(v), 10))
			updates.WriteString(strconv.Itoa(v))
		case int64:
			values = append(values, strconv.FormatInt(v, 10))
			updates.WriteString(strconv.FormatInt(v, 10))
		case int32:
			values = append(values, strconv.FormatInt(int64(v), 10))
			updates.WriteString(strconv.FormatInt(int64(v), 10))
		case float64:
			s := fmt.Sprintf("%.3f", v)
			values = append(values, s)
			updates.WriteString(s)
		case float32:
			s := fmt.Sprintf("%.3f", v)
			values = append(values, s)
			updates.WriteString(s)
		case bool:
			s := strconv.FormatBool(v)
			values = append(values, s)
			updates.WriteString(s)
		default:
			log.Printf("[error] unsupported type %T key = %s\n", v, k)
		}
		updates.WriteString(",")
	}

	updatesStr := updates.String()
	return fmt.Sprintf("insert into %s(%s) values (%s) on duplicate key update %s",
		wb.TableName,
		strings.Join(keys, ","),
		strings.Join(values, ","),
		updatesStr[:len(updatesStr)-1],
	)
}

func (wb *Widetable) Import() {
	go func() {
		for sqls := range wb.ch {
			for _, sql := range sqls {
				err := wb.Gdb.Exec(sql).Error
				if err != nil {
					log.Println("[error] db err", err)
				}
			}
		}
	}()
}
