package upload

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"time"

	"github.com/pingcap/diag/pkg/utils"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/api/common"
	error22 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/config"
	_const "github.com/pingcap-inc/clinic/pkg/const"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/task_manager"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	common2 "github.com/pingcap-inc/clinic/pkg/common"
	"github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/upload_file/manager"
)

type UploadService struct {
	manager        *manager.FileStoreageManager
	taskManagerCli *task_manager.TaskManagerClient
	cfg            *config.Config
}

func NewUploadService(cfg *config.Config, manager *manager.FileStoreageManager, taskManagerCli *task_manager.TaskManagerClient) *UploadService {
	return &UploadService{
		cfg:            cfg,
		manager:        manager,
		taskManagerCli: taskManagerCli,
	}
}

func (s *UploadService) UploadFile(context *gin.Context) {
	// auth
	fileUUID := context.Query("uuid")
	if fileUUID == "" {
		log.Warn("empty file uuid parameter", zap.String("file", fileUUID))
		_ = context.Error(error22.ErrBadRequest.New("bad parameter"))
		return
	}

	partseq, err := strconv.ParseInt(context.Query("sequence"), 10, 32)
	if err != nil {
		log.Warn("format of sequence is wrong", zap.String("file", fileUUID), zap.Error(err))
		_ = context.Error(error22.ErrBadRequest.New("bad parameter"))
		return
	}

	partLen, err := strconv.ParseInt(context.Query("length"), 10, 64)
	if err != nil {
		log.Warn("format of length is wrong", zap.String("file", fileUUID), zap.Error(err))
		_ = context.Error(error22.ErrBadRequest.New("bad parameter"))
		return
	}

	if partLen == 0 {
		log.Warn("empty part content", zap.String("file", fileUUID), zap.Error(err))
		_ = context.Error(error22.ErrBadRequest.New("bad parameter"))
		return
	}

	file, path, err := uploadBody(context, fileUUID)
	defer func() {
		if file != nil {
			_ = file.Close()
		}

		if path != "" {
			_ = os.Remove(path)
		}
	}()

	if err != nil {
		log.Error("get upload body failed", zap.String("file", fileUUID), zap.Error(err))
		if err := model.UpdateUploadStatus(fileUUID, db.StatusFailed); err != nil {
			log.Error("update status of upload files failed", zap.String("fileUUID", fileUUID), zap.Error(err))
		}

		_ = context.Error(error22.ErrInternal.New("upload failed"))
		return
	}

	ierr := s.manager.UploadFile(fileUUID, file, int(partseq), uint64(partLen))
	if ierr != nil {
		log.Error("upload file failed", zap.String("file", fileUUID), zap.Error(ierr))
		if err := model.UpdateUploadStatus(fileUUID, db.StatusFailed); err != nil {
			log.Error("update status of upload files failed", zap.String("fileUUID", fileUUID), zap.Error(err))
		}

		if ierr.IsConflictErr() {
			_ = context.Error(error22.ErrBadRequest.New("%s", ierr.Error()))
		} else {
			_ = context.Error(error22.ErrInternal.New("upload failed, please try again later"))
		}

		return
	}

	context.JSON(http.StatusOK, common.SuccessResponse())
}

func uploadBody(context *gin.Context, fileUUID string) (io.ReadSeekCloser, string, error) {
	if context.GetHeader("Content-Type") == "application/octet-stream" {
		log.Info("use octet-stream header to upload data")
		file, err := os.CreateTemp("", "")
		if err != nil {
			log.Error("create tmp file failed", zap.Error(err))
			return nil, "", err
		}

		reader := context.Request.Body
		defer func() {
			reader.Close()
		}()
		n, err := io.Copy(file, reader)
		if err != nil {
			log.Error("copy body to tmp file failed", zap.String("fileUUID", fileUUID), zap.Int64("readBytes", n), zap.Error(err))
			return nil, file.Name(), err
		}

		_, err = file.Seek(0, 0)
		if err != nil {
			log.Error("seek tmp file failed", zap.Error(err))
			return nil, file.Name(), err
		}

		return file, file.Name(), nil
	}

	log.Info("use multipart header to upload data", zap.String("Content-Type", context.GetHeader("Content-Type")))
	file, _, err := context.Request.FormFile("file")
	return file, "", err
}

func (s *UploadService) PreCreate(context *gin.Context) {
	data := context.GetHeader(common.UserHeader)

	var val common.ApiUserContext
	err := json.Unmarshal([]byte(data), &val)
	if err != nil {
		log.Error("fetch api user context failed", zap.Error(err))
		_ = context.Error(error22.ErrBadRequest.New("lost context"))
		return
	}

	fileUUID := context.Query("uuid")
	if fileUUID == "" {
		log.Warn("empty file uuid parameter")
		_ = context.Error(error22.ErrBadRequest.New("bad parameter"))
		return
	}

	log.Info("upload file package", zap.String("fileUUID", fileUUID), zap.Uint("user", val.UserID))
	//s.auditCli.Do(auditlog.AuditLog{
	//	Email:     "",
	//	UserName:  fmt.Sprintf("%d", val.UserID),
	//	Resource:  fmt.Sprintf("/upload/%s", fileUUID),
	//	Operator:  "POST",
	//	Timestamp: time.Now().UnixNano() / 1000000,
	//})
	//user, _, ok := context.Request.BasicAuth()
	//if ok {
	//	log.Info("upload file package", zap.String("fileUUID", fileUUID), zap.String("user", user))
	//	s.auditCli.Do(auditlog.AuditLog{
	//		Email:     user,
	//		UserName:  user,
	//		Resource:  fmt.Sprintf("/upload/%s", fileUUID),
	//		Operator:  "POST",
	//		Timestamp: time.Now().UnixNano() / 1000000,
	//	})
	//}

	fileLen, err := strconv.ParseInt(context.Query("length"), 10, 64)
	if err != nil {
		log.Warn("format of file len is wrong")
		_ = context.Error(error22.ErrBadRequest.New("bad parameter"))
		return
	}

	if fileLen > common2.MaxFileSizeInBytes {
		log.Warn("too large file", zap.Int64("filelen", fileLen))
		_ = context.Error(error22.ErrBadRequest.New("too large file"))
		return
	}

	originalName := context.Query("filename")
	if originalName == "" {
		log.Warn("empty file originalName parameter")
		_ = context.Error(error22.ErrBadRequest.New("empty file name"))
		return
	}

	clusterID := context.Query(_const.ClusterIDParameter)
	if clusterID == "" {
		_ = context.Error(error22.ErrBadRequest.New("invalid cluster id"))
		return
	}

	if !checkClusterPermission(clusterID, val.OrgID) {
		_ = context.Error(error22.ErrBadRequest.New("the cluster has been created in other organizations"))
		return
	}

	dirsizeStr := context.Query(_const.ClusterDirSize)
	size, _ := strconv.Atoi(dirsizeStr)

	uf := &db.UploadFile{
		Model: gorm.Model{
			CreatedAt: time.Now(),
			UpdatedAt: time.Now(),
		},
		ClusterName:      context.Query(_const.ClusterNameParameter),
		FileUUID:         fileUUID,
		Alias:            context.Query("alias"),
		ClusterID:        clusterID,
		Creator:          val.UserID,
		OrgID:            val.OrgID,
		FileLen:          fileLen,
		DirSize:          size,
		FileOriginalName: originalName,
		Rebuild:          context.Query(_const.ClusterRebuildParameter),
		ClusterType:      context.Query(_const.ClusterTypeParameter),
	}

	begin, err := utils.ParseTime(context.Query(_const.ClusterBegineTimeParameter))
	if err != nil {
		_ = context.Error(error22.ErrBadRequest.New("invalid begin time"))
		return
	}
	uf.BeginTime = &begin

	end, err := utils.ParseTime(context.Query(_const.ClusterEndTimeParameter))
	if err != nil {
		_ = context.Error(error22.ErrBadRequest.New("invalid end time"))
		return
	}
	uf.EndTime = &end

	serialNum, ierr := s.manager.PreUploadFile(uf)
	if ierr != nil {
		log.Error("preupload file failed", zap.String("file", fileUUID), zap.Error(ierr))
		if ierr.IsConflictErr() {
			_ = context.Error(error22.ErrBadRequest.New("the file is completed"))
		} else {
			_ = context.Error(error22.ErrBadRequest.New("preupload file failed, please try again later"))
		}

		return
	}

	blockSize := common2.GetBlockSizeInBytes(fileLen)
	log.Info("precreate block size", zap.Int64("fileLen", fileLen), zap.Int64("blocksize", blockSize))

	context.JSON(http.StatusOK, common.PreCreateResponse{
		Partseq:    serialNum,
		BlockBytes: blockSize,
	})
}

func checkClusterPermission(clusterID string, orgID uint) bool {
	c, err := model.GetCluster(clusterID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return true
		}

		log.Error("get cluster failed", zap.String("cluster", clusterID), zap.Error(err))
		return false
	}

	return c.OrgID == orgID
}

func parseIssueNumber(key string) (uint, error) {
	re := regexp.MustCompile(`ONCALL-(\d+)`)
	xs := re.FindStringSubmatch(key)
	if len(xs) == 0 {
		return 0, fmt.Errorf("invalid key: %s", key)
	}
	num, err := strconv.ParseUint(xs[1], 10, 64)
	if err != nil {
		return 0, err
	}
	return uint(num), nil
}

func (s *UploadService) UploadComplete(context *gin.Context) {
	fileUUID := context.Query("uuid")
	if fileUUID == "" {
		log.Warn("empty file uuid parameter")

		_ = context.Error(error22.ErrBadRequest.New("bad parameter"))
		return
	}

	file, err := s.manager.UploadFileComplete(fileUUID)
	if err != nil {
		log.Error("flush file failed", zap.String("file", fileUUID), zap.Error(err))

		_ = context.Error(error22.ErrBadRequest.New("flush file failed"))
		return
	}

	issue := context.Query("issue")
	if issue != "" {
		if num, err := parseIssueNumber(issue); err != nil {
			log.Error("can't parse issue key", zap.String("key", issue), zap.Error(err))
		} else if err := model.CreateJiraAssociateRequest(fileUUID, num); err != nil {
			log.Error("create jira associate request failed", zap.Error(err))
		}
	}

	//cliVer := context.Request.Header.Get("x-diag-version")
	//if semver.Compare(cliVer, "v0.2.0") < 0 {
	//	fmt.Println(cliVer)
	//	// For version < v0.1.0, use old style url
	//	context.JSON(http.StatusOK, fmt.Sprintf("https://%s/api/internal/files/%s", context.Request.Host, fileUUID))
	//	return
	//}

	host := context.Request.Host
	if s.cfg.ServerConfig.Host != "" {
		host = s.cfg.ServerConfig.Host
	}
	// delete the oldest package if size of existed packages exceeds the limit

	data := context.GetHeader(common.UserHeader)
	var user common.ApiUserContext
	jsonErr := json.Unmarshal([]byte(data), &user)
	if jsonErr != nil {
		log.Error("fetch api user context failed", zap.Error(err))
		_ = context.Error(error22.ErrBadRequest.New("lost context"))
		return
	}

	context.JSON(http.StatusOK, common.FlushResponse{
		ResultURL: fmt.Sprintf("https://%s/portal/#/orgs/%d/clusters/%s", host, file.OrgID, file.ClusterID),
	})
}
