package handler

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path"
	"strings"
	"syscall"
	"time"

	"github.com/artalkjs/artalk/v2/internal/config"
	"github.com/artalkjs/artalk/v2/internal/core"
	"github.com/artalkjs/artalk/v2/internal/i18n"
	"github.com/artalkjs/artalk/v2/internal/log"
	"github.com/artalkjs/artalk/v2/internal/utils"
	"github.com/artalkjs/artalk/v2/server/common"
	"github.com/gofiber/fiber/v2"
)

type ParamsUpload struct {
}

type ResponseUpload struct {
	FileType  string `json:"file_type" enum:"image"`
	FileName  string `json:"file_name"`
	PublicURL string `json:"public_url"`
}

// @Id           Upload
// @Summary      Upload
// @Description  Upload file from this endpoint
// @Tags         Upload
// @Param        file           formData  file    true   "Upload file"
// @Security     ApiKeyAuth
// @Accept       mpfd
// @Produce      json
// @Success      200  {object}  ResponseUpload
// @Failure      400  {object}  Map{msg=string}
// @Failure      403  {object}  Map{msg=string}
// @Failure      500  {object}  Map{msg=string}
// @Router       /upload  [post]
func Upload(app *core.App, router fiber.Router) {
	router.Post("/upload", common.LimiterGuard(app, func(c *fiber.Ctx) error {
		// 功能开关 (管理员始终开启)
		if !app.Conf().ImgUpload.Enabled && !common.CheckIsAdminReq(app, c) {
			return common.RespError(c, 403, i18n.T("Image upload forbidden"), common.Map{
				"img_upload_enabled": false,
			})
		}

		// 传入参数解析
		var p ParamsUpload
		if isOK, resp := common.ParamsDecode(c, &p); !isOK {
			return resp
		}

		// find page
		// page := entity.FindPage(p.PageKey, p.PageTitle)
		// ip := c.RealIP()
		// ua := c.Request().UserAgent()

		// 图片大小限制 (Based on content length)
		if app.Conf().ImgUpload.MaxSize != 0 {
			if int64(c.Request().Header.ContentLength()) > app.Conf().ImgUpload.MaxSize*1024*1024 {
				return common.RespError(c, 400, i18n.T("Image exceeds {{file_size}} limit", Map{
					"file_size": fmt.Sprintf("%dMB", app.Conf().ImgUpload.MaxSize),
				}))
			}
		}

		// 获取 Form
		file, err := c.FormFile("file")
		if err != nil {
			log.Error(err)
			return common.RespError(c, 500, "File read failed")
		}

		// 打开文件
		src, err := file.Open()
		if err != nil {
			log.Error(err)
			return common.RespError(c, 500, "File open failed")
		}
		defer src.Close()

		// 读取文件
		buf, err := io.ReadAll(src)
		if err != nil {
			log.Error(err)
			return common.RespError(c, 500, "File read failed")
		}

		// 大小限制 (Based on content read)
		if app.Conf().ImgUpload.MaxSize != 0 {
			if int64(len(buf)) > app.Conf().ImgUpload.MaxSize*1024*1024 {
				return common.RespError(c, 400, i18n.T("Image exceeds {{file_size}} limit", Map{
					"file_size": fmt.Sprintf("%dMB", app.Conf().ImgUpload.MaxSize),
				}))
			}
		}

		// 文件格式判断
		// The http.DetectContentType function reads the first 512 bytes of a file
		// and uses these bytes (aka the magic number) to determine the file's content type.
		// @link https://mimesniff.spec.whatwg.org/
		// @link https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types
		fileMine := http.DetectContentType(buf)
		allowMines := []string{
			"image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp",
			// "image/svg+xml",
		}
		if !utils.ContainsStr(allowMines, fileMine) {
			return common.RespError(c, 400, i18n.T("Unsupported formats"))
		}

		// 图片文件名
		mineToExts := map[string]string{
			"image/jpeg": ".jpg",
			"image/png":  ".png",
			"image/gif":  ".gif",
			"image/webp": ".webp",
			"image/bmp":  ".bmp",
			// "image/svg+xml": ".svg",
		}

		t := time.Now()
		filename := t.Format("20060102-150405.000") + mineToExts[fileMine]

		// 创建图片目标文件
		if err := utils.EnsureDir(app.Conf().ImgUpload.Path); err != nil {
			log.Error(err)
			return common.RespError(c, 500, "Folder creation failed")
		}

		fileFullPath := strings.TrimSuffix(app.Conf().ImgUpload.Path, "/") + "/" + filename
		dst, err := os.Create(fileFullPath)
		if err != nil {
			log.Error(err)
			return common.RespError(c, 500, "File creation failed")
		}
		defer dst.Close()

		// 写入图片文件
		if _, err = dst.Write(buf); err != nil {
			log.Error(err)
			return common.RespError(c, 500, "File write failed")
		}

		// 生成外部可访问链接
		baseURL := app.Conf().ImgUpload.PublicPath
		if baseURL == "" {
			baseURL = config.IMG_UPLOAD_PUBLIC_PATH
		}

		var imgURL string
		if utils.ValidateURL(baseURL) {
			// full url
			imgURL = strings.TrimSuffix(baseURL, "/") + "/" + filename
		} else {
			// relative path
			imgURL = path.Join(baseURL, filename)
		}

		// 使用 upgit
		if app.Conf().ImgUpload.Upgit.Enabled {
			upgitURL := execUpgitUpload(app.Conf().ImgUpload.Upgit.Exec, fileFullPath)
			if upgitURL == "" || !utils.ValidateURL(upgitURL) {
				// 上传失败，删除源图片文件
				var err = os.Remove(fileFullPath)
				if err != nil {
					log.Error(err)
				}

				log.Error("[IMG_UPLOAD] [upgit] upgit output: ", upgitURL)
				return common.RespError(c, 500, i18n.T("Upload image via {{method}} failed", Map{"method": "upgit"}))
			}

			// 上传成功，删除本地文件
			if app.Conf().ImgUpload.Upgit.DelLocal {
				var err = os.Remove(fileFullPath)
				if err != nil {
					log.Error(err)
				}
			}

			// 使用从 upgit 获取的图片 URL
			imgURL = upgitURL
		}

		// 响应数据
		return common.RespData(c, ResponseUpload{
			FileType:  "image",
			FileName:  filename,
			PublicURL: imgURL,
		})
	}))
}

// Call UpGit to upload images
func execUpgitUpload(execCommand string, filename string) string {
	LogTag := "[IMG_UPLOAD] [upgit] "

	// Separate the command and arguments
	cmdStrSplitted := strings.Split(execCommand, " ")

	// For security reasons, Artalk no longer allows you to specify the executable file path of UpGit.
	// execApp := cmdStrSplitted[0]
	execApp := "upgit"

	execArgs := cmdStrSplitted[1:]
	execArgs = append(execArgs, filename)

	// Execute the command
	cmd := exec.Command(execApp, execArgs...)
	stdout, _ := cmd.StdoutPipe()

	if err := cmd.Start(); err != nil {
		log.Error(LogTag, "cmd.Start: ", err)
		return ""
	}

	result, _ := io.ReadAll(stdout)
	if err := cmd.Wait(); err != nil {
		if exiterr, ok := err.(*exec.ExitError); ok {
			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
				log.Error(LogTag, "Exit Status: ", status.ExitStatus())
			}
		} else {
			log.Error(LogTag, "cmd.Wait: ", err)
		}

		return ""
	}

	return strings.TrimSpace(string(result))
}
