package biz

import (
	"context"
	"fmt"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	pb "moredoc/api/v1"
	"moredoc/middleware/auth"
	"moredoc/model"
	"moredoc/util"
	"moredoc/util/filetil"

	"github.com/gin-gonic/gin"
	"github.com/gofrs/uuid"
	"github.com/golang-jwt/jwt"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
)

type ginResponse struct {
	Error   string      `json:"error,omitempty"`
	Message string      `json:"message,omitempty"`
	Code    int         `json:"code,omitempty"`
	Data    interface{} `json:"data,omitempty"`
}

type AttachmentAPIService struct {
	pb.UnimplementedAttachmentAPIServer
	dbModel *model.DBModel
	logger  *zap.Logger
}

func NewAttachmentAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *AttachmentAPIService) {
	return &AttachmentAPIService{dbModel: dbModel, logger: logger.Named("AttachmentAPIService")}
}

// checkPermission 检查用户权限
func (s *AttachmentAPIService) checkPermission(ctx context.Context) (userClaims *auth.UserClaims, err error) {
	return checkGRPCPermission(s.dbModel, ctx)
}

// checkPermission 检查用户权限
// 文件等的上传，也要验证用户是否有权限，无论是否是管理员
func (s *AttachmentAPIService) checkGinPermission(ctx *gin.Context) (userClaims *auth.UserClaims, statusCode int, err error) {
	return checkGinPermission(s.dbModel, ctx)
}

func (s *AttachmentAPIService) checkLogin(ctx *gin.Context) (userClaims *auth.UserClaims, statusCode int, err error) {
	return checkGinLogin(s.dbModel, ctx)
}

// UpdateAttachment 更新附件。只允许更新附件名称、是否合法以及描述字段
func (s *AttachmentAPIService) UpdateAttachment(ctx context.Context, req *pb.Attachment) (*emptypb.Empty, error) {
	_, err := s.checkPermission(ctx)
	if err != nil {
		return nil, err
	}

	updateFields := []string{"name", "enable", "description"}
	err = s.dbModel.UpdateAttachment(&model.Attachment{
		Id:          req.Id,
		Name:        req.Name,
		Description: req.Description,
		Enable:      req.Enable,
	}, updateFields...)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &emptypb.Empty{}, nil
}

func (s *AttachmentAPIService) DeleteAttachment(ctx context.Context, req *pb.DeleteAttachmentRequest) (*emptypb.Empty, error) {
	_, err := s.checkPermission(ctx)
	if err != nil {
		return nil, err
	}

	err = s.dbModel.DeleteAttachment(req.Id)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &emptypb.Empty{}, nil
}

// GetAttachment 查询单个附件信息
func (s *AttachmentAPIService) GetAttachment(ctx context.Context, req *pb.GetAttachmentRequest) (*pb.Attachment, error) {
	_, err := s.checkPermission(ctx)
	if err != nil {
		return nil, err
	}

	attachment, err := s.dbModel.GetAttachment(req.Id)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	pbAttachment := &pb.Attachment{}
	util.CopyStruct(&attachment, pbAttachment)

	return pbAttachment, nil
}

func (s *AttachmentAPIService) ListAttachment(ctx context.Context, req *pb.ListAttachmentRequest) (*pb.ListAttachmentReply, error) {
	_, err := s.checkPermission(ctx)
	if err != nil {
		return nil, err
	}

	opt := &model.OptionGetAttachmentList{
		Page:      int(req.Page),
		Size:      int(req.Size_),
		WithCount: true,
		QueryIn:   make(map[string][]interface{}),
	}

	if len(req.UserId) > 0 {
		opt.QueryIn["user_id"] = util.Slice2Interface(req.UserId)
	}

	if len(req.Enable) > 0 {
		opt.QueryIn["enable"] = util.Slice2Interface(req.Enable)
	}

	if len(req.Type) > 0 {
		opt.QueryIn["type"] = util.Slice2Interface(req.Type)
	}

	req.Wd = strings.TrimSpace(req.Wd)
	if req.Wd != "" {
		wd := "%" + req.Wd + "%"
		opt.QueryLike = map[string][]interface{}{"name": {wd}, "description": {wd}}
	}

	attachments, total, err := s.dbModel.GetAttachmentList(opt)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	var pbAttachments []*pb.Attachment
	util.CopyStruct(&attachments, &pbAttachments)

	var (
		userIds        []interface{}
		userIdIndexMap = make(map[int64][]int)
	)

	for idx, attchment := range pbAttachments {
		attchment.TypeName = s.dbModel.GetAttachmentTypeName(int(attchment.Type))
		userIds = append(userIds, attchment.UserId)
		userIdIndexMap[attchment.UserId] = append(userIdIndexMap[attchment.UserId], idx)
		pbAttachments[idx] = attchment
	}

	if size := len(userIds); size > 0 {
		users, _, _ := s.dbModel.GetUserList(&model.OptionGetUserList{Ids: userIds, Page: 1, Size: size, SelectFields: []string{"id", "username"}})
		s.logger.Debug("GetUserList", zap.Any("users", users))
		for _, user := range users {
			if indexes, ok := userIdIndexMap[user.Id]; ok {
				for _, idx := range indexes {
					pbAttachments[idx].Username = user.Username
				}
			}
		}
	}
	return &pb.ListAttachmentReply{Total: total, Attachment: pbAttachments}, nil
}

// UploadDocument 上传文档
func (s *AttachmentAPIService) UploadDocument(ctx *gin.Context) {
	// 检查用户是否已登录
	userClaims, statusCodes, err := s.checkLogin(ctx)
	if err != nil {
		s.logger.Debug("checkGinPermission", zap.Error(err), zap.Any("userClaims", userClaims), zap.Any("statusCodes", statusCodes))
		ctx.JSON(statusCodes, ginResponse{Code: statusCodes, Message: err.Error(), Error: err.Error()})
		return
	}

	// 检查用户是否有权限上传文档
	if !s.dbModel.CanIUploadDocument(userClaims.UserId) {
		ctx.JSON(http.StatusForbidden, ginResponse{Code: http.StatusForbidden, Message: "没有权限上传文档", Error: "没有权限上传文档"})
		return
	}

	name := "file"
	fileheader, err := ctx.FormFile(name)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: err.Error(), Error: err.Error()})
		return
	}

	unsuportedExt := "不支持的文档类型"
	ext := strings.ToLower(filepath.Ext(fileheader.Filename))
	if !filetil.IsDocument(ext) {
		ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: unsuportedExt, Error: unsuportedExt})
		return
	}

	allowedExt := s.dbModel.GetConfigOfSecurity(model.ConfigSecurityDocumentAllowedExt).DocumentAllowedExt
	if len(allowedExt) > 0 && !util.InSlice(allowedExt, ext) {
		ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: unsuportedExt, Error: unsuportedExt})
		return
	}

	attachment, err := s.saveFile(ctx, fileheader, true)
	if err != nil {
		os.Remove("." + attachment.Path)
		ctx.JSON(http.StatusInternalServerError, ginResponse{Code: http.StatusInternalServerError, Message: err.Error(), Error: err.Error()})
		return
	}
	attachment.UserId = userClaims.UserId
	attachment.Type = model.AttachmentTypeDocument

	err = s.dbModel.CreateAttachment(attachment)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, ginResponse{Code: http.StatusInternalServerError, Message: err.Error(), Error: err.Error()})
		return
	}

	// ctx.JSON(http.StatusOK, ginResponse{Code: http.StatusOK, Message: "ok", Data: attachment})
	ctx.JSON(http.StatusOK, ginResponse{Code: http.StatusOK, Message: "ok", Data: map[string]interface{}{"id": attachment.Id}})
}

// UploadAvatar 上传头像
func (s *AttachmentAPIService) UploadAvatar(ctx *gin.Context) {
	s.uploadImage(ctx, model.AttachmentTypeAvatar)
}

// UploadConfig 上传配置项中的相关图片
func (s *AttachmentAPIService) UploadConfig(ctx *gin.Context) {
	s.uploadImage(ctx, model.AttachmentTypeConfig)
}

// ViewDocumentPages 浏览文档页面
func (s *AttachmentAPIService) ViewDocumentPages(ctx *gin.Context) {
	hash := ctx.Param("hash")
	if len(hash) != 32 {
		ctx.JSON(http.StatusNotFound, nil)
		return
	}
	page := strings.TrimLeft(ctx.Param("page"), "./")
	if strings.HasSuffix(page, ".gzip.svg") {
		ctx.Header("Content-Encoding", "gzip")
	}
	ctx.Header("Content-Type", "image/svg+xml")
	ctx.File(fmt.Sprintf("documents/%s/%s/%s", strings.Join(strings.Split(hash, "")[:5], "/"), hash, page))
}

func (s *AttachmentAPIService) ViewDocumentCover(ctx *gin.Context) {
	hash := ctx.Param("hash")
	file := fmt.Sprintf("documents/%s/%s/cover.png", strings.Join(strings.Split(hash, "")[:5], "/"), hash)
	if len(hash) != 32 {
		ctx.JSON(http.StatusNotFound, map[string]interface{}{"code": http.StatusNotFound, "message": "文件不存在"})
		return
	}
	ctx.File(file)
}

// DownloadDocument 下载文档
func (s *AttachmentAPIService) DownloadDocument(ctx *gin.Context) {
	claims := &jwt.StandardClaims{}
	token := ctx.Param("jwt")
	cfg := s.dbModel.GetConfigOfDownload(model.ConfigDownloadSecretKey)
	// 验证JWT是否合法
	jwtToken, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
		return []byte(cfg.SecretKey), nil
	})
	if err != nil || !jwtToken.Valid {
		ctx.String(http.StatusBadRequest, "下载链接已失效")
		return
	}
	filename := ctx.Query("filename")
	file := fmt.Sprintf("documents/%s/%s%s", strings.Join(strings.Split(claims.Id, "")[:5], "/"), claims.Id, filepath.Ext(filename))
	ctx.FileAttachment(file, filename)
}

//	UploadArticle 上传文章相关图片和视频。这里不验证文件格式。
//
// 注意：当前适配了wangeditor的接口规范，如果需要适配其他编辑器，需要修改此接口或者增加其他接口
func (s *AttachmentAPIService) UploadArticle(ctx *gin.Context) {
	typ := ctx.Query("type")
	if typ != "image" && typ != "video" {
		ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": "类型参数错误"})
		return
	}

	userCliams, _, err := s.checkGinPermission(ctx)
	if err != nil {
		ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
		return
	}

	name := "file"
	fileHeader, err := ctx.FormFile(name)
	if err != nil {
		s.logger.Error("MultipartForm", zap.Error(err))
		ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
		return
	}

	attachment, err := s.saveFile(ctx, fileHeader)
	if err != nil {
		s.logger.Error("saveFile", zap.Error(err))
		os.Remove("." + attachment.Path)
		ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
		return
	}
	attachment.UserId = userCliams.UserId
	attachment.Type = model.AttachmentTypeArticle

	err = s.dbModel.CreateAttachment(attachment)
	if err != nil {
		s.logger.Error("CreateAttachments", zap.Error(err))
		ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
		return
	}

	if typ == "image" {
		ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 0, "data": map[string]interface{}{
			"url": attachment.Path,
			"alt": attachment.Name,
			// "href": "",
		}})
	} else {
		ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 0, "data": map[string]interface{}{
			"url": attachment.Path,
			// "poster": "",
		}})
	}
}

// UploadBanner 上传横幅，创建横幅的时候，要根据附件id，更新附件的type_id字段
func (s *AttachmentAPIService) UploadBanner(ctx *gin.Context) {
	s.uploadImage(ctx, model.AttachmentTypeBanner)
}

// 上传文档分类封面
func (s *AttachmentAPIService) UploadCategory(ctx *gin.Context) {
	s.uploadImage(ctx, model.AttachmentTypeCategoryCover)
}

func (s *AttachmentAPIService) uploadImage(ctx *gin.Context, attachmentType int) {
	name := "file"
	userClaims, statusCodes, err := s.checkGinPermission(ctx)
	if err != nil {
		if !(userClaims != nil && attachmentType == model.AttachmentTypeAvatar) {
			ctx.JSON(statusCodes, ginResponse{Code: statusCodes, Message: err.Error(), Error: err.Error()})
			return
		}
	}

	// 验证文件是否是图片
	fileHeader, err := ctx.FormFile(name)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: err.Error(), Error: err.Error()})
		return
	}

	ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
	if !filetil.IsImage(ext) {
		message := "请上传图片格式文件，支持.jpg、.jpeg、.png、.gif和.ico格式图片"
		ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: message, Error: message})
		return
	}

	attachment, err := s.saveFile(ctx, fileHeader)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: err.Error(), Error: err.Error()})
		return
	}
	attachment.Type = attachmentType
	attachment.UserId = userClaims.UserId

	if attachmentType == model.AttachmentTypeAvatar {
		attachment.TypeId = userClaims.UserId
		// 更新用户头像信息
		err = s.dbModel.UpdateUser(&model.User{Id: userClaims.UserId, Avatar: attachment.Path}, "avatar")
		if err != nil {
			ctx.JSON(http.StatusInternalServerError, ginResponse{Code: http.StatusInternalServerError, Message: err.Error(), Error: err.Error()})
		}
		// 标记删除旧头像附件记录
		s.dbModel.GetDB().Where("type = ? AND type_id = ?", model.AttachmentTypeAvatar, userClaims.UserId).Delete(&model.Attachment{})
	}

	// 保存附件信息
	err = s.dbModel.CreateAttachment(attachment)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, ginResponse{Code: http.StatusInternalServerError, Message: err.Error(), Error: err.Error()})
		return
	}
	ctx.JSON(http.StatusOK, ginResponse{Code: http.StatusOK, Message: "上传成功", Data: attachment})
}

// saveFile 保存文件。文件以md5值命名以及存储
// 同时，返回附件信息
func (s *AttachmentAPIService) saveFile(ctx *gin.Context, fileHeader *multipart.FileHeader, isDocument ...bool) (attachment *model.Attachment, err error) {
	cacheDir := fmt.Sprintf("cache/uploads/%s", time.Now().Format("2006/01/02"))
	os.MkdirAll(cacheDir, os.ModePerm)
	ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
	cachePath := fmt.Sprintf("%s/%s%s", cacheDir, uuid.Must(uuid.NewV4()).String(), ext)
	defer func() {
		os.Remove(cachePath)
	}()

	// 保存到临时文件
	err = ctx.SaveUploadedFile(fileHeader, cachePath)
	if err != nil {
		s.logger.Error("SaveUploadedFile", zap.Error(err), zap.String("filename", fileHeader.Filename), zap.String("cachePath", cachePath))
		return
	}

	// 获取文件md5值
	md5hash, errHash := filetil.GetFileMD5(cachePath)
	if errHash != nil {
		err = errHash
		return
	}

	savePathFormat := "uploads/%s/%s%s"
	if len(isDocument) > 0 && isDocument[0] {
		savePathFormat = "documents/%s/%s%s"
	}
	savePath := fmt.Sprintf(savePathFormat, strings.Join(strings.Split(md5hash, "")[0:5], "/"), md5hash, ext)
	os.MkdirAll(filepath.Dir(savePath), os.ModePerm)
	err = util.CopyFile(cachePath, savePath)
	if err != nil {
		s.logger.Error("Rename", zap.Error(err), zap.String("cachePath", cachePath), zap.String("savePath", savePath))
		return
	}

	attachment = &model.Attachment{
		Size:   fileHeader.Size,
		Name:   fileHeader.Filename,
		Ip:     ctx.ClientIP(),
		Ext:    ext,
		Enable: true, // 默认都是合法的
		Hash:   md5hash,
		Path:   "/" + savePath,
	}

	// 对于图片，直接获取图片的宽高
	if filetil.IsImage(ext) {
		attachment.Width, attachment.Height, _ = filetil.GetImageSize(cachePath)
	}

	return
}

func (s *AttachmentAPIService) Favicon(ctx *gin.Context) {
	favicon := strings.TrimLeft(s.dbModel.GetConfigOfSystem("favicon").Favicon, "./")
	faviconIco := "favicon.ico"
	if favicon != "" {
		_, err := os.Stat(favicon)
		if err != nil {
			favicon = faviconIco
		}
	} else {
		favicon = faviconIco
	}
	ctx.File(favicon)
}
