package v2

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/dto/request"
	"github.com/1Panel-dev/1Panel/agent/app/dto/response"
	"github.com/1Panel-dev/1Panel/agent/buserr"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/global"
	"github.com/1Panel-dev/1Panel/agent/utils/files"
	websocket2 "github.com/1Panel-dev/1Panel/agent/utils/websocket"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	qrcode "github.com/skip2/go-qrcode"
)

var cancelledChunkUploads = struct {
	sync.RWMutex
	ids map[string]struct{}
}{ids: make(map[string]struct{})}

// @Tags File
// @Summary List files
// @Accept json
// @Param request body request.FileOption true "request"
// @Success 200 {object} response.FileInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/search [post]
func (b *BaseApi) ListFiles(c *gin.Context) {
	var req request.FileOption
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	fileList, err := fileService.GetFileList(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, fileList)
}

// @Tags File
// @Summary File search: content grep + optional AI summary
// @Description When file-management AI is enabled, returns mode=ai with summary and hits. When disabled, returns mode=grep with hits only. Scans file contents only. Supports match options, extension/size/time filters, and scan limits.
// @Accept json
// @Param request body request.FileAISearch true "request"
// @Success 200 {object} response.FileAISearchResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/ai-search [post]
func (b *BaseApi) FileAISearch(c *gin.Context) {
	var req request.FileAISearch
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if strings.TrimSpace(req.ResponseLanguage) == "" {
		req.ResponseLanguage = strings.TrimSpace(c.GetHeader("Accept-Language"))
	}
	res, err := fileService.AISearch(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, res)
}

// @Tags File
// @Summary Page file
// @Accept json
// @Param request body request.SearchUploadWithPage true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/upload/search [post]
func (b *BaseApi) SearchUploadWithPage(c *gin.Context) {
	var req request.SearchUploadWithPage
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	total, files, err := fileService.SearchUploadWithPage(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, dto.PageResult{
		Items: files,
		Total: total,
	})
}

// @Tags File
// @Summary Load files tree
// @Accept json
// @Param request body request.FileOption true "request"
// @Success 200 {array} response.FileTree
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/tree [post]
func (b *BaseApi) GetFileTree(c *gin.Context) {
	var req request.FileOption
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	tree, err := fileService.GetFileTree(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithDataGzipped(c, tree)
}

// @Tags File
// @Summary Create file
// @Accept json
// @Param request body request.FileCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建文件/文件夹 [path]","formatEN":"Create dir or file [path]"}
func (b *BaseApi) CreateFile(c *gin.Context) {
	var req request.FileCreate
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	err := fileService.Create(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Delete file
// @Accept json
// @Param request body request.FileDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/del [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除文件/文件夹 [path]","formatEN":"Delete dir or file [path]"}
func (b *BaseApi) DeleteFile(c *gin.Context) {
	var req request.FileDelete
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	err := fileService.Delete(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Batch delete file
// @Accept json
// @Param request body request.FileBatchDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/batch/del [post]
// @x-panel-log {"bodyKeys":["paths"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量删除文件/文件夹 [paths]","formatEN":"Batch delete dir or file [paths]"}
func (b *BaseApi) BatchDeleteFile(c *gin.Context) {
	var req request.FileBatchDelete
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	err := fileService.BatchDelete(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Change file mode
// @Accept json
// @Param request body request.FileCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/mode [post]
// @x-panel-log {"bodyKeys":["path","mode"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改权限 [path] => [mode]","formatEN":"Change mode [path] => [mode]"}
func (b *BaseApi) ChangeFileMode(c *gin.Context) {
	var req request.FileCreate
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	err := fileService.ChangeMode(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Change file owner
// @Accept json
// @Param request body request.FileRoleUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/owner [post]
// @x-panel-log {"bodyKeys":["path","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改用户/组 [path] => [user]/[group]","formatEN":"Change owner [path] => [user]/[group]"}
func (b *BaseApi) ChangeFileOwner(c *gin.Context) {
	var req request.FileRoleUpdate
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if err := fileService.ChangeOwner(req); err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Compress file
// @Accept json
// @Param request body request.FileCompress true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/compress [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"压缩文件 [name]","formatEN":"Compress file [name]"}
func (b *BaseApi) CompressFile(c *gin.Context) {
	var req request.FileCompress
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	err := fileService.Compress(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Stop compress task
// @Accept json
// @Param request body request.FileCompressStopReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/compress/stop [post]
func (b *BaseApi) StopCompressFile(c *gin.Context) {
	var req request.FileCompressStopReq
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if err := fileService.StopCompress(req.TaskID); err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Decompress file
// @Accept json
// @Param request body request.FileDeCompress true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/decompress [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"解压 [path]","formatEN":"Decompress file [path]"}
func (b *BaseApi) DeCompressFile(c *gin.Context) {
	var req request.FileDeCompress
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	err := fileService.DeCompress(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Stop decompress task
// @Accept json
// @Param request body request.FileDeCompressStopReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/decompress/stop [post]
func (b *BaseApi) StopDeCompressFile(c *gin.Context) {
	var req request.FileDeCompressStopReq
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if err := fileService.StopDeCompress(req.TaskID); err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Load file content
// @Accept json
// @Param request body request.FileContentReq true "request"
// @Success 200 {object} response.FileInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/content [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"获取文件内容 [path]","formatEN":"Load file content [path]"}
func (b *BaseApi) GetContent(c *gin.Context) {
	var req request.FileContentReq
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	info, err := fileService.GetContent(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	if info.Size > 2*1024 && info.Size < 5*1024*1024 {
		helper.SuccessWithDataGzipped(c, info)
	} else {
		helper.SuccessWithData(c, info)
	}
}

// @Tags File
// @Summary Preview file content
// @Accept json
// @Param request body request.FileContentReq true "request"
// @Success 200 {object} response.FileInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/preview [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"预览文件内容 [path]","formatEN":"Preview file content [path]"}
func (b *BaseApi) PreviewContent(c *gin.Context) {
	var req request.FileContentReq
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	info, err := fileService.GetPreviewContent(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}

	helper.SuccessWithData(c, info)
}

// @Tags File
// @Summary Update file content
// @Accept json
// @Param request body request.FileEdit true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/save [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新文件内容 [path]","formatEN":"Update file content [path]"}
func (b *BaseApi) SaveContent(c *gin.Context) {
	var req request.FileEdit
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if err := fileService.SaveContent(req); err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Upload file
// @Param file formData file true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/upload [post]
// @x-panel-log {"bodyKeys":["path", "file"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"上传文件 [path]/[file]","formatEN":"Upload file [path]/[file]"}
func (b *BaseApi) UploadFiles(c *gin.Context) {
	form, err := c.MultipartForm()
	if err != nil {
		helper.BadRequest(c, err)
		return
	}
	uploadFiles := form.File["file"]
	paths := form.Value["path"]

	overwrite := true
	if ow, ok := form.Value["overwrite"]; ok {
		if len(ow) != 0 {
			parseBool, _ := strconv.ParseBool(ow[0])
			overwrite = parseBool
		}
	}

	if len(paths) == 0 || !strings.Contains(paths[0], "/") {
		helper.BadRequest(c, errors.New("error paths in request"))
		return
	}
	dir := path.Clean(paths[0])

	_, err = os.Stat(dir)
	if err != nil && os.IsNotExist(err) {
		mode, err := files.GetParentMode(dir)
		if err != nil {
			helper.InternalServer(c, err)
			return
		}
		if err = os.MkdirAll(dir, mode); err != nil {
			helper.BadRequest(c, fmt.Errorf("mkdir %s failed, err: %v", dir, err))
			return
		}
	}
	info, err := os.Stat(dir)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	mode := info.Mode()

	fileOp := files.NewFileOp()
	stat, ok := info.Sys().(*syscall.Stat_t)
	uid, gid := -1, -1
	if ok {
		uid, gid = int(stat.Uid), int(stat.Gid)
	}
	success := 0
	failures := make(buserr.MultiErr)
	for _, file := range uploadFiles {
		dstFilename := path.Join(paths[0], file.Filename)
		dstDir := path.Dir(dstFilename)
		if !fileOp.Stat(dstDir) {
			if err = fileOp.CreateDir(dstDir, mode); err != nil {
				e := fmt.Errorf("create dir [%s] failed, err: %v", path.Dir(dstFilename), err)
				failures[file.Filename] = e
				global.LOG.Error(e)
				continue
			}
			_ = os.Chown(dstDir, uid, gid)
		}
		dstDirMode := mode
		dstFileMode := mode.Perm()
		if dstDirInfo, err := os.Stat(dstDir); err == nil {
			dstDirMode = dstDirInfo.Mode()
			dstFileMode = dstDirInfo.Mode().Perm()
		}
		tmpFilename := dstFilename + ".tmp"
		if err := c.SaveUploadedFile(file, tmpFilename, dstDirMode); err != nil {
			_ = os.Remove(tmpFilename)
			e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
			failures[file.Filename] = e
			global.LOG.Error(e)
			continue
		}
		dstInfo, statErr := os.Stat(dstFilename)
		if overwrite {
			_ = os.Remove(dstFilename)
		}

		err = os.Rename(tmpFilename, dstFilename)
		if err != nil {
			_ = os.Remove(tmpFilename)
			e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
			failures[file.Filename] = e
			global.LOG.Error(e)
			continue
		}
		if statErr == nil {
			_ = os.Chmod(dstFilename, dstInfo.Mode())
		} else {
			_ = os.Chmod(dstFilename, dstFileMode)
		}
		if uid != -1 && gid != -1 {
			_ = os.Chown(dstFilename, uid, gid)
		}
		success++
	}
	if success == 0 {
		helper.InternalServer(c, failures)
	} else {
		helper.SuccessWithMsg(c, fmt.Sprintf("%d files upload success", success))
	}
}

// @Tags File
// @Summary Check file exist
// @Accept json
// @Param request body request.FilePathCheck true "request"
// @Success 200 {boolean} isOk
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/check [post]
func (b *BaseApi) CheckFile(c *gin.Context) {
	var req request.FilePathCheck
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	fileOp := files.NewFileOp()
	if fileOp.Stat(req.Path) {
		helper.SuccessWithData(c, true)
		return
	}
	if req.WithInit {
		if err := fileOp.CreateDir(req.Path, 0644); err != nil {
			helper.SuccessWithData(c, false)
			return
		}
		helper.SuccessWithData(c, true)
		return
	}
	helper.SuccessWithData(c, false)
}

// @Tags File
// @Summary Batch check file exist
// @Accept json
// @Param request body request.FilePathsCheck true "request"
// @Success 200 {array} response.ExistFileInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/batch/check [post]
func (b *BaseApi) BatchCheckFiles(c *gin.Context) {
	var req request.FilePathsCheck
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	fileList := fileService.BatchCheckFiles(req)
	helper.SuccessWithData(c, fileList)
}

// @Tags File
// @Summary Change file name
// @Accept json
// @Param request body request.FileRename true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/rename [post]
// @x-panel-log {"bodyKeys":["oldName","newName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"重命名 [oldName] => [newName]","formatEN":"Rename [oldName] => [newName]"}
func (b *BaseApi) ChangeFileName(c *gin.Context) {
	var req request.FileRename
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if err := fileService.ChangeName(req); err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Wget file
// @Accept json
// @Param request body request.FileWget true "request"
// @Success 200 {object} response.FileWgetRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget [post]
// @x-panel-log {"bodyKeys":["url","path","name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载 url => [path]/[name]","formatEN":"Download url => [path]/[name]"}
func (b *BaseApi) WgetFile(c *gin.Context) {
	var req request.FileWget
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	key, err := fileService.Wget(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, response.FileWgetRes{
		Key: key,
	})
}

// @Tags File
// @Summary Stop wget file download
// @Accept json
// @Param request body request.FileProcessReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget/stop [post]
// @x-panel-log {"bodyKeys":["key"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"停止下载任务 [key]","formatEN":"Stop wget task [key]"}
func (b *BaseApi) StopWget(c *gin.Context) {
	var req request.FileProcessReq
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if strings.TrimSpace(req.Key) == "" {
		helper.BadRequest(c, errors.New("key is required"))
		return
	}

	files.CancelDownload(req.Key)
	helper.Success(c)
}

// @Tags File
// @Summary Move file
// @Accept json
// @Param request body request.FileMove true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/move [post]
// @x-panel-log {"bodyKeys":["oldPaths","newPath"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移动文件 [oldPaths] => [newPath]","formatEN":"Move [oldPaths] => [newPath]"}
func (b *BaseApi) MoveFile(c *gin.Context) {
	var req request.FileMove
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if err := fileService.MvFile(req); err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Download file
// @Accept json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/download [get]
func (b *BaseApi) Download(c *gin.Context) {
	filePath := c.Query("path")
	if files.ShouldDenySensitiveFileRead(filePath) {
		helper.InternalServer(c, buserr.New("ErrSensitiveFileRead"))
		return
	}
	file, err := os.Open(filePath)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	defer file.Close()
	info, err := file.Stat()
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	c.Header("Content-Length", strconv.FormatInt(info.Size(), 10))
	c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
	http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
}

// @Tags File
// @Summary Chunk Download file
// @Accept json
// @Param request body request.FileDownload true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/chunkdownload [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载文件 [name]","formatEN":"Download file [name]"}
func (b *BaseApi) DownloadChunkFiles(c *gin.Context) {
	var req request.FileChunkDownload
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	fileOp := files.NewFileOp()
	if !fileOp.Stat(req.Path) {
		helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrPathNotFound", nil)
		return
	}
	if files.ShouldDenySensitiveFileRead(req.Path) {
		helper.InternalServer(c, buserr.New("ErrSensitiveFileRead"))
		return
	}
	filePath := req.Path
	fstFile, err := fileOp.OpenFile(filePath)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	info, err := fstFile.Stat()
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	if info.IsDir() {
		helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrFileDownloadDir", err)
		return
	}

	c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", req.Name))
	c.Writer.Header().Set("Content-Type", "application/octet-stream")
	c.Writer.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10))
	c.Writer.Header().Set("Accept-Ranges", "bytes")

	if c.Request.Header.Get("Range") != "" {
		rangeHeader := c.Request.Header.Get("Range")
		rangeArr := strings.Split(rangeHeader, "=")[1]
		rangeParts := strings.Split(rangeArr, "-")

		startPos, _ := strconv.ParseInt(rangeParts[0], 10, 64)

		var endPos int64
		if rangeParts[1] == "" {
			endPos = info.Size() - 1
		} else {
			endPos, _ = strconv.ParseInt(rangeParts[1], 10, 64)
		}

		c.Writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", startPos, endPos, info.Size()))
		c.Writer.WriteHeader(http.StatusPartialContent)

		buffer := make([]byte, 1024*1024)
		file, err := os.Open(filePath)
		if err != nil {
			helper.InternalServer(c, err)
			return
		}
		defer file.Close()

		_, _ = file.Seek(startPos, 0)
		reader := io.LimitReader(file, endPos-startPos+1)
		_, err = io.CopyBuffer(c.Writer, reader, buffer)
		if err != nil {
			helper.InternalServer(c, err)
			return
		}
	} else {
		c.File(filePath)
	}
}

// @Tags File
// @Summary Load file size
// @Accept json
// @Param request body request.DirSizeReq true "request"
// @Success 200 {object} response.DirSizeRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/size [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"获取文件夹大小 [path]","formatEN":"Load file size [path]"}
func (b *BaseApi) Size(c *gin.Context) {
	var req request.DirSizeReq
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	res, err := fileService.DirSize(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, res)
}

// @Tags File
// @Summary Multi file size
// @Accept json
// @Param request body request.DirSizeReq true "request"
// @Success 200 {array} response.DepthDirSizeRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/depth/size [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"获取目录及其第一层子目录文件夹大小 [path]","formatEN":"Multi file size [path]"}
func (b *BaseApi) DepthDirSize(c *gin.Context) {
	var req request.DirSizeReq
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	res, err := fileService.DepthDirSize(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, res)
}

func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
	defer func() {
		_ = os.RemoveAll(fileDir)
	}()

	op := files.NewFileOp()
	dstDir = strings.TrimSpace(dstDir)
	mode, _ := files.GetParentMode(dstDir)
	if mode == 0 {
		mode = constant.DirPerm
	}
	if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) {
		if err = op.CreateDir(dstDir, mode); err != nil {
			return err
		}
	}
	if dstDirInfo, err := os.Stat(dstDir); err == nil {
		mode = dstDirInfo.Mode().Perm()
	}
	dstFileName := filepath.Join(dstDir, fileName)
	dstInfo, statErr := os.Stat(dstFileName)
	if statErr == nil {
		mode = dstInfo.Mode()
	}
	if overwrite {
		_ = os.Remove(dstFileName)
	}
	targetFile, err := os.OpenFile(dstFileName, os.O_RDWR|os.O_CREATE, mode)
	if err != nil {
		return err
	}
	defer targetFile.Close()
	for i := 0; i < chunkCount; i++ {
		chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", fileName, i))
		chunkData, err := os.ReadFile(chunkPath)
		if err != nil {
			return err
		}
		_, err = targetFile.Write(chunkData)
		if err != nil {
			return err
		}
		_ = os.Remove(chunkPath)
	}
	_ = os.Chmod(dstFileName, mode)

	return nil
}

// @Tags File
// @Summary ChunkUpload file
// @Param file formData file true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/chunkupload [post]
func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
	var err error
	fileForm, err := c.FormFile("chunk")
	if err != nil {
		helper.BadRequest(c, err)
		return
	}
	uploadFile, err := fileForm.Open()
	if err != nil {
		helper.BadRequest(c, err)
		return
	}
	defer uploadFile.Close()
	chunkIndex, err := strconv.Atoi(c.PostForm("chunkIndex"))
	if err != nil {
		helper.BadRequest(c, err)
		return
	}
	chunkCount, err := strconv.Atoi(c.PostForm("chunkCount"))
	if err != nil {
		helper.BadRequest(c, err)
		return
	}
	fileOp := files.NewFileOp()
	tmpDir := path.Join(global.Dir.TmpDir, "upload")
	if !fileOp.Stat(tmpDir) {
		if err := fileOp.CreateDir(tmpDir, constant.DirPerm); err != nil {
			helper.BadRequest(c, err)
			return
		}
	}
	filename := c.PostForm("filename")
	if filename == "" || filepath.Base(filename) != filename || strings.ContainsAny(filename, `/\\`) {
		helper.BadRequest(c, errors.New("invalid filename"))
		return
	}
	uploadID := strings.TrimSpace(c.PostForm("uploadID"))
	cancellable := uploadID != ""
	if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
		helper.BadRequest(c, errors.New("invalid upload ID"))
		return
	}
	if !cancellable {
		uploadID = filename
	}
	fileDir := filepath.Join(tmpDir, uploadID)
	if cancellable && chunkUploadCancelled(uploadID) {
		helper.BadRequest(c, errors.New("upload cancelled"))
		return
	}
	if chunkIndex == 0 {
		if fileOp.Stat(fileDir) {
			_ = fileOp.DeleteDir(fileDir)
		}
		_ = os.MkdirAll(fileDir, constant.DirPerm)
	}
	filePath := filepath.Join(fileDir, filename)

	defer func() {
		if err != nil {
			_ = os.RemoveAll(fileDir)
		}
	}()
	var (
		emptyFile *os.File
		chunkData []byte
	)

	emptyFile, err = os.Create(filePath)
	if err != nil {
		helper.BadRequest(c, err)
		return
	}
	defer emptyFile.Close()

	chunkData, err = io.ReadAll(uploadFile)
	if err != nil {
		helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
		return
	}
	if cancellable && chunkUploadCancelled(uploadID) {
		err = errors.New("upload cancelled")
		helper.BadRequest(c, err)
		return
	}

	chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
	err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
	if err != nil {
		helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
		return
	}

	if chunkIndex+1 == chunkCount {
		overwrite := true
		if ow := c.PostForm("overwrite"); ow != "" {
			overwrite, _ = strconv.ParseBool(ow)
		}
		err = mergeChunks(filename, fileDir, c.PostForm("path"), chunkCount, overwrite)
		if err != nil {
			helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
			return
		}
		if cancellable {
			cancelledChunkUploads.Lock()
			delete(cancelledChunkUploads.ids, uploadID)
			cancelledChunkUploads.Unlock()
		}
		helper.SuccessWithData(c, true)
	} else {
		return
	}
}

// StopChunkUpload removes temporary chunks left by a cancelled upload.
func (b *BaseApi) StopChunkUpload(c *gin.Context) {
	var req request.FileProcessReq
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	uploadID := strings.TrimSpace(req.Key)
	if uploadID == "" || filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`) {
		helper.BadRequest(c, errors.New("invalid upload ID"))
		return
	}
	cancelledChunkUploads.Lock()
	cancelledChunkUploads.ids[uploadID] = struct{}{}
	cancelledChunkUploads.Unlock()
	time.AfterFunc(10*time.Minute, func() {
		cancelledChunkUploads.Lock()
		delete(cancelledChunkUploads.ids, uploadID)
		cancelledChunkUploads.Unlock()
	})
	if err := os.RemoveAll(filepath.Join(global.Dir.TmpDir, "upload", uploadID)); err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

func chunkUploadCancelled(uploadID string) bool {
	cancelledChunkUploads.RLock()
	_, ok := cancelledChunkUploads.ids[uploadID]
	cancelledChunkUploads.RUnlock()
	return ok
}

var wsUpgrade = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

// @Tags File
// @Summary Wget process
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget/process [get]
func (b *BaseApi) WgetProcess(c *gin.Context) {
	if !websocket.IsWebSocketUpgrade(c.Request) {
		helper.Success(c)
		return
	}
	ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		return
	}
	wsClient := websocket2.NewWsClient("fileClient", ws)
	go wsClient.Read()
	go wsClient.Write()
}

// @Tags File
// @Summary Process keys
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget/process/keys [get]
func (b *BaseApi) ProcessKeys(c *gin.Context) {
	res := &response.FileProcessKeys{}
	keys := global.CACHE.PrefixScanKey("file-wget-")
	for _, key := range keys {
		value := global.CACHE.Get(key)
		if value == "" {
			continue
		}
		var process files.Process
		if err := json.Unmarshal([]byte(value), &process); err != nil {
			continue
		}
		res.Keys = append(res.Keys, key)
	}
	helper.SuccessWithData(c, res)
}

// @Tags File
// @Summary Read file by Line
// @Param type path string true "type"
// @Param request body request.FileReadByLineReq true "request"
// @Success 200 {object} response.FileLineContent
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/read/{type} [post]
func (b *BaseApi) ReadFileByLine(c *gin.Context) {
	var req request.FileReadByLineReq
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if readType := strings.TrimSpace(c.Param("type")); readType != "" {
		req.Type = readType
	}
	res, err := fileService.ReadLogByLine(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	if res.TotalLines > 100 {
		helper.SuccessWithDataGzipped(c, res)
	} else {
		helper.SuccessWithData(c, res)
	}
}

// @Tags File
// @Summary Batch change file mode and owner
// @Accept json
// @Param request body request.FileRoleReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/batch/role [post]
// @x-panel-log {"bodyKeys":["paths","mode","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]","formatEN":"Batch change file mode and owner [paths] => [mode]/[user]/[group]"}
func (b *BaseApi) BatchChangeModeAndOwner(c *gin.Context) {
	var req request.FileRoleReq
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if err := fileService.BatchChangeModeAndOwner(req); err != nil {
		helper.InternalServer(c, err)
	}
	helper.Success(c)
}

// @Tags File
// @Summary system mount
// @Accept json
// @Success 200 {object} dto.DiskInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/mount [post]
func (b *BaseApi) GetHostMount(c *gin.Context) {
	disks := fileService.GetHostMount()
	helper.SuccessWithData(c, disks)
}

// @Tags File
// @Summary system user and group
// @Accept json
// @Success 200 {object} response.UserGroupResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/user/group [post]
func (b *BaseApi) GetUsersAndGroups(c *gin.Context) {
	res, err := fileService.GetUsersAndGroups()
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, res)
}

// @Tags File
// @Summary Convert file
// @Accept json
// @Param request body request.FileConvert true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/convert [post]
func (b *BaseApi) ConvertFile(c *gin.Context) {
	var req request.FileConvertRequest
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	fileService.Convert(req)
	helper.SuccessWithData(c, nil)
}

// @Tags File
// @Summary Convert file
// @Accept json
// @Param request body dto.PageInfo true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/convert/log [post]
func (b *BaseApi) ConvertLog(c *gin.Context) {
	var req dto.PageInfo
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	total, logs, err := fileService.ConvertLog(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, dto.PageResult{
		Items: logs,
		Total: total,
	})
}

// @Tags File
// @Summary Batch get file remarks
// @Accept json
// @Param request body request.FileRemarkBatch true "request"
// @Success 200 {object} response.FileRemarksRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/remarks [post]
func (b *BaseApi) BatchGetFileRemarks(c *gin.Context) {
	var req request.FileRemarkBatch
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	remarks := fileService.BatchGetRemarks(req)
	helper.SuccessWithData(c, response.FileRemarksRes{Remarks: remarks})
}

// @Tags File
// @Summary Set file remark
// @Accept json
// @Param request body request.FileRemarkUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/remark [post]
func (b *BaseApi) SetFileRemark(c *gin.Context) {
	var req request.FileRemarkUpdate
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if err := fileService.SetRemark(req); err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary List file shares
// @Accept json
// @Param request body dto.PageInfo true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/share/search [post]
func (b *BaseApi) SearchFileShare(c *gin.Context) {
	var req dto.PageInfo
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	total, list, err := fileShareService.Page(req)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, dto.PageResult{
		Total: total,
		Items: list,
	})
}

// @Tags File
// @Summary Get file share detail by path
// @Accept json
// @Param request body dto.FilePath true "request"
// @Success 200 {object} response.FileShareInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/share/detail [post]
func (b *BaseApi) GetFileShareDetail(c *gin.Context) {
	var req dto.FilePath
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	info, err := fileShareService.GetByPath(req.Path)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, info)
}

// @Tags File
// @Summary Get file share detail by code (no login)
// @Param code query string true "share code"
// @Success 200 {object} response.FileSharePublicInfo
// @Router /files/share/info [get]
func (b *BaseApi) GetPublicFileShareInfo(c *gin.Context) {
	code := strings.TrimSpace(c.Query("code"))
	if code == "" {
		helper.BadRequest(c, errors.New("code is required"))
		return
	}
	info, err := fileShareService.GetPublicByCode(code)
	if err != nil {
		if be, ok := err.(buserr.BusinessError); ok {
			helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
			return
		}
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, info)
}

func buildSharePublicURL(c *gin.Context, code, operateNode string) string {
	scheme := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto"))
	if scheme == "" {
		if c.Request.TLS != nil {
			scheme = "https"
		} else {
			scheme = "http"
		}
	}
	host := strings.TrimSpace(c.GetHeader("X-Forwarded-Host"))
	if host == "" {
		host = c.Request.Host
	}
	shareURL := url.URL{
		Scheme: scheme,
		Host:   host,
		Path:   "/s/" + url.PathEscape(code),
	}
	query := shareURL.Query()
	if strings.TrimSpace(operateNode) != "" {
		query.Set("operateNode", operateNode)
	}
	shareURL.RawQuery = query.Encode()
	return shareURL.String()
}

// @Tags File
// @Summary Get file share QR code image
// @Produce png
// @Param code query string true "share code"
// @Param operateNode query string false "operate node"
// @Success 200 {file} file
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/share/qrcode [get]
func (b *BaseApi) GetFileShareQRCode(c *gin.Context) {
	code := strings.TrimSpace(c.Query("code"))
	if code == "" {
		helper.BadRequest(c, errors.New("code is required"))
		return
	}
	if _, err := fileShareService.GetByCode(code); err != nil {
		if be, ok := err.(buserr.BusinessError); ok {
			helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
			return
		}
		helper.InternalServer(c, err)
		return
	}

	png, err := qrcode.Encode(buildSharePublicURL(c, code, c.Query("operateNode")), qrcode.Medium, 256)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	c.Header("Cache-Control", "private, max-age=300")
	c.Data(http.StatusOK, "image/png", png)
}

// @Tags File
// @Summary Create temporary file share link
// @Accept json
// @Param request body request.FileShareCreate true "request"
// @Success 200 {object} response.FileShareInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/share/create [post]
// @x-panel-log {"bodyKeys":["path","expireMinutes"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建文件分享 [path]","formatEN":"Create file share [path]"}
func (b *BaseApi) CreateFileShare(c *gin.Context) {
	var req request.FileShareCreate
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	res, err := fileShareService.Create(req)
	if err != nil {
		if be, ok := err.(buserr.BusinessError); ok {
			helper.ErrorWithDetail(c, http.StatusInternalServerError, be.Msg, be.Err)
			return
		}
		helper.InternalServer(c, err)
		return
	}
	helper.SuccessWithData(c, res)
}

// @Tags File
// @Summary Delete file share by path
// @Accept json
// @Param request body dto.FilePath true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/share/del [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"关闭文件分享 [path]","formatEN":"Close file share [path]"}
func (b *BaseApi) DeleteFileShare(c *gin.Context) {
	var req dto.FilePath
	if err := helper.CheckBindAndValidate(&req, c); err != nil {
		return
	}
	if err := fileShareService.DeleteByPath(req.Path); err != nil {
		if be, ok := err.(buserr.BusinessError); ok {
			helper.ErrorWithDetail(c, http.StatusInternalServerError, be.Msg, be.Err)
			return
		}
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Check file share code (no login)
// @Param code query string true "share code"
// @Param password query string false "optional password"
// @Success 200 {object} dto.Response
// @Router /files/share/check [get]
func (b *BaseApi) CheckFileShare(c *gin.Context) {
	code := strings.TrimSpace(c.Query("code"))
	password := c.Query("password")
	if code == "" {
		helper.BadRequest(c, errors.New("code is required"))
		return
	}
	if err := fileShareService.Check(code, password); err != nil {
		if be, ok := err.(buserr.BusinessError); ok {
			helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
			return
		}
		helper.InternalServer(c, err)
		return
	}
	helper.Success(c)
}

// @Tags File
// @Summary Download file by share code (no login)
// @Produce octet-stream
// @Param code query string true "share code"
// @Param password query string false "optional password"
// @Success 200 {file} file
// @Router /files/share/download [get]
func (b *BaseApi) DownloadFileShare(c *gin.Context) {
	code := strings.TrimSpace(c.Query("code"))
	password := c.Query("password")
	if code == "" {
		helper.BadRequest(c, errors.New("code is required"))
		return
	}
	filePath, displayName, err := fileShareService.PrepareDownload(code, password)
	if err != nil {
		if be, ok := err.(buserr.BusinessError); ok {
			helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
			return
		}
		helper.InternalServer(c, err)
		return
	}
	if files.ShouldDenySensitiveFileRead(filePath) {
		helper.InternalServer(c, buserr.New("ErrSensitiveFileRead"))
		return
	}
	file, err := os.Open(filePath)
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	defer file.Close()
	info, err := file.Stat()
	if err != nil {
		helper.InternalServer(c, err)
		return
	}
	c.Header("Content-Length", strconv.FormatInt(info.Size(), 10))
	c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(displayName))
	http.ServeContent(c.Writer, c.Request, displayName, info.ModTime(), file)
}
