package om

import (
	"errors"
	"fmt"
	"reflect"
	"strings"

	"git.simanc.com/software/golib/v4/features/sdb"
)

var (
	ErrRowNotFound = errors.New("row not found")
)

type ORM struct {
	TableName string
	DB        *sdb.DB
}

func (o *ORM) Find(query Params, limit LimitParams, order OrderBy) ([]sdb.M, error) {
	builder := NewBuilder()
	builder.Table(o.TableName)
	if err := builder.Query(query); err != nil {
		return nil, err
	}
	builder.Limit(limit)
	builder.OrderBy(order)
	sql := builder.GetSelectSQL()
	values := builder.GetValues()
	return o.DB.Query(sql, values...)
}

func (o *ORM) FindOne(query Params) (sdb.M, error) {
	return o.FindOneByOrder(query, OrderBy{})
}

func (o *ORM) FindOneByOrder(query Params, order OrderBy) (sdb.M, error) {
	rows, err := o.Find(query, LimitParams{Limit: 1}, order)
	if err != nil {
		return nil, err
	}
	if len(rows) == 0 {
		return nil, ErrRowNotFound
	}
	return rows[0], nil
}

func (o *ORM) InsertOne(row sdb.M) error {
	k, v := o.splitMap(row)
	query := CreateInsertSQL(o.TableName, k)
	return o.DB.Exec(query, v...)
}

func (o *ORM) InsertMany(rows []sdb.M) error {
	if len(rows) == 0 {
		return nil
	}
	if len(rows) == 1 {
		return o.InsertOne(rows[0])
	}
	k := make([]string, 0, len(rows))
	for key := range rows[0] {
		k = append(k, key)
	}
	args := make([][]any, len(rows))
	for i, row := range rows {
		arg := make([]any, len(k))
		for j, key := range k {
			if val, ok := row[key]; ok {
				arg[j] = val
			} else {
				return fmt.Errorf("idx:%d key: %s not found", i, key)
			}
		}
		args[i] = arg
	}
	query := CreateInsertSQL(o.TableName, k)
	return o.DB.Execs(query, args...)
}

func (o *ORM) InsertAny(v any) error {
	if row, ok := v.(sdb.M); ok {
		return o.InsertOne(row)
	}
	if rows, ok := v.([]sdb.M); ok {
		return o.InsertMany(rows)
	}
	rk := reflect.ValueOf(v).Kind()
	switch rk {
	case reflect.Struct:
		row, err := sdb.Encode(v)
		if err != nil {
			return err
		}
		return o.InsertOne(row)
	case reflect.Slice, reflect.Array:
		rows, err := sdb.Encodes(v)
		if err != nil {
			return err
		}
		return o.InsertMany(rows)
	default:
		return fmt.Errorf("unsupported value type: %s", rk.String())
	}
}

func (o *ORM) Delete(query Params) error {
	builder := NewBuilder()
	builder.Table(o.TableName)
	if err := builder.Query(query); err != nil {
		return err
	}
	sql := builder.GetDeleteSQL()
	value := builder.GetValues()
	return o.DB.Exec(sql, value...)
}

func (o *ORM) Update(query Params, update sdb.M) error {
	qk, qv := o.splitMap(query)
	k, v := o.splitMap(update)
	v = append(v, qv...)
	sql := CreateUpdateSql(o.TableName, k, qk...)
	return o.DB.Exec(sql, v...)
}

func (o *ORM) UpdateBySn(sn string, update sdb.M) error {
	delete(update, defaultQueryField)
	k, v := o.splitMap(update)
	v = append(v, sn)
	sql := CreateUpdateSql(o.TableName, k, defaultQueryField)
	return o.DB.Exec(sql, v...)
}

func (o *ORM) ListWithParams(query Params, limit LimitParams, orderBy OrderBy) ([]sdb.M, int64, error) {
	var total int64 = 0
	if limit.Limit > 0 {
		total, _ = o.Count(query)
		if total <= 0 {
			return []sdb.M{}, 0, nil
		}
	}
	retMaps, err := o.Find(query, limit, orderBy)
	if err != nil {
		return nil, 0, err
	}
	if limit.Limit == 0 {
		total = int64(len(retMaps))
	}
	return retMaps, total, nil
}

func (o *ORM) Count(query Params) (int64, error) {
	builder := NewBuilder()
	builder.Table(o.TableName)
	if err := builder.Query(query); err != nil {
		return 0, err
	}
	sql := builder.GetCountSQL()
	values := builder.GetValues()
	counts, err := o.DB.Count(1, sql, values...)
	if err != nil {
		return 0, err
	}
	return counts[0], nil
}

func (o *ORM) BatchUpdate(update sdb.M, idField string, ids []string) error {
	k, v := o.splitMap(update)
	sep := `' = ?, '`
	columns := strings.Join(k, sep)
	ins := func() string {
		mark := make([]string, len(ids))
		for i := 0; i < len(ids); i++ {
			mark[i] = "?"
			v = append(v, ids[i])
		}
		return strings.Join(mark, ", ")
	}()
	query := fmt.Sprintf(`UPDATE '%s' SET '%s' = ? WHERE %s IN (%s)`, o.TableName, columns, idField, ins)
	return o.DB.Exec(query, v...)
}

func (o *ORM) splitMap(param map[string]any) ([]string, []any) {
	var k []string
	var v []any
	for key, val := range param {
		v = append(v, val)
		k = append(k, key)
	}
	return k, v
}
