//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

// Gap B — `POST /api/v1/agents/<agent_id>/upload` (Python
// api/apps/restful_apis/agent_api.py:761-790).
//
// Mirrors the python upload_agent_file handler:
//   - @_require_canvas_access_async  → loader.LoadCanvasByID (returns
//     ErrUserCanvasNotFound for both missing and forbidden; we map that
//     to CodeOperatingError (103) with the python permission message
//     instead of the chat-path 103 "canvas not found.")
//   - single file  + ?url=  → FileService.upload_info(tenant, file, url)
//     via UploadFromURL (URL-import mode)
//   - single file  + no url  → FileService.upload_info(tenant, file)
//     via UploadInfos with a one-element slice
//   - multi file  + any url  → FileService.upload_info * N
//     via UploadInfos (Python ignores ?url= on the multi-file path)
//
// 64 MB upload cap (D3 default).

package handler

import (
	"net/http"

	"github.com/gin-gonic/gin"

	"ragflow/internal/common"
	"ragflow/internal/dao"
)

// uploadMaxBytes caps the multipart form body at 64 MB. The python
// reference relies on Quart/werkzeug defaults which are well above
// this; we set it explicitly so the cap is auditable and stable across
// test environments.
const uploadMaxBytes int64 = 64 << 20 // 64 MiB

// canvasNoAccessMessage mirrors the python permission error
// (api/apps/restful_apis/agent_api.py:78,89). Kept identical to
// python so existing clients can pattern-match the message text.
const canvasNoAccessMessage = "Make sure you have permission to access the agent."

// UploadAgentFile POST /api/v1/agents/:canvas_id/upload
func (h *AgentHandler) UploadAgentFile(c *gin.Context) {
	user, code, msg := GetUser(c)
	if code != common.CodeSuccess {
		jsonError(c, code, msg)
		return
	}
	canvasID := c.Param("canvas_id")
	if canvasID == "" {
		jsonError(c, common.CodeArgumentError, "`canvas_id` is required.")
		return
	}

	// Canvas access check: matches python @_require_canvas_access_async.
	// We deliberately do NOT differentiate "missing" from "forbidden"
	// (LoadCanvasByID collapses both into ErrUserCanvasNotFound) for
	// IDOR mitigation; the user-visible envelope uses OPERATING_ERROR
	// (103) with the python permission message so existing clients can
	// still pattern-match the text.
	if _, err := h.loader.LoadCanvasByID(c.Request.Context(), user.ID, canvasID); err != nil {
		if err == dao.ErrUserCanvasNotFound {
			jsonError(c, common.CodeOperatingError, canvasNoAccessMessage)
			return
		}
		jsonError(c, common.CodeServerError, err.Error())
		return
	}

	// Hard cap the body before any parsing (security review H3).
	// Without MaxBytesReader, a 1 GB request body is fully drained
	// into memory by ParseMultipartForm before any size check fires.
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, uploadMaxBytes)
	if cl := c.Request.ContentLength; cl > uploadMaxBytes {
		jsonError(c, common.CodeArgumentError, "request body too large.")
		return
	}
	if err := c.Request.ParseMultipartForm(uploadMaxBytes); err != nil {
		jsonError(c, common.CodeArgumentError, "invalid multipart form: "+err.Error())
		return
	}
	defer func() {
		if c.Request.MultipartForm != nil {
			_ = c.Request.MultipartForm.RemoveAll()
		}
	}()
	form := c.Request.MultipartForm
	if form == nil {
		jsonError(c, common.CodeArgumentError, "missing multipart form.")
		return
	}
	files := form.File["file"]

	// URL-import mode: matches python's behaviour exactly
	// (api/apps/restful_apis/agent_api.py:775-783). The url query
	// param is consulted ONLY on the single-file branch; for 0 or
	// >1 files, the url is silently ignored and the request flows
	// into the normal UploadInfos path. We replicate that with a
	// guard that dispatches to UploadFromURL only when both
	// conditions are met.
	if url := c.Query("url"); url != "" && len(files) == 1 {
		uploaded, err := h.fileService.UploadFromURL(user.ID, url)
		if err != nil {
			jsonError(c, common.CodeServerError, err.Error())
			return
		}
		c.JSON(200, gin.H{
			"code":    common.CodeSuccess,
			"data":    uploaded,
			"message": "success",
		})
		return
	}

	if len(files) == 0 {
		jsonError(c, common.CodeArgumentError, "`file` field is required.")
		return
	}

	results, err := h.fileService.UploadInfos(user.ID, files)
	if err != nil {
		jsonError(c, common.CodeServerError, err.Error())
		return
	}

	// Python parity: 1 file → single dict; >1 → list.
	var payload any
	if len(results) == 1 {
		payload = results[0]
	} else {
		payload = results
	}
	c.JSON(200, gin.H{
		"code":    common.CodeSuccess,
		"data":    payload,
		"message": "success",
	})
}
