// Copyright 2016 The Xorm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package xorm

import (
	"reflect"

	"code.forgejo.org/xorm/xorm/internal/statements"
	"code.forgejo.org/xorm/xorm/internal/utils"
	"code.forgejo.org/xorm/xorm/schemas"
	"xorm.io/builder"
)

// enumerated all errors
var (
	ErrNoColumnsTobeUpdated = statements.ErrNoColumnsTobeUpdated
)

func (session *Session) genAutoCond(condiBean any) (builder.Cond, error) {
	if session.statement.NoAutoCondition {
		return builder.NewCond(), nil
	}

	if c, ok := condiBean.(map[string]any); ok {
		eq := make(builder.Eq)
		for k, v := range c {
			eq[session.engine.Quote(k)] = v
		}

		if session.statement.RefTable != nil {
			if col := session.statement.RefTable.DeletedColumn(); col != nil && !session.statement.GetUnscoped() { // tag "deleted" is enabled
				return eq.And(session.statement.CondDeleted(col)), nil
			}
		}
		return eq, nil
	}

	ct := reflect.TypeOf(condiBean)
	k := ct.Kind()
	if k == reflect.Pointer {
		k = ct.Elem().Kind()
	}
	if k != reflect.Struct {
		return nil, ErrConditionType
	}

	condTable, err := session.engine.TableInfo(condiBean)
	if err != nil {
		return nil, err
	}
	return session.statement.BuildConds(condTable, condiBean, true, true, false, true, false)
}

// Update records, bean's non-empty fields are updated contents,
// condiBean' non-empty filds are conditions
// CAUTION:
//
//	1.bool will defaultly be updated content nor conditions
//	 You should call UseBool if you have bool to use.
//	2.float32 & float64 may be not inexact as conditions
func (session *Session) Update(bean any, condiBean ...any) (int64, error) {
	if session.isAutoClose {
		defer session.Close()
	}

	defer session.resetStatement()

	if session.statement.LastError != nil {
		return 0, session.statement.LastError
	}

	v := utils.ReflectValue(bean)
	t := v.Type()

	// handle before update processors
	for _, closure := range session.beforeClosures {
		closure(bean)
	}
	cleanupProcessorsClosures(&session.beforeClosures) // cleanup after used
	if processor, ok := bean.(BeforeUpdateProcessor); ok {
		processor.BeforeUpdate()
	}
	// --

	var colNames []string
	var args []any
	var err error
	isMap := t.Kind() == reflect.Map
	isStruct := t.Kind() == reflect.Struct
	if isStruct {
		if err := session.statement.SetRefBean(bean); err != nil {
			return 0, err
		}

		if len(session.statement.TableName()) == 0 {
			return 0, ErrTableNotFound
		}

		if session.statement.ColumnStr() == "" {
			colNames, args, err = session.statement.BuildUpdates(v, false, false,
				false, false, true)
		} else {
			colNames, args, err = session.genUpdateColumns(bean)
		}
		if err != nil {
			return 0, err
		}
	} else if isMap {
		colNames = make([]string, 0)
		args = make([]any, 0)
		bValue := reflect.Indirect(reflect.ValueOf(bean))

		for _, v := range bValue.MapKeys() {
			colNames = append(colNames, session.engine.Quote(v.String())+" = ?")
			args = append(args, bValue.MapIndex(v).Interface())
		}
	} else {
		return 0, ErrParamsType
	}

	table := session.statement.RefTable

	if session.statement.UseAutoTime && table != nil && table.Updated != "" {
		if !session.statement.ColumnMap.Contain(table.Updated) &&
			!session.statement.OmitColumnMap.Contain(table.Updated) {
			colNames = append(colNames, session.engine.Quote(table.Updated)+" = ?")
			col := table.UpdatedColumn()
			val, t, err := session.engine.nowTime(col)
			if err != nil {
				return 0, err
			}
			args = append(args, val)

			colName := col.Name
			if isStruct {
				session.afterClosures = append(session.afterClosures, func(bean any) {
					col := table.GetColumn(colName)
					setColumnTime(bean, col, t)
				})
			}
		}
	}

	if err = session.statement.ProcessIDParam(); err != nil {
		return 0, err
	}

	var autoCond builder.Cond
	if len(condiBean) > 0 {
		autoCond, err = session.genAutoCond(condiBean[0])
		if err != nil {
			return 0, err
		}
	} else if table != nil {
		if col := table.DeletedColumn(); col != nil && !session.statement.GetUnscoped() { // tag "deleted" is enabled
			autoCond1 := session.statement.CondDeleted(col)

			if autoCond == nil {
				autoCond = autoCond1
			} else {
				autoCond = autoCond.And(autoCond1)
			}
		}
	}

	var (
		cond     = session.statement.Conds().And(autoCond)
		doIncVer = isStruct && (table != nil && table.Version != "" && session.statement.CheckVersion)
		verValue *reflect.Value
	)
	if doIncVer {
		verValue, err = table.VersionColumn().ValueOfV(&v)
		if err != nil {
			return 0, err
		}

		if verValue != nil {
			cond = cond.And(builder.Eq{session.engine.Quote(table.Version): verValue.Interface()})
		}
	}

	updateWriter := builder.NewWriter()
	if err := session.statement.WriteUpdate(updateWriter, cond, v, colNames, args); err != nil {
		return 0, err
	}

	tableName := session.statement.TableName() // table name must been get before exec because statement will be reset
	useCache := session.statement.UseCache

	res, err := session.exec(updateWriter.String(), updateWriter.Args()...)
	if err != nil {
		return 0, err
	} else if doIncVer {
		if verValue != nil && verValue.IsValid() && verValue.CanSet() {
			session.incrVersionFieldValue(verValue)
		}
	}

	if cacher := session.engine.GetCacher(tableName); cacher != nil && useCache {
		session.engine.logger.Debugf("[cache] clear table: %v", tableName)
		cacher.ClearIds(tableName)
		cacher.ClearBeans(tableName)
	}

	// handle after update processors
	if session.isAutoCommit {
		for _, closure := range session.afterClosures {
			closure(bean)
		}
		if processor, ok := bean.(AfterUpdateProcessor); ok {
			session.engine.logger.Debugf("[event] %v has after update processor", tableName)
			processor.AfterUpdate()
		}
	} else {
		lenAfterClosures := len(session.afterClosures)
		if lenAfterClosures > 0 {
			if value, has := session.afterUpdateBeans[bean]; has && value != nil {
				*value = append(*value, session.afterClosures...)
			} else {
				afterClosures := make([]func(any), lenAfterClosures)
				copy(afterClosures, session.afterClosures)
				// FIXME: if bean is a map type, it will panic because map cannot be as map key
				session.afterUpdateBeans[bean] = &afterClosures
			}
		} else {
			if _, ok := bean.(AfterUpdateProcessor); ok {
				session.afterUpdateBeans[bean] = nil
			}
		}
	}
	cleanupProcessorsClosures(&session.afterClosures) // cleanup after used
	// --

	return res.RowsAffected()
}

func (session *Session) genUpdateColumns(bean any) ([]string, []any, error) {
	table := session.statement.RefTable
	colNames := make([]string, 0, len(table.ColumnsSeq()))
	args := make([]any, 0, len(table.ColumnsSeq()))

	for _, col := range table.Columns() {
		if !col.IsVersion && !col.IsCreated && !col.IsUpdated {
			if session.statement.OmitColumnMap.Contain(col.Name) {
				continue
			}
		}
		if col.MapType == schemas.ONLYFROMDB {
			continue
		}

		fieldValuePtr, err := col.ValueOf(bean)
		if err != nil {
			return nil, nil, err
		}
		fieldValue := *fieldValuePtr

		if col.IsAutoIncrement && utils.IsValueZero(fieldValue) {
			continue
		}

		if (col.IsDeleted && !session.statement.GetUnscoped()) || col.IsCreated {
			continue
		}

		// if only update specify columns
		if len(session.statement.ColumnMap) > 0 && !session.statement.ColumnMap.Contain(col.Name) {
			continue
		}

		if session.statement.IncrColumns.IsColExist(col.Name) {
			continue
		} else if session.statement.DecrColumns.IsColExist(col.Name) {
			continue
		} else if session.statement.ExprColumns.IsColExist(col.Name) {
			continue
		}

		// !evalphobia! set fieldValue as nil when column is nullable and zero-value
		if hasFlagForColumn(session.statement.NullableMap, col) {
			if col.Nullable && utils.IsValueZero(fieldValue) {
				var nilValue *int
				fieldValue = reflect.ValueOf(nilValue)
			}
		}

		if col.IsUpdated && session.statement.UseAutoTime /*&& isZero(fieldValue.Interface())*/ {
			// if time is non-empty, then set to auto time
			val, t, err := session.engine.nowTime(col)
			if err != nil {
				return nil, nil, err
			}
			args = append(args, val)

			colName := col.Name
			session.afterClosures = append(session.afterClosures, func(bean any) {
				col := table.GetColumn(colName)
				setColumnTime(bean, col, t)
			})
		} else if col.IsVersion && session.statement.CheckVersion {
			args = append(args, 1)
		} else {
			arg, err := session.statement.Value2Interface(col, fieldValue)
			if err != nil {
				return colNames, args, err
			}
			args = append(args, arg)
		}

		colNames = append(colNames, session.engine.Quote(col.Name)+" = ?")
	}
	return colNames, args, nil
}
