/*
=================================================================================
* License: GPL-2.0 license
* Author: 众产®  https://ciy.cn/code
* Version: 0.1.0
=================================================================================
单数据库连接池 NewCiyMysql
=================================================================================
db, err = c.NewCiyMysql("user:pass@tcp(localhost:3306)/dbname", 最大连接数, 空闲连接数, 保活秒) //main初始化
defer db.Close()
db.Err() //查看最后一条错误信息。  db.ClearError() 清空历史错误。    db.Error //查看原始错误数组。
db.Get(csql, &total) //获取多行数据
db.Getone(csql) //获取单行数据，返回[]map[string]any
db.Get1(csql) //获取单行数据，返回any
db.GetField(csql) //获取列备注及排序
db.Getdbcodes(csql) //获取列备注字典

db.Execute(csql, any...) //执行带问号的SQL语句，返回影响行数和最后插入ID
db.Update(csql, map) //更新数据，返回影响行数
db.Insert(csql, map) //新增数据，返回最后插入ID
db.Delete(csql, backup) //删除数据，返回影响行数。支持副表备份、删除标记、直接删除。
db.Tran(fn) //批处理事务。自动Begin/Commit/Rollback

=================================================================================
查询示例:
csql := c.NewCiySQL("user")
csql.Where("name like", "王").Where("age>", 18).Order("id desc").Limit(1, 10)
var total int //全部行数
list, err := c.DB.Get(csql, &total)

	for _, v := range list {
		id := c.Getint(v, "id")
	}

增删改示例:
=================================================================================
*/
package zciyon

import (
	"database/sql"
	"fmt"
	"runtime"
	"strings"
	"time"
)

type CiyMysql struct {
	Db     *sql.DB
	Tx     *sql.Tx
	Error  []string
	UserID int
	Health int //健康状态，0-100，越小越好
}

// 单个数据库连接池，平时单独使用
func NewCiyMysql(dsn string, maxopenconn int, maxidelconn int, maxlifesec int) (*CiyMysql, error) {
	db := &CiyMysql{}
	db.Error = make([]string, 0)
	db.Health = 50
	db.UserID = 0
	var err error
	db.Db, err = sql.Open("mysql", dsn)
	if err != nil {
		db.addError("NewCiyMysql.Open", err)
		return nil, err
	}
	err = db.Db.Ping()
	if err != nil {
		db.addError("NewCiyMysql.Ping", err)
		return nil, err
	}
	if maxopenconn <= 0 {
		maxopenconn = runtime.NumCPU()*2 + 1
	}
	if maxidelconn < 1 {
		maxidelconn = 2
	}
	if maxlifesec < 1 {
		maxlifesec = 3600 * 6
	}
	db.Db.SetMaxOpenConns(maxopenconn) //最大连接数
	db.Db.SetMaxIdleConns(maxidelconn) //保持空闲的连接数
	db.Db.SetConnMaxLifetime(time.Duration(maxlifesec) * time.Second)
	return db, nil
}

func (thos *CiyMysql) MonitorHealth(sec float64) {
	go func() {
		for {
			Sleep(sec)
			thos.Health = 50
			stat := thos.Db.Stats()
			//Health%=已连接数+等待连接数*2/最大连接数
			thos.Health = (stat.OpenConnections + int(stat.WaitCount)*2) * 100 / stat.MaxOpenConnections
			Clog("health:%d", thos.Health)
		}
	}()
}
func (thos *CiyMysql) Close() error {
	return thos.Db.Close()
}
func (thos *CiyMysql) addError(from string, err error) {
	thos.Error = append(thos.Error, from+":"+err.Error())
}
func (thos *CiyMysql) Err() string {
	if len(thos.Error) == 0 {
		return ""
	}
	return thos.Error[len(thos.Error)-1]
}
func (thos *CiyMysql) ClearError() {
	thos.Error = make([]string, 0)
}

func (thos *CiyMysql) Execute(sqlstr string, argdata ...any) (int, int, error) {
	var rett sql.Result
	var err error
	if thos.Tx != nil {
		rett, err = thos.Tx.Exec(sqlstr, argdata...)
	} else {
		rett, err = thos.Db.Exec(sqlstr, argdata...)
	}
	if err != nil {
		thos.addError("Execute.Exec", err)
		return 0, 0, err
	}
	affect, err := rett.RowsAffected()
	if err != nil {
		thos.addError("Execute.RowsAffected", err)
		return 0, 0, err
	}
	var lastid int64
	if strings.HasPrefix(sqlstr, "insert") {
		lastid, err = rett.LastInsertId()
		if err != nil {
			thos.addError("Execute.LastInsertId", err)
			return 0, 0, err
		}
	}
	return int(lastid), int(affect), nil
}

func (thos *CiyMysql) Update(csql *CiySQL, updata map[string]any) (int, error) {
	if csql.rawsql != "" {
		e := fmt.Errorf("forbid use rawsql")
		thos.addError("Update.Init", e)
		return 0, e
	}
	if len(updata) == 0 {
		e := fmt.Errorf("updata empty")
		thos.addError("Update.Init", e)
		return 0, e
	}
	if csql.sqlwhere == "" {
		e := fmt.Errorf("no where")
		thos.addError("Update.Init", e)
		return 0, e
	}
	sets := []string{}
	datas := []any{}

	for field, data := range updata {
		if field == "id" {
			continue
		}
		if arr, ok := data.([]string); ok {
			sets = append(sets, "`"+field+"`="+arr[0])
		} else {
			sets = append(sets, "`"+field+"`=?")
			datas = append(datas, data)
		}
	}

	setstr := strings.Join(sets, ",")
	combined := make([]any, 0, len(datas)+len(csql.tsmt))
	combined = append(combined, datas...)
	combined = append(combined, csql.tsmt...)
	sqlstr := "update " + csql.table + " set " + setstr + csql.Buildwhere()
	_, lastaffect, err := thos.Execute(sqlstr, combined...)
	if err != nil {
		thos.addError("Update.Execute", err)
		return 0, err
	}
	return lastaffect, nil
}
func (thos *CiyMysql) Insert(csql *CiySQL, updata map[string]any) (int, error) {
	if csql.rawsql != "" {
		e := fmt.Errorf("forbid use rawsql")
		thos.addError("Insert.Init", e)
		return 0, e
	}
	if len(updata) == 0 {
		e := fmt.Errorf("updata empty")
		thos.addError("Insert.Init", e)
		return 0, e
	}
	columns := []string{}
	values := []string{}
	datas := []any{}

	for field, data := range updata {
		columns = append(columns, "`"+field+"`")
		if arr, ok := data.([]string); ok {
			values = append(values, arr[0])
		} else {
			values = append(values, "?")
			datas = append(datas, data)
		}
	}
	columnstr := strings.Join(columns, ",")
	valuestr := strings.Join(values, ",")
	sqlstr := "insert into " + csql.table + " (" + columnstr + ") values (" + valuestr + ")"
	lastid, _, err := thos.Execute(sqlstr, datas...)
	if err != nil {
		thos.addError("Insert.Execute", err)
		return 0, err
	}
	return lastid, nil
}

func (thos *CiyMysql) Delete(csql *CiySQL, argbackup ...int) (int, error) {
	if csql.rawsql != "" {
		e := fmt.Errorf("forbid use rawsql")
		thos.addError("Delete.Init", e)
		return 0, e
	}
	if csql.sqlwhere == "" {
		e := fmt.Errorf("no where")
		thos.addError("Delete.Init", e)
		return 0, e
	}
	var lastaffect int
	var err error
	var backup int = CIYDB_DELETE_BACKUP_NONE
	if len(argbackup) > 0 {
		backup = argbackup[0]
	}
	if backup == CIYDB_DELETE_BACKUP_TABLE { //写到备份表
		var columns []string
		columns, err = thos.getcolumn(csql)
		if err != nil {
			thos.addError("Delete.getcolumn", err)
			return 0, err
		}
		columnstr := strings.Join(columns, ",")
		sqlstr := "insert into " + csql.table + "_bak (" + columnstr + ") select " + columnstr + " from " + csql.table + csql.Buildwhere()
		_, _, err = thos.Execute(sqlstr)
		if err != nil {
			thos.addError("Delete.insertbak", err)
			return 0, err
		}
	}
	if backup == CIYDB_DELETE_BACKUP_FIELD { //修改删除标志位
		sqlstr := "update " + csql.table + " set deltimes=" + Tostr(Tostamp()) + csql.Buildwhere()
		_, lastaffect, err = thos.Execute(sqlstr, csql.tsmt...)
		if err != nil {
			thos.addError("Delete.deltimes", err)
			return 0, err
		}
	} else { //删除数据
		sqlstr := "delete from " + csql.table + csql.Buildwhere()
		_, lastaffect, err = thos.Execute(sqlstr, csql.tsmt...)
		if err != nil {
			thos.addError("Delete.delete", err)
			return 0, err
		}
	}
	return lastaffect, nil
}

func (thos *CiyMysql) Tran(fn func() error) (err error) {
	thos.Tx, err = thos.Db.Begin()
	if err != nil {
		thos.Tx = nil
		return err
	}
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("%v", r)
			thos.addError("Tran.catch", fmt.Errorf("%v", r))
			if thos.Tx != nil {
				err2 := thos.Tx.Rollback()
				CiyVars.Func_rollback++
				thos.Tx = nil
				if err2 != nil {
					thos.addError("Tran.catch.Rollback", err2)
					err = fmt.Errorf("%v[%v]", err2, err)
				}
			}
		} else {
			if thos.Tx != nil {
				err2 := thos.Tx.Commit()
				CiyVars.Func_commit++
				thos.Tx = nil
				if err2 != nil {
					thos.addError("Tran.catch.Commit", err2)
					err = err2
				}
			}
		}
	}()
	err = fn()
	if err != nil {
		if thos.Tx != nil {
			errtx := thos.Tx.Rollback()
			CiyVars.Func_rollback++
			if errtx != nil {
				thos.addError("Tran.Rollback", err)
			}
			thos.Tx = nil
		}
		thos.addError("Tran", err)
	} else {
		if thos.Tx != nil {
			errtx := thos.Tx.Commit()
			CiyVars.Func_commit++
			thos.Tx = nil
			if errtx != nil {
				thos.addError("Tran.Commit", errtx)
			}
		}
	}
	return nil
}
func (thos *CiyMysql) getcolumn(csql *CiySQL) ([]string, error) {
	rows, err := thos.Db.Query("select * from " + csql.table + " where id=0")
	if err != nil {
		return nil, fmt.Errorf("query: %v", err)
	}
	defer rows.Close()
	columns, err := rows.Columns()
	if err != nil {
		return nil, fmt.Errorf("columns: %v", err)
	}
	return columns, nil
}
func (thos *CiyMysql) calcolumn(csql *CiySQL) error {
	if csql.sqlcolumn == "" {
		csql.sqlcolumn = "*"
	}
	if !strings.HasPrefix(csql.sqlcolumn, "!") {
		return nil
	}
	columns, err := thos.getcolumn(csql)
	if err != nil {
		return err
	}

	csql.sqlcolumn = strings.TrimLeft(csql.sqlcolumn, "!")

	fields := strings.Split(csql.sqlcolumn, ",")
	excludedFields := make(map[string]bool)
	for _, field := range fields {
		excludedFields[strings.TrimSpace(field)] = true
	}
	var filteredColumns []string
	for _, column := range columns {
		if !excludedFields[column] {
			filteredColumns = append(filteredColumns, column)
		}
	}
	csql.sqlcolumn = strings.Join(filteredColumns, ",")
	return nil
}

// 错误err，没有数据返回正常空数组
func (thos *CiyMysql) Get(csql *CiySQL, argrowcount ...int) ([]map[string]any, int, error) {
	var err error
	rowcount := -1
	if len(argrowcount) > 0 {
		rowcount = argrowcount[0]
	}
	sqlstr := csql.rawsql
	if sqlstr == "" {
		err = thos.calcolumn(csql)
		if err != nil {
			thos.addError("Get.calcolumn", err)
			return nil, -1, err
		}
		sqlstr, _ = csql.Buildsql(csql.sqlcolumn)
		limit := csql.sqllimit
		if thos.Tx != nil {
			limit += " for update"
		}
		sqlstr += limit
	}
	csql.LastSQL = sqlstr
	var rows *sql.Rows
	if thos.Tx != nil {
		rows, err = thos.Tx.Query(sqlstr, csql.tsmt...)
	} else {
		rows, err = thos.Db.Query(sqlstr, csql.tsmt...)
	}
	if err != nil {
		thos.addError("Get.Query", err)
		return nil, -1, err
	}
	defer rows.Close()
	columns, err := rows.Columns()
	if err != nil {
		thos.addError("Get.Columns", err)
		return nil, -1, err
	}
	scanArgs := make([]any, len(columns))
	values := make([]any, len(columns))
	for i := range values {
		scanArgs[i] = &values[i]
	}
	ret := make([]map[string]any, 0)
	for rows.Next() {
		err := rows.Scan(scanArgs...)
		if err != nil {
			thos.addError("Get.Scan", err)
			return nil, -1, err
		}
		record := make(map[string]any, len(columns))
		for i, col := range values {
			if col != nil {
				switch col := col.(type) {
				case []byte:
					record[columns[i]] = string(col)
				default:
					record[columns[i]] = col
				}
			} else {
				record[columns[i]] = ""
			}
		}
		ret = append(ret, record)
	}
	if csql.rawsql == "" {
		if rowcount == -1 {
			sqlstr, _ := csql.Buildsql("count(*)")
			row := thos.Db.QueryRow(sqlstr, csql.tsmt...)
			if err := row.Err(); err != nil {
				thos.addError("Get.count.QueryRow", err)
				return ret, -1, err
			}
			var value any
			err := row.Scan(&value)
			if err != nil {
				thos.addError("Get.count.Scan", err)
				return ret, -1, err
			}
			return ret, Toint(value), nil
		}
	}
	return ret, rowcount, nil
}
func (thos *CiyMysql) Getraw(sqlstr string) ([]map[string]any, error) {
	var err error
	var rows *sql.Rows
	if thos.Tx != nil {
		rows, err = thos.Tx.Query(sqlstr)
	} else {
		rows, err = thos.Db.Query(sqlstr)
	}
	if err != nil {
		thos.addError("Get.Query", err)
		return nil, err
	}
	defer rows.Close()
	columns, err := rows.Columns()
	if err != nil {
		thos.addError("Get.Columns", err)
		return nil, err
	}
	scanArgs := make([]any, len(columns))
	values := make([]any, len(columns))
	for i := range values {
		scanArgs[i] = &values[i]
	}
	ret := make([]map[string]any, 0)
	for rows.Next() {
		err := rows.Scan(scanArgs...)
		if err != nil {
			thos.addError("Get.Scan", err)
			return nil, err
		}
		record := make(map[string]any, len(columns))
		for i, col := range values {
			if col != nil {
				switch col := col.(type) {
				case []byte:
					record[columns[i]] = string(col)
				default:
					record[columns[i]] = col
				}
			} else {
				record[columns[i]] = ""
			}
		}
		ret = append(ret, record)
	}
	return ret, nil
}
func (thos *CiyMysql) GetField(csql *CiySQL) (map[string]map[string]any, string) {
	retfield := map[string]map[string]any{}
	fshow := ""
	rowsfield, err := thos.Db.Query("show full fields from " + csql.table)
	if err != nil {
		thos.addError("GetField.Query", err)
		return retfield, fshow
	}
	defer rowsfield.Close()
	columns, err := rowsfield.Columns()
	if err != nil {
		thos.addError("GetField.Columns", err)
		return retfield, fshow
	}
	scanArgs := make([]any, len(columns))
	values := make([]any, len(columns))
	for i := range values {
		scanArgs[i] = &values[i]
	}
	for rowsfield.Next() {
		err := rowsfield.Scan(scanArgs...)
		if err != nil {
			thos.addError("GetField.Scan", err)
			return retfield, fshow
		}
		field := ""
		var comment string
		for i, col := range values {
			if columns[i] == "Field" {
				field = string(col.([]byte))
			}
			if columns[i] == "Comment" {
				comment = string(col.([]byte))
			}
		}
		if field != "" {
			if comment != "" && !strings.HasPrefix(comment, ",") {
				fshow += field + ","
			}
			retfield[field] = map[string]any{"c": comment}
		}
	}
	return retfield, strings.TrimRight(fshow, ",")
}
func (thos *CiyMysql) Getdbcodes(table, column string) []map[string]any {
	ret := make([]map[string]any, 0)
	rows, err := thos.Db.Query("show full fields from "+table+" where Field=?", column)
	if err != nil {
		thos.addError("Getdbcodes.Query", err)
		return ret
	}
	defer rows.Close()
	columns, err := rows.Columns()
	if err != nil {
		thos.addError("Getdbcodes.Columns", err)
		return ret
	}
	scanArgs := make([]any, len(columns))
	values := make([]any, len(columns))
	for i := range values {
		scanArgs[i] = &values[i]
	}
	for rows.Next() {
		err := rows.Scan(scanArgs...)
		if err != nil {
			thos.addError("Getdbcodes.Scan", err)
			return ret
		}
		var comment string
		for i, col := range values {
			if columns[i] == "Comment" {
				comment = string(col.([]byte))
			}
		}
		if comment != "" {
			ind := strings.Index(comment, ",TINT")
			if ind == -1 {
				ind = strings.Index(comment, ",TBIN")
			}
			if ind == -1 {
				ind = strings.Index(comment, ",BOOL")
			}
			if ind == -1 {
				return ret
			}
			exts := strings.Split(comment[ind+5:], ",")
			for i, ext := range exts {
				excos := strings.Split(ext, ".")
				if len(excos) > 1 {
					ret = append(ret, map[string]any{"id": excos[0], "name": excos[1]})
				} else if len(excos[0]) > 0 {
					ret = append(ret, map[string]any{"id": i, "name": excos[0]})
				}
			}
			if strings.Contains(comment, ",BOOL") {
				if len(ret) == 0 {
					ret = append(ret, map[string]any{"id": "1", "name": "✔"})
				}
				if len(ret) == 1 {
					ret = append(ret, map[string]any{"id": "2", "name": "✘"})
				}
			}
		}
	}
	return ret
}

// 错误err，没有数据返回err，只有有数据正常
func (thos *CiyMysql) Getone(csql *CiySQL) (map[string]any, error) {
	var err error
	sqlstr := csql.rawsql
	if sqlstr == "" {
		err = thos.calcolumn(csql)
		if err != nil {
			thos.addError("Getone.calcolumn", err)
			return nil, err
		}
		sqlstr, _ = csql.Buildsql(csql.sqlcolumn)
		limit := " limit 0,1"
		if thos.Tx != nil {
			limit += " for update"
		}
		sqlstr += limit
	}
	csql.LastSQL = sqlstr
	var rows *sql.Rows
	if thos.Tx != nil {
		rows, err = thos.Tx.Query(sqlstr, csql.tsmt...)
	} else {
		rows, err = thos.Db.Query(sqlstr, csql.tsmt...)
	}
	if err != nil {
		thos.addError("Getone.Query", err)
		return nil, err
	}
	defer rows.Close()
	columns, err := rows.Columns()
	if err != nil {
		thos.addError("Getone.Columns", err)
		return nil, err
	}
	scanArgs := make([]any, len(columns))
	values := make([]any, len(columns))
	for i := range values {
		scanArgs[i] = &values[i]
	}
	bdat := rows.Next()
	if !bdat {
		return nil, nil
	}
	err = rows.Scan(scanArgs...)
	if err != nil {
		thos.addError("Getone.Scan", err)
		return nil, err
	}
	record := make(map[string]any, len(columns))
	for i, col := range values {
		if col != nil {
			switch col := col.(type) {
			case []byte:
				record[columns[i]] = string(col)
			default:
				record[columns[i]] = col
			}
		} else {
			record[columns[i]] = ""
		}
	}
	return record, nil
}

// 无数据和错误都返回""，通过.Error判断
func (thos *CiyMysql) Get1(csql *CiySQL) any {
	var err error
	sqlstr := csql.rawsql
	if sqlstr == "" {
		col := csql.sqlcolumn
		if col == "" || col == "*" || col[0] == '!' {
			col = "count(*)"
		}
		var column string
		sqlstr, column = csql.Buildsql(col)
		limit := csql.sqllimit
		if limit == "" && !strings.Contains(column, "(") {
			limit = " limit 0,1"
		}
		if thos.Tx != nil {
			limit += " for update"
		}
		sqlstr += limit
	}

	csql.LastSQL = sqlstr
	var row *sql.Row
	if thos.Tx != nil {
		row = thos.Tx.QueryRow(sqlstr, csql.tsmt...)
	} else {
		row = thos.Db.QueryRow(sqlstr, csql.tsmt...)
	}
	if err := row.Err(); err != nil {
		thos.addError("Get1.QueryRow", err)
		return ""
	}
	var value any
	err = row.Scan(&value)
	if err != nil {
		thos.addError("Get1.Scan", err)
		return ""
	}
	if value == nil {
		return ""
	}
	return value
}
