package controller

import (
	"fmt"
	"io"
	"net"
	"net/http"
	"regexp"
	"strings"
	"time"

	"dataservices/commons/contextvar"
	"dataservices/commons/logger"
	"dataservices/commons/utils"
	dto "dataservices/internal/dto/dataapi_gateway"
	"dataservices/internal/entity"
	service "dataservices/internal/service/dataapi_gateway"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/tidbcloud/dataservices/idl/pbgen/proto/dataapi"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"go.uber.org/zap"
)

var (
	funcNameRegexp = regexp.MustCompile(`\.func\d+`)
)

func TestEndpointV2(c *gin.Context) {
	ctx := c.Request.Context()
	req := &dto.TestEndpointReqV2{}

	failResAndLog := func(err errs.Error) {
		innerFail(c, req, err)
	}

	err := parseParamFromBody(c, req)
	if err != nil {
		failResAndLog(err)
		return
	}

	rpcReq, err := dto.ConvertTestEndpointReqToProtoV2(req)
	if err != nil {
		failResAndLog(err)
		return
	}

	resp, err1 := GlobalDataAPIService.TestEndpointV2(ctx, rpcReq)
	if err1 != nil {
		failResAndLog(errs.NewInternalServerError(err1.Error(), err1))
		return
	}

	innerSucc(c, req, resp)
}

func ExecSqlTemplateV2(c *gin.Context) {
	ctx := c.Request.Context()
	req := &dto.ExecSqlTemplateReqV2{}

	failResAndLog := func(err errs.Error) {
		innerFail(c, req, err)
	}
	err := parseParamFromBody(c, req)
	if err != nil {
		failResAndLog(err)
		return
	}

	rpcReq, err := dto.ConvertExecSqlReqToProtoV2(req)
	if err != nil {
		failResAndLog(err)
		return
	}

	resp, err1 := GlobalDataAPIService.ExecSqlTemplateV2(ctx, rpcReq)
	if err1 != nil {
		failResAndLog(errs.NewInternalServerError(err1.Error(), err1))
		return
	}

	innerSucc(c, req, resp)
}

func CheckAppIDClusterPermission(c *gin.Context) {
	req := &dto.CheckAppIDClusterPermissionReq{}
	err := parseParamFromBody(c, req)
	if err != nil {
		c.JSON(http.StatusBadRequest, dto.CheckAppIDClusterPermissionResp{
			Pass:    false,
			Message: fmt.Sprintf("%s", err),
		})
		return
	}

	ctx := c.Request.Context()
	pass, e := service.HasClusterPermission(ctx, req.AppId, req.ClusterId)

	c.JSON(http.StatusOK, dto.CheckAppIDClusterPermissionResp{
		Pass:    pass,
		Message: fmt.Sprintf("%s", e),
	})
}

func ExecSql(c *gin.Context) {
	ctx := c.Request.Context()
	req := &dto.ExecSqlReq{}

	failResAndLog := func(err errs.Error) {
		innerFail(c, req, err)
	}

	err := parseParamFromBody(c, req)
	if err != nil {
		failResAndLog(err)
		return
	}

	rpcReq, err := dto.ConvertExecSqlReqToProto(req)
	if err != nil {
		failResAndLog(err)
		return
	}

	resp, err1 := GlobalDataAPIService.ExecSql(ctx, rpcReq)
	if err1 != nil {
		failResAndLog(err1.(errs.Error))
		return
	} else if resp.BaseResp.ErrCode != 0 && strings.Contains(errs.GetOrigErrMsgFromBaseResp(resp.BaseResp), "context deadline") {
		timeout := time.Millisecond * time.Duration(req.TimeoutMs)
		tilog.Error(ctx, "DS-Region-Inner-/exec/sql[post]-ExecSql Exec sql timeout", zap.Any("resp", resp))
		failResAndLog(errs.NewBadRequest(fmt.Sprintf("Execute sql timeout, timeout is %s", timeout.String()), nil))
		return
	}
	//tilog.Info(ctx, "ExecSql bytes_count", zap.Any("resp", resp))
	innerSucc(c, req, resp)
}

func GetDatabaseMeta(c *gin.Context) {
	ctx := c.Request.Context()
	startTime := time.Now()
	req := &dto.GetDatabaseMetaReq{}
	failResAndLog := func(err errs.Error) {
		c.JSON(err.HTTPCode(), dto.NewErrorExecSqlResp(int64(err.HTTPCode()), err.ErrMsg()))
		tilog.Error(ctx, "ExecSqlTemplate failed",
			zap.Any("req", req),
			zap.Duration("cost", time.Since(startTime)),
			zap.Error(err))
	}
	err := parseParamFromBody(c, req)
	if err != nil {
		failResAndLog(err)
		return
	}

	rpcReq, err := dto.ConvertGetDatabaseMetaReqToProto(req)
	if err != nil {
		failResAndLog(err)
		return
	}

	resp, err1 := GlobalDataAPIService.GetDatabaseMeta(ctx, rpcReq)
	if err1 != nil {
		failResAndLog(errs.NewInternalServerError(err1.Error(), err1))
		return
	}
	getDatabaseMetaResp := dto.ConvertProtoToGetDatabaseMetaResp(resp)
	httpCode := getHttpCodeFromBaseResp(resp.BaseResp)
	c.JSON(httpCode, getDatabaseMetaResp)
}

func CreateSession(c *gin.Context) {
	ctx := c.Request.Context()
	startTime := time.Now()
	req := &dto.CreateSessionReq{}
	failResAndLog := func(err errs.Error) {
		c.JSON(err.HTTPCode(), dto.NewErrorExecSqlResp(int64(err.HTTPCode()), err.ErrMsg()))
		tilog.Error(ctx, "ExecSqlTemplate failed",
			zap.Any("req", req),
			zap.Duration("cost", time.Since(startTime)),
			zap.Error(err))
	}
	err := parseParamFromBody(c, req)
	if err != nil {
		failResAndLog(err)
		return
	}

	rpcReq, err := dto.ConvertCreateSessionReqToProto(req)
	if err != nil {
		failResAndLog(err)
		return
	}

	resp, err1 := GlobalDataAPIService.CreateSession(ctx, rpcReq)
	if err1 != nil {
		failResAndLog(errs.NewInternalServerError(err1.Error(), err1))
		return
	}
	httpResp := dto.ConvertProtoToCreateSessionResp(resp)
	httpCode := getHttpCodeFromBaseResp(resp.BaseResp)
	c.JSON(httpCode, httpResp)
}

func CloseSession(c *gin.Context) {
	ctx := c.Request.Context()
	startTime := time.Now()
	req := &dto.CloseSessionReq{}
	failResAndLog := func(err errs.Error) {
		c.JSON(err.HTTPCode(), dto.NewErrorExecSqlResp(int64(err.HTTPCode()), err.ErrMsg()))
		tilog.Error(ctx, "ExecSqlTemplate failed",
			zap.Any("req", req),
			zap.Duration("cost", time.Since(startTime)),
			zap.Error(err))
	}
	err := parseParamFromBody(c, req)
	if err != nil {
		failResAndLog(err)
		return
	}

	rpcReq, err0 := dto.ConvertCloseSessionReqToProto(req)
	if err0 != nil {
		failResAndLog(errs.NewInternalServerError(err0.Error(), err0))
		return
	}

	resp, err1 := GlobalDataAPIService.CloseSession(ctx, rpcReq)
	if err1 != nil {
		failResAndLog(errs.NewInternalServerError(err1.Error(), err1))
		return
	}
	httpResp := dto.ConvertProtoToCloseSessionResp(resp)
	httpCode := getHttpCodeFromBaseResp(resp.BaseResp)
	c.JSON(httpCode, httpResp)
}

func ParseOneStmt(c *gin.Context) {
	ctx := c.Request.Context()
	startTime := time.Now()
	req := &dto.ParseOneStmtReq{}
	failResAndLog := func(err errs.Error) {
		c.JSON(err.HTTPCode(), dto.NewErrorExecSqlResp(int64(err.HTTPCode()), err.ErrMsg()))
		tilog.Error(ctx, "ExecSqlTemplate failed",
			zap.Any("req", req),
			zap.Duration("cost", time.Since(startTime)),
			zap.Error(err))
	}
	err := parseParamFromBody(c, req)
	if err != nil {
		failResAndLog(err)
		return
	}

	resp, err1 := GlobalDataAPIService.ParseOneStmt(ctx, &dataapi.ParseOneStmtReq{Sql: req.Sql})
	if err1 != nil {
		failResAndLog(errs.NewInternalServerError(err1.Error(), err1))
		return
	}
	httpResp := dto.ConvertProtoToParseOneStmtResp(resp)
	httpCode := getHttpCodeFromBaseResp(resp.BaseResp)
	c.JSON(httpCode, httpResp)
}

func getHttpCodeFromBaseResp(baseResp *base.BaseResp) int {
	if baseResp.ErrCode == 0 || baseResp.ErrCode == http.StatusOK {
		return http.StatusOK
	}
	tierr := errs.GetTiErrFromBaseResp(baseResp)
	httpCode := tierr.HTTPCode()
	if httpCode == http.StatusInternalServerError {
		httpCode = getHttpCodeFromRespCode(int(baseResp.ErrCode))
	}
	return httpCode
}

func getHttpCodeFromRespCode(code int) int {
	//code := int(resp.Data.Result.Code)
	if code == 0 || code == http.StatusOK {
		return http.StatusOK
	}
	if http.StatusText(code) != "" {
		return code
	}
	httpCode, exist := ErrsCodeToHttpCode[int64(code)]
	if !exist {
		httpCode = 500
	}
	return httpCode
}

func parseParamFromBody(c *gin.Context, param interface{}) errs.Error {
	if c.ContentType() != binding.MIMEJSON {
		return errs.NewBadRequest("Parse param failed, Only json formatted content is supported", nil)
	}
	err := c.ShouldBindBodyWith(param, binding.JSON)
	if err != nil && err != io.EOF {
		return errs.NewBadRequest(fmt.Sprintf("Parse param failed, error: %v", err.Error()), err)
	}
	return nil
}

func innerSucc(c *gin.Context, req any, resp *dataapi.ExecSqlRespV2) {
	ctx := c.Request.Context()
	startTime := contextvar.RequestBeginTime(ctx)
	endTime := time.Now()
	result := dto.ConvertProtoToExecSqlResp(resp)
	result.Data.Result.StartMs = startTime.UnixMilli()
	result.Data.Result.EndMs = endTime.UnixMilli()
	result.Data.Result.Latency = endTime.Sub(startTime).String()

	if testReq, ok := req.(*dto.TestEndpointReqV2); ok {
		if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
			if _, ok := resp.BaseResp.Tags["TiError_ORIG_ERR_MSG"]; ok {
				reportEndpointExecRecord(c, &entity.Endpoint{ID: testReq.EndpointId}, &result.Data, testReq.Credential.AccountID, true, "inner_test", resp.BaseResp.Tags["TiError_ORIG_ERR_MSG"])
			} else {
				reportEndpointExecRecord(c, &entity.Endpoint{ID: testReq.EndpointId}, &result.Data, testReq.Credential.AccountID, true, "inner_test", "")
			}
		} else {
			reportEndpointExecRecord(c, &entity.Endpoint{ID: testReq.EndpointId}, &result.Data, testReq.Credential.AccountID, true, "inner_test", "")
		}
		result.Data.Result.WarnCount = 0
		result.Data.Result.WarnMessages = nil
		result.Data.Result.Databases = nil
	}

	parentFN := utils.GetParentFuncName()
	tilog.Info(ctx, parentFN+" completed",
		logger.Redact("req", req),
		zap.Duration("cost", endTime.Sub(startTime)))

	httpCode := getHttpCodeFromBaseResp(resp.BaseResp)
	c.JSON(httpCode, result)
}

func innerFail(c *gin.Context, req any, err errs.Error) {
	ctx := c.Request.Context()
	startTime := contextvar.RequestBeginTime(ctx)
	endTime := time.Now()
	result := dto.NewErrorExecSqlResp(int64(err.HTTPCode()), err.ErrMsg())
	result.Data.Result.StartMs = startTime.UnixMilli()
	result.Data.Result.EndMs = endTime.UnixMilli()
	result.Data.Result.Latency = endTime.Sub(startTime).String()

	if testReq, ok := req.(*dto.TestEndpointReqV2); ok {
		reportEndpointExecRecord(c, &entity.Endpoint{ID: testReq.EndpointId}, &result.Data, testReq.Credential.AccountID, true, "inner_test", "")
	}

	parentFN := utils.GetParentFuncName()
	if funcNameRegexp.MatchString(parentFN) {
		parentFN = utils.GetGrandFuncName()
	}
	logger.LogWithError(ctx, err, "DS-Region-Inner-"+parentFN+" failed",
		zap.Any("req", req),
		zap.Duration("cost", endTime.Sub(startTime)))

	c.JSON(err.HTTPCode(), result)
}

func ResolveHost(c *gin.Context) {
	host := c.Query("host")
	ip, err := resolveHostIp(host)
	var resp dto.ResolveHostResp
	if err != nil {
		resp.Code = err.BizCode()
		resp.Message = err.ErrMsg()
		c.JSON(http.StatusOK, resp)
		return
	}

	resp.Code = 0
	resp.Message = "OK"
	resp.IP = ip
	c.JSON(http.StatusOK, resp)
}

func resolveHostIp(host string) (string, errs.Error) {
	if host == "" {
		return "", errs.NewBadRequest("host is required", nil)
	}
	ipAddr, err := net.ResolveIPAddr("ip", host)
	if err != nil {
		if strings.Contains(err.Error(), "no such host") {
			return "", nil
		}
		return "", errs.NewBadRequest("failed to resolve host ip", err)
	}
	if ipAddr == nil {
		return "", errs.NewBadRequest("no IP address found for host "+host, nil)
	}
	return ipAddr.String(), nil
}
