package dto

import (
	"fmt"
	"time"

	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"

	"github.com/tidbcloud/tiinf/errs"

	"dataservices/commons/constants"
	"dataservices/commons/utils"
	"dataservices/internal/entity"

	"github.com/tidbcloud/dataservices/idl/pbgen/proto/dataapi"
)

type BasicRes struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

type AdvancedRes struct {
	Type string         `json:"type"`
	Data entity.DataRes `json:"data"`
}

type BatchAdvancedRes struct {
	Type string              `json:"type"`
	Data entity.BatchDataRes `json:"data"`
}

type CheckAppIDClusterPermissionResp struct {
	Pass    bool   `json:"pass"`
	Message string `json:"message"`
}

type ExecSqlResp struct {
	Data       entity.DataRes `json:"data"`
	BytesCount int64          `json:"bytes_count"`
}

func (r *ExecSqlResp) GetErrCode() int {
	return int(r.Data.Result.Code)
}
func (r *ExecSqlResp) GetErrMsg() string {
	return r.Data.Result.Message
}

const (
	TypeCredentialAuth0   = "AUTH0"
	TypeCredentialApikey  = "APIKEY"
	TypeCredentialAccount = "ACCOUNT"
	TypeCredentialEmail   = "EMAIL"
)

type Credential struct {
	Type      string `json:"type"`
	Key       string `json:"key"`
	AccountID string `json:"account_id"`
}

type TestEndpointReq struct {
	ProjectId           uint64        `json:"project_id"`
	AppId               uint64        `json:"app_id"`
	EndpointId          uint64        `json:"endpoint_id"`
	EndpointType        string        `json:"endpoint_type"`
	Args                []EndpointArg `json:"args"`
	ReturnAllStmtResult bool          `json:"return_all_stmt_result"` // currently not support
	Credential          Credential    `json:"credential"`
}

type TestEndpointReqV2 struct {
	ProjectId           uint64           `json:"project_id"`
	AppId               uint64           `json:"app_id"`
	EndpointId          uint64           `json:"endpoint_id"`
	ArgValues           []map[string]any `json:"arg_values"`
	ReturnAllStmtResult bool             `json:"return_all_stmt_result"` // currently not support
	Credential          Credential       `json:"credential"`
}

type EndpointArg struct {
	Name         string `json:"name"`
	Type         string `json:"type"` // string/bigint/bigint unsigned/double/array/enum/replace, if type is array/enum, should also specify the value-type.
	Required     int8   `json:"required"`
	Description  string `json:"description"`
	Value        any    `json:"value"`
	DefaultValue any    `json:"default"`

	Enum      []string `json:"enum,omitempty"`
	ItemType  string   `json:"item_type,omitempty"`
	Maximum   int64    `json:"maximum,omitempty"`
	Minimum   int64    `json:"minimum,omitempty"`
	Maxlength int64    `json:"maxlength,omitempty"`
	Minlength int64    `json:"minlength,omitempty"`
}

type SqlArg struct {
	Tp    string `json:"type"`
	Value string `json:"value"`
	Name  string `json:"name"`
}
type ExecSqlTemplateReqV2 struct {
	ProjectId           uint64                  `json:"project_id"`
	ClusterId           uint64                  `json:"cluster_id"`
	Database            string                  `json:"database"`
	SqlTemplate         string                  `json:"sql_template"`
	Settings            entity.EndpointSettings `json:"settings"`
	Args                []entity.EndpointArgs   `json:"args"`
	ArgValues           []map[string]any        `json:"arg:_values"`
	SessionId           string                  `json:"session_id"`
	LimitRows           uint64                  `json:"limit_rows"`
	TimeoutMs           uint64                  `json:"timeout_ms"`
	ReturnAllStmtResult bool                    `json:"return_all_stmt_result"` // currently not support
	Credential          Credential              `json:"credential"`
	Source              string                  `json:"source"` // dataservice-console
}

type ExecSqlTemplateReq struct {
	ProjectId           uint64        `json:"project_id"`
	ClusterId           uint64        `json:"cluster_id"`
	Database            string        `json:"database"`
	SqlTemplate         string        `json:"sql_template"`
	Args                []EndpointArg `json:"args"`
	SessionId           string        `json:"session_id"`
	LimitRows           uint64        `json:"limit_rows"`
	TimeoutMs           uint64        `json:"timeout_ms"`
	EnablePagination    int           `json:"enable_pagination"`
	ReturnAllStmtResult bool          `json:"return_all_stmt_result"` // currently not support
	Credential          Credential    `json:"credential"`
}

type CheckAppIDClusterPermissionReq struct {
	ClusterId uint64 `json:"cluster_id"`
	AppId     uint64 `json:"app_id"`
}

type ExecSqlReq struct {
	ProjectId           uint64     `json:"project_id"`
	ClusterId           uint64     `json:"cluster_id"`
	Database            string     `json:"database"`
	Sql                 string     `json:"sql"`
	SessionId           string     `json:"session_id"`
	LimitRows           uint64     `json:"limit_rows"`
	TimeoutMs           uint64     `json:"timeout_ms"`
	ReturnAllStmtResult bool       `json:"return_all_stmt_result"` // currently not support
	Credential          Credential `json:"credential"`
	Source              string     `json:"source"`
}

type GetDatabaseMetaReq struct {
	ProjectId  uint64     `json:"project_id"`
	ClusterId  uint64     `json:"cluster_id"`
	Database   string     `json:"database"`
	Credential Credential `json:"credential"`
}

type GatewayBaseResp struct {
	Code    int64  `json:"code"`
	Message string `json:"message"`
}

func (r *GatewayBaseResp) GetErrCode() int {
	return int(r.Code)
}
func (r *GatewayBaseResp) GetErrMsg() string {
	return r.Message
}

type GetDatabaseMetaResp struct {
	GatewayBaseResp
	Metas []*TableMeta `json:"metas"`
}

type TableMeta struct {
	Table    string   `json:"table"`
	Columns  []string `json:"columns"`
	Database string   `json:"database"`
}

type CreateSessionReq struct {
	ProjectId  uint64     `json:"project_id"`
	ClusterId  uint64     `json:"cluster_id"`
	Database   string     `json:"database"`
	ExpireSec  uint64     `json:"expire_sec"`
	Credential Credential `json:"credential"`
}

type CreateSessionResp struct {
	GatewayBaseResp
	SessionId string `json:"session_id"`
}

type CloseSessionReq struct {
	SessionId  string     `json:"session_id"`
	ClusterId  uint64     `json:"cluster_id"`
	ProjectId  uint64     `json:"project_id"`
	Credential Credential `json:"credential"`
}
type CloseSessionResp struct {
	GatewayBaseResp
}

type ParseOneStmtReq struct {
	Sql string `json:"sql"`
}

type ParseOneStmtResp struct {
	Code     int64            `json:"code"`
	Message  string           `json:"message"`
	StmtType dataapi.StmtType `json:"stmt_type"`
}

type ResolveHostResp struct {
	GatewayBaseResp
	IP string `json:"ip"`
}

func NewErrorDataRes(code int64, msg string) entity.DataRes {
	return entity.DataRes{
		Result: entity.Result{
			Code:    code,
			Message: msg,
		},
	}
}

func NewErrorExecSqlResp(code int64, msg string) *ExecSqlResp {
	return &ExecSqlResp{
		Data: NewErrorDataRes(code, msg),
	}
}

func NewErrorAdvancedRes(code int64, msg string, epResTypes ...string) *AdvancedRes {
	epResType := constants.EndpointResTypeSQL
	if len(epResTypes) > 0 {
		epResType = epResTypes[0]
	}
	return &AdvancedRes{
		Type: epResType,
		Data: entity.DataRes{
			Columns: []entity.Column{},
			Rows:    []entity.Row{},
			Result: entity.Result{
				Code:    code,
				Message: msg,
			},
		},
	}
}

func ConvertErrorToAdvancedRes(httpCode int, message string, epResTypes ...string) *AdvancedRes {
	return NewErrorAdvancedRes(int64(httpCode), message, epResTypes...)
}

func ConvertDataAPIResToAdvanced(req *dataapi.BasicEndpointReqV2, resp *dataapi.ExecSqlRespV2, epResType string) *AdvancedRes {
	if resp.GetBaseResp().GetErrCode() != 0 {
		return NewErrorAdvancedRes(resp.GetBaseResp().GetErrCode(), resp.GetBaseResp().GetErrMsg(), epResType)
	}
	return ConvertToAdvancedRes(resp.GetStmtResults(), epResType)
}

func ConvertToAdvancedRes(stmtResults []*dataapi.ExecStmtResult, epResTypes ...string) *AdvancedRes {
	epResType := constants.EndpointResTypeSQL
	if len(epResTypes) > 0 {
		epResType = epResTypes[0]
	}
	res := &AdvancedRes{
		Type: epResType,
	}
	if len(stmtResults) == 0 {
		return res
	}

	// Only keep the last sql resp.
	stmtResult := stmtResults[len(stmtResults)-1]
	res.Data = ConvertToDataRes(stmtResult)
	return res
}

func ConvertToDataRes(stmtResult *dataapi.ExecStmtResult) entity.DataRes {
	startTime := time.UnixMilli(stmtResult.StartMs)
	endTime := time.UnixMilli(stmtResult.EndMs)
	latency := endTime.Sub(startTime)
	convertColumns := ConvertToExecSqlRespColumns(stmtResult.Columns)
	res := entity.DataRes{
		Columns: convertColumns,
		Rows:    convertToExecSqlRespRows(convertColumns, stmtResult.Rows, stmtResult.StmtType),
		Result: entity.Result{
			Code:      stmtResult.ErrCode,
			Message:   stmtResult.ErrMessage,
			StartMs:   stmtResult.StartMs,
			EndMs:     stmtResult.EndMs,
			Latency:   latency.String(),
			RowCount:  stmtResult.RowNum,
			RowAffect: stmtResult.Affect,
			Limit:     stmtResult.LimitRows,
			Databases: stmtResult.Databases,
		},
	}
	if stmtResult.ErrCode == 0 {
		res.Result.Code = 200
		res.Result.Message, res.Result.WarnMessages = ResultMessage(stmtResult, latency)
		res.Result.WarnCount = int(stmtResult.WarnCount)
	}

	return res
}

var complexSuffix = map[bool]string{true: "s"}

func ResultMessage(stmtResult *dataapi.ExecStmtResult, latency time.Duration) (string, []string) {
	var (
		res          string
		warnMessages []string
	)
	switch stmtResult.StmtType {
	case dataapi.StmtType_Select, dataapi.StmtType_Show, dataapi.StmtType_Explain:
		res = "Query OK!"
	case dataapi.StmtType_Use:
		res = "Database changed!"
	default:
		tips := fmt.Sprintf("%d row%s affected", stmtResult.Affect, complexSuffix[stmtResult.Affect == 0 || stmtResult.Affect > 1])
		if stmtResult.WarnCount > 0 {
			tips += fmt.Sprintf(", %d warning%s", stmtResult.WarnCount, complexSuffix[stmtResult.WarnCount > 1])
		}
		res = fmt.Sprintf("Query OK, %s (%.3f sec)", tips, latency.Seconds())
	}

	for _, warnMessage := range stmtResult.WarnMessages {
		warnMessages = append(warnMessages, fmt.Sprintf("(%s) %s;", warnMessage.Code, warnMessage.Message))
	}

	return res, warnMessages
}
func BatchConvertToExecSqlRespV2(res *BatchAdvancedRes, stmtType dataapi.StmtType) *dataapi.ExecSqlRespV2 {
	rows := make([]*dataapi.Row, 0)
	for _, v := range res.Data.Rows {
		item := make([]string, 0)
		item = append(item, v.Index)
		item = append(item, v.Success)
		item = append(item, v.Message)
		item = append(item, v.AutoIncrementID)
		rows = append(rows, &dataapi.Row{Items: item})
	}
	ele := &dataapi.ExecStmtResult{
		ErrCode:    res.Data.Result.Code,
		ErrMessage: res.Data.Result.Message,
		Rows:       rows,
		Columns:    nil,
		StartMs:    res.Data.Result.StartMs,
		EndMs:      res.Data.Result.EndMs,
		Affect:     res.Data.Result.RowAffect,
		RowNum:     res.Data.Result.RowCount,
		Databases:  res.Data.Result.Databases,
		LimitRows:  res.Data.Result.Limit,
		StmtType:   stmtType,
	}
	results := make([]*dataapi.ExecStmtResult, 1)
	results[0] = ele
	return &dataapi.ExecSqlRespV2{
		StmtResults: results,
		BaseResp: &base.BaseResp{
			ErrCode: 0,
			ErrMsg:  "ok",
		},
	}
}

func ConvertToBatchAdvancedRes(stmtResult *dataapi.ExecStmtResult, rows []entity.BatchRowsStr, epResTypes ...string) *BatchAdvancedRes {
	epResType := constants.EndpointResTypeSQL
	if len(epResTypes) > 0 {
		epResType = epResTypes[0]
	}
	res := &BatchAdvancedRes{
		Type: epResType,
	}

	res.Data = ConvertToBatchDataRes(stmtResult, rows)
	return res
}

func ConvertToBatchDataRes(stmtResult *dataapi.ExecStmtResult, rows []entity.BatchRowsStr) entity.BatchDataRes {
	startTime := time.UnixMilli(stmtResult.StartMs)
	endTime := time.UnixMilli(stmtResult.EndMs)
	latency := endTime.Sub(startTime)
	convertColumns := ConvertToExecSqlRespColumns(stmtResult.Columns)
	res := entity.BatchDataRes{
		Columns: convertColumns,
		Rows:    rows,
		Result: entity.Result{
			Code:      stmtResult.ErrCode,
			Message:   stmtResult.ErrMessage,
			StartMs:   stmtResult.StartMs,
			EndMs:     stmtResult.EndMs,
			Latency:   latency.String(),
			RowCount:  stmtResult.RowNum,
			RowAffect: stmtResult.Affect,
			Limit:     stmtResult.LimitRows,
			Databases: stmtResult.Databases,
		},
	}
	if stmtResult.ErrCode == 0 {
		//case dataapi.StmtType_Insert:
		//	res.Result.Message = "Insert completed"
		//case dataapi.StmtType_Update:
		//	res.Result.Message = "Update completed"
		//case dataapi.StmtType_Delete:
		//	res.Result.Message = "Delete completed"
		res.Result.Code = 200
		res.Result.Message = fmt.Sprintf("Query completed, %v rows affected (%.3f sec)", stmtResult.Affect, latency.Seconds())
	}
	return res
}

type ColumnArray struct {
	IsDup     bool
	IndexList []int
}

func ConvertToExecSqlRespColumns(cols []*dataapi.Column) []entity.Column {
	result := make([]entity.Column, len(cols))
	uniqueCol := make(map[string]ColumnArray)
	dupCol := make(map[int]int)
	for i, col := range cols {
		if len(uniqueCol[col.Name].IndexList) >= 1 {
			dupArray := uniqueCol[col.Name].IndexList
			dupArray = append(dupArray, i)
			uniqueCol[col.Name] = ColumnArray{
				IsDup:     true,
				IndexList: dupArray,
			}
		} else {
			initArray := make([]int, 0)
			initArray = append(initArray, i)
			uniqueCol[col.Name] = ColumnArray{
				IsDup:     false,
				IndexList: initArray,
			}
		}
	}

	for i, col := range cols {
		if ok := uniqueCol[col.Name].IsDup; ok {
			for labelIndex, colIndex := range uniqueCol[col.Name].IndexList {
				if i == colIndex {
					dupCol[i] = labelIndex
					break
				}
			}

		} else {
			dupCol[i] = 0
		}
	}
	for i, col := range cols {
		if dupCol[i] == 0 {
			result[i] = entity.Column{
				Col:      col.Name,
				DataType: col.DataType,
				Nullable: col.Nullable,
			}
		} else {
			result[i] = entity.Column{
				Col:      col.Name + "_" + fmt.Sprintf("%d", dupCol[i]),
				DataType: col.DataType,
				Nullable: col.Nullable,
			}
		}

	}
	return result
}

func convertToExecSqlRespRows(cols []entity.Column, rows []*dataapi.Row, stmtType dataapi.StmtType) []entity.Row {
	rowRes := make([]entity.Row, 0)
	for _, row := range rows {
		if row == nil {
			continue
		}
		dbRow := entity.Row{}
		for i := range row.Items {
			col := "error"
			if len(cols) == 0 && len(rows) > 0 {
				switch i {
				case 0:
					col = "index"
					dbRow[col] = row.Items[i]
				case 1:
					col = "success"
					dbRow[col] = row.Items[i]
				case 2:
					col = "message"
					dbRow[col] = row.Items[i]
				case 3:
					col = "auto_increment_id"
					if row.Items[i] == "0" {
						dbRow[col] = "null"
					} else {
						dbRow[col] = row.Items[i]
					}
				default:
					col = "other"
				}
			} else {
				col = cols[i].Col
				dbRow[col] = row.Items[i]
			}

		}
		rowRes = append(rowRes, dbRow)
	}
	return rowRes
}

func ConvertProtoToExecSqlResp(proto *dataapi.ExecSqlRespV2) ExecSqlResp {
	if proto.BaseResp != nil && proto.BaseResp.ErrCode != 0 {
		return ExecSqlResp{Data: NewErrorDataRes(proto.BaseResp.ErrCode, proto.BaseResp.ErrMsg)}
	}
	if len(proto.StmtResults) == 0 {
		return ExecSqlResp{}
	}
	stmtResult := proto.StmtResults[len(proto.StmtResults)-1]
	return ExecSqlResp{Data: ConvertToDataRes(stmtResult), BytesCount: proto.BytesCount}
}

func ConvertTestEndpointReqToProtoV2(req *TestEndpointReqV2) (*dataapi.TestEndpointReqV2, errs.Error) {
	credential, err := ConvertCredentialToProto(req.Credential)
	if err != nil {
		return nil, err
	}
	return &dataapi.TestEndpointReqV2{
		ProjectId:           req.ProjectId,
		AppId:               req.AppId,
		EndpointId:          req.EndpointId,
		ArgValues:           utils.ToJson(req.ArgValues),
		ReturnAllStmtResult: req.ReturnAllStmtResult,
		Credential:          credential,
	}, nil
}

func ConvertExecSqlReqToProtoV2(req *ExecSqlTemplateReqV2) (*dataapi.ExecSqlTemplateReqV2, errs.Error) {
	credential, err := ConvertCredentialToProto(req.Credential)
	if err != nil {
		return nil, err
	}
	return &dataapi.ExecSqlTemplateReqV2{
		ProjectId:           req.ProjectId,
		ClusterId:           req.ClusterId,
		Database:            req.Database,
		SqlTemplate:         req.SqlTemplate,
		Args:                utils.ToJson(req.Args),
		ArgValues:           utils.ToJson(req.ArgValues),
		SessionId:           req.SessionId,
		ReturnAllStmtResult: req.ReturnAllStmtResult,
		Credential:          credential,
		Settings:            ConvertEndpointSettingToProto(req.Settings),
		Source:              req.Source,
	}, nil
}

func ConvertEndpointSettingToProto(settings entity.EndpointSettings) *dataapi.Settings {
	return &dataapi.Settings{
		Timeout:          uint64(settings.TimeOut),
		RowLimit:         uint64(settings.RowsLimit),
		CacheEnabled:     int32(settings.CacheEnabled),
		CacheTtl:         uint64(settings.CacheTTL),
		EnablePagination: int32(settings.EnablePagination),
	}
}

func ConvertEndpointSettingFromProto(settings *dataapi.Settings) entity.EndpointSettings {
	return entity.EndpointSettings{
		TimeOut:          int(settings.Timeout),
		RowsLimit:        int(settings.RowLimit),
		CacheEnabled:     int8(settings.CacheEnabled),
		CacheTTL:         int(settings.CacheTtl),
		EnablePagination: int(settings.EnablePagination),
	}
}

func ConvertExecSqlReqToProto(req *ExecSqlReq) (*dataapi.BasicEndpointReqV2, errs.Error) {
	credential, err := ConvertCredentialToProto(req.Credential)
	if err != nil {
		return nil, err
	}
	return &dataapi.BasicEndpointReqV2{
		ProjectId:           req.ProjectId,
		ClusterId:           req.ClusterId,
		Database:            req.Database,
		Sql:                 req.Sql,
		SessionId:           req.SessionId,
		LimitRows:           req.LimitRows,
		TimeoutMs:           req.TimeoutMs,
		ReturnAllStmtResult: req.ReturnAllStmtResult,
		Credential:          credential,
		Source:              req.Source,
	}, nil
}

func ConvertGetDatabaseMetaReqToProto(req *GetDatabaseMetaReq) (*dataapi.GetDatabaseMetaReq, errs.Error) {
	credential, err := ConvertCredentialToProto(req.Credential)
	if err != nil {
		return nil, err
	}
	return &dataapi.GetDatabaseMetaReq{
		ProjectId:  req.ProjectId,
		ClusterId:  req.ClusterId,
		Database:   req.Database,
		Credential: credential,
	}, nil
}

func ConvertProtoToGetDatabaseMetaResp(proto *dataapi.GetDatabaseMetaResp) *GetDatabaseMetaResp {
	resp := &GetDatabaseMetaResp{
		Metas: nil,
	}
	if proto.BaseResp != nil {
		resp.Code = proto.BaseResp.ErrCode
		resp.Message = proto.BaseResp.ErrMsg
	}
	for _, meta := range proto.Metas {
		resp.Metas = append(resp.Metas, &TableMeta{
			Table:    meta.Table,
			Columns:  meta.Columns,
			Database: meta.Database,
		})
	}
	return resp
}

func ConvertCreateSessionReqToProto(req *CreateSessionReq) (*dataapi.CreateSessionReq, errs.Error) {
	credential, err := ConvertCredentialToProto(req.Credential)
	if err != nil {
		return nil, err
	}
	return &dataapi.CreateSessionReq{
		ProjectId:  req.ProjectId,
		ClusterId:  req.ClusterId,
		Database:   req.Database,
		ExpireSec:  req.ExpireSec,
		Credential: credential,
	}, nil
}

func ConvertProtoToCreateSessionResp(proto *dataapi.CreateSessionResp) *CreateSessionResp {
	resp := &CreateSessionResp{
		SessionId: proto.SessionId,
	}
	if proto.BaseResp != nil {
		resp.Code = proto.BaseResp.ErrCode
		resp.Message = proto.BaseResp.ErrMsg
	}
	return resp
}

func ConvertCloseSessionReqToProto(req *CloseSessionReq) (*dataapi.CloseSessionReq, error) {
	credential, err := ConvertCredentialToProto(req.Credential)
	if err != nil {
		return nil, err
	}
	return &dataapi.CloseSessionReq{
		SessionId:  req.SessionId,
		ClusterId:  req.ClusterId,
		ProjectId:  req.ProjectId,
		Credential: credential,
	}, nil
}

func ConvertProtoToCloseSessionResp(proto *dataapi.CloseSessionResp) *CloseSessionResp {
	resp := &CloseSessionResp{}
	if proto.BaseResp != nil {
		resp.Code = proto.BaseResp.ErrCode
		resp.Message = proto.BaseResp.ErrMsg
	}
	return resp
}

func ConvertProtoToParseOneStmtResp(proto *dataapi.ParseOneStmtResp) *ParseOneStmtResp {
	resp := &ParseOneStmtResp{
		StmtType: proto.StmtType,
	}
	if proto.BaseResp != nil {
		resp.Code = proto.BaseResp.ErrCode
		resp.Message = proto.BaseResp.ErrMsg
	}
	return resp
}

func ConvertCredentialToProto(c Credential) (*dataapi.Credential, errs.Error) {
	if c.Type == "" && c.Key == "" {
		return &dataapi.Credential{}, nil
	}
	crtType := dataapi.CrtType(dataapi.CrtType_value[c.Type])
	return &dataapi.Credential{
		Type:      crtType,
		Key:       c.Key,
		AccountId: c.AccountID,
	}, nil
}
