package file

import (
	"fmt"
	"io"
	"mime/multipart"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"be.ems/src/framework/config"
	"be.ems/src/framework/constants"
	"be.ems/src/framework/logger"
	"be.ems/src/framework/utils/date"
	"be.ems/src/framework/utils/generate"
	"be.ems/src/framework/utils/parse"
	"be.ems/src/framework/utils/regular"
)

/**最大文件名长度 */
const DEFAULT_FILE_NAME_LENGTH = 100

// 文件上传路径 prefix, dir
func resourceUpload() (string, string) {
	upload := config.Get("staticFile.upload").(map[string]any)
	dir, err := filepath.Abs(upload["dir"].(string))
	if err != nil {
		logger.Errorf("file resourceUpload err %v", err)
	}
	return upload["prefix"].(string), dir
}

// 最大上传文件大小
func uploadFileSize() int64 {
	fileSize := 1 * 1024 * 1024
	size := config.Get("upload.fileSize").(int)
	if size > 1 {
		fileSize = size * 1024 * 1024
	}
	return int64(fileSize)
}

// 文件上传扩展名白名单
func uploadWhiteList() []string {
	arr := config.Get("upload.whitelist").([]any)
	strings := make([]string, len(arr))
	for i, val := range arr {
		if str, ok := val.(string); ok {
			strings[i] = str
		}
	}
	return strings
}

// 生成文件名称 fileName_随机值.extName
//
// fileName 原始文件名称含后缀，如：logo.png
func generateFileName(fileName string) string {
	fileExt := filepath.Ext(fileName)
	// 去除后缀
	newFileName := regular.Replace(fileName, fileExt, "")
	// 去除非法字符
	newFileName = regular.Replace(newFileName, `[\\/:*?"<>|]`, "")
	// 去除空格
	newFileName = regular.Replace(newFileName, `\s`, "_")
	newFileName = strings.TrimSpace(newFileName)
	return fmt.Sprintf("%s_%s%s", newFileName, generate.Code(6), fileExt)
}

// 检查文件允许写入本地
//
// fileName 原始文件名称含后缀，如：midway1_logo_iipc68.png
//
// allowExts 允许上传拓展类型，['.png']
func isAllowWrite(fileName string, allowExts []string, fileSize int64) error {
	// 判断上传文件名称长度
	if len(fileName) > DEFAULT_FILE_NAME_LENGTH {
		return fmt.Errorf("the maximum length limit for uploading file names is %d", DEFAULT_FILE_NAME_LENGTH)
	}

	// 最大上传文件大小
	maxFileSize := uploadFileSize()
	if fileSize > maxFileSize {
		return fmt.Errorf("maximum upload file size %s", parse.Bit(float64(maxFileSize)))
	}

	// 判断文件拓展是否为允许的拓展类型
	fileExt := filepath.Ext(fileName)
	hasExt := false
	if len(allowExts) == 0 {
		allowExts = uploadWhiteList()
	}
	for _, ext := range allowExts {
		if ext == fileExt {
			hasExt = true
			break
		}
	}
	if !hasExt {
		return fmt.Errorf("the upload file type is not supported, only the following types are supported: %s", strings.Join(allowExts, ","))
	}

	return nil
}

// 检查文件允许本地读取
//
// filePath 文件存放资源路径，URL相对地址
func isAllowRead(filePath string) error {
	// 禁止目录上跳级别
	if strings.Contains(filePath, "..") {
		return fmt.Errorf("prohibit jumping levels on the directory")
	}

	// 检查允许下载的文件规则
	fileExt := filepath.Ext(filePath)
	hasExt := false
	for _, ext := range uploadWhiteList() {
		if ext == fileExt {
			hasExt = true
			break
		}
	}
	if !hasExt {
		return fmt.Errorf("rules for illegally downloaded files: %s", fileExt)
	}

	return nil
}

// TransferUploadFile 上传资源文件转存
//
// subPath 子路径，默认 UploadSubPath.DEFAULT
//
// allowExts 允许上传拓展类型（含“.”)，如 ['.png','.jpg']
func TransferUploadFile(file *multipart.FileHeader, subPath string, allowExts []string) (string, error) {
	// 上传文件检查
	err := isAllowWrite(file.Filename, allowExts, file.Size)
	if err != nil {
		return "", err
	}
	// 上传资源路径
	prefix, dir := resourceUpload()
	// 新文件名称并组装文件地址
	fileName := generateFileName(file.Filename)
	filePath := filepath.Join(subPath, date.ParseDatePath(time.Now()))
	writePathFile := filepath.Join(dir, filePath, fileName)
	// 存入新文件路径
	err = transferToNewFile(file, writePathFile)
	if err != nil {
		return "", err
	}
	urlPath := filepath.Join(prefix, filePath, fileName)
	return filepath.ToSlash(urlPath), nil
}

// ReadUploadFileStream 上传资源文件读取
//
// filePath 文件存放资源路径，URL相对地址 如：/upload/common/2023/06/xxx.png
//
// headerRange 断点续传范围区间，bytes=0-12131
func ReadUploadFileStream(filePath, headerRange string) (map[string]any, error) {
	// 读取文件检查
	err := isAllowRead(filePath)
	if err != nil {
		return map[string]any{}, err
	}
	// 上传资源路径
	prefix, dir := resourceUpload()
	fileAsbPath := strings.Replace(filePath, prefix, dir, 1)

	// 响应结果
	result := map[string]any{
		"range":     "",
		"chunkSize": 0,
		"fileSize":  0,
		"data":      []byte{},
	}

	// 文件大小
	fileSize := getFileSize(fileAsbPath)
	if fileSize <= 0 {
		return result, fmt.Errorf("file does not exist")
	}
	result["fileSize"] = fileSize

	if headerRange != "" {
		partsStr := strings.Replace(headerRange, "bytes=", "", 1)
		parts := strings.Split(partsStr, "-")
		start, err := strconv.ParseInt(parts[0], 10, 64)
		if err != nil || start > fileSize {
			start = 0
		}
		end, err := strconv.ParseInt(parts[1], 10, 64)
		if err != nil || end > fileSize {
			end = fileSize - 1
		}
		if start > end {
			start = end
		}

		// 分片结果
		result["range"] = fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize)
		result["chunkSize"] = end - start + 1
		byteArr, err := getFileStream(fileAsbPath, start, end)
		if err != nil {
			return map[string]any{}, fmt.Errorf("fail to read file")
		}
		result["data"] = byteArr
		return result, nil
	}

	byteArr, err := getFileStream(fileAsbPath, 0, fileSize)
	if err != nil {
		return map[string]any{}, fmt.Errorf("fail to read file")
	}
	result["data"] = byteArr
	return result, nil
}

// TransferChunkUploadFile 上传资源切片文件转存
//
// file 上传文件对象
//
// index 切片文件序号
//
// identifier 切片文件目录标识符
func TransferChunkUploadFile(file *multipart.FileHeader, index, identifier string) (string, error) {
	// 上传文件检查
	err := isAllowWrite(file.Filename, []string{}, file.Size)
	if err != nil {
		return "", err
	}
	// 上传资源路径
	prefix, dir := resourceUpload()
	// 新文件名称并组装文件地址
	filePath := filepath.Join(constants.UPLOAD_CHUNK, date.ParseDatePath(time.Now()), identifier)
	writePathFile := filepath.Join(dir, filePath, index)
	// 存入新文件路径
	err = transferToNewFile(file, writePathFile)
	if err != nil {
		return "", err
	}
	urlPath := filepath.Join(prefix, filePath, index)
	return filepath.ToSlash(urlPath), nil
}

// 上传资源切片文件检查
//
// identifier 切片文件目录标识符
//
// originalFileName 原始文件名称，如logo.png
func ChunkCheckFile(identifier, originalFileName string) ([]string, error) {
	// 读取文件检查
	err := isAllowWrite(originalFileName, []string{}, 0)
	if err != nil {
		return []string{}, err
	}
	// 上传资源路径
	_, dir := resourceUpload()
	dirPath := path.Join(constants.UPLOAD_CHUNK, date.ParseDatePath(time.Now()), identifier)
	readPath := path.Join(dir, dirPath)
	fileList, err := getDirFileNameList(readPath)
	if err != nil {
		return []string{}, fmt.Errorf("fail to read file")
	}
	return fileList, nil
}

// 上传资源切片文件检查
//
// identifier 切片文件目录标识符
//
// originalFileName 原始文件名称，如logo.png
//
// subPath 子路径，默认 DEFAULT
func ChunkMergeFile(identifier, originalFileName, subPath string) (string, error) {
	// 读取文件检查
	err := isAllowWrite(originalFileName, []string{}, 0)
	if err != nil {
		return "", err
	}
	// 上传资源路径
	prefix, dir := resourceUpload()
	// 切片存放目录
	dirPath := path.Join(constants.UPLOAD_CHUNK, date.ParseDatePath(time.Now()), identifier)
	readPath := path.Join(dir, dirPath)
	// 组合存放文件路径
	fileName := generateFileName(originalFileName)
	filePath := path.Join(subPath, date.ParseDatePath(time.Now()))
	writePath := path.Join(dir, filePath)
	err = mergeToNewFile(readPath, writePath, fileName)
	if err != nil {
		return "", err
	}
	urlPath := filepath.Join(prefix, filePath, fileName)
	return filepath.ToSlash(urlPath), nil
}

// CopyUploadFile 将上传文件资源转移新目录
//
// filePath 上传得到的文件路径 /upload....
// dst 新文件路径 /a/xx.pdf
func CopyUploadFile(filePath, dst string) error {
	srcPath := ParseUploadFileAbsPath(filePath)
	src, err := os.Open(srcPath)
	if err != nil {
		return err
	}
	defer src.Close()

	if err := os.MkdirAll(filepath.Dir(dst), 0775); err != nil {
		return err
	}

	// 如果目标文件已经存在，先将目标文件重命名
	if info, err := os.Stat(dst); err == nil && !info.IsDir() {
		ext := filepath.Ext(dst)
		name := dst[0 : len(dst)-len(ext)]
		newName := fmt.Sprintf("%s-%s%s", name, time.Now().Format("20060102_150405"), ext)
		err := os.Rename(dst, newName)
		if err != nil {
			return err
		}
	}

	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, src)
	return err
}

// ParseUploadFileDir 得到上传资源目录
//
// subPath 子路径，默认 UploadSubPath.DEFAULT
func ParseUploadFileDir(subPath string) string {
	_, dir := resourceUpload()
	filePath := filepath.Join(subPath, date.ParseDatePath(time.Now()))
	return filepath.Join(dir, filePath)
}

// ParseUploadFileAbsPath  上传资源本地绝对资源路径
//
// filePath 上传文件路径
func ParseUploadFileAbsPath(filePath string) string {
	prefix, dir := resourceUpload()
	absPath := strings.Replace(filePath, prefix, dir, 1)
	return filepath.ToSlash(absPath)
}
