package tools

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

	"security-center-mcp/internal/client"
)

// This file defines upload_vulnerability_attachment (P3-T5, plan
// §3.6/§3.8). Unlike the four JSON write tools in vuln_write.go it is
// not an EndpointRef mapping: the tool orchestrates the three-step
// attachment flow on behalf of the LLM — ① POST /uploads to presign,
// ② client.PutRaw the local file bytes to the presigned URL,
// ③ POST /uploads/confirm — and returns only the attachment_id (+key).
// Dispatch lives in vuln.go's dispatchVuln (case
// "upload_vulnerability_attachment").
//
// Contract source: the embedded swagger snapshot (handler.
// uploadAttachmentRequest{filename, content_type} → {attachment_id,
// upload_url, key, expires_in}; handler.confirmAttachmentUploadRequest
// {attachment_id}).

const (
	// uploadPresignPath is step 1 (create pending attachment + presign).
	uploadPresignPath = "/api/v1/vulnerabilities/uploads"
	// uploadConfirmPath is step 3 (validate the object, mark ready).
	uploadConfirmPath = "/api/v1/vulnerabilities/uploads/confirm"

	// maxAttachmentBytes is the backend's 10MB image cap, enforced
	// locally so oversized files fail before any HTTP call (the
	// backend re-validates size and real content type at confirm time).
	maxAttachmentBytes = 10 << 20
)

// attachmentContentTypes maps the supported file extensions (lowercase,
// with dot) to the content type declared at presign time. SVG is
// deliberately absent — the backend contract is raster images only.
var attachmentContentTypes = map[string]string{
	".png":  "image/png",
	".jpg":  "image/jpeg",
	".jpeg": "image/jpeg",
	".webp": "image/webp",
	".gif":  "image/gif",
}

func vulnUploadTool() Tool {
	return Tool{
		Name: "upload_vulnerability_attachment",
		Description: "上传一张本地图片作为漏洞附件：给本地文件路径即可——申请预签名 → 直传对象存储 → 确认三步由本服务自动完成，" +
			"返回 attachment_id（在 submit_vulnerability / update_vulnerability 的 description HTML 里用 " +
			"data-sc-attachment-id=\"<attachment_id>\" 引用）。限制：仅图片（png/jpeg/webp/gif，SVG 不允许）、大小 ≤10MB，" +
			"格式不支持或超限会在本地直接报错不发请求；后端确认时还会复核真实内容类型与大小，不合规对象会被删除。" +
			"content_type 缺省按扩展名推断，显式指定时必须与文件真实类型一致（预签名地址绑定 Content-Type，不一致会直传失败）。" +
			"已知限制：依赖 MCP server 与你同机运行（stdio 子进程，读取的是 server 本地文件系统上的路径），远程 MCP 形态暂不支持。",
		InputSchema: objectSchema(map[string]Property{
			"file_path": strProp("本地图片文件路径（必填；png/jpg/jpeg/webp/gif，≤10MB；读的是 MCP server 所在机器的文件系统）"),
			"content_type": {Type: "string", Description: "图片 Content-Type（可选，缺省按扩展名推断；必须与文件真实类型一致）",
				Enum: []string{"image/png", "image/jpeg", "image/webp", "image/gif"}},
		}, "file_path"),
		// 编排工具：两次 POST 由 vulnUploadAttachment 按序驱动（中间夹
		// PutRaw 直传），不走 callEndpoint，故无 ParamMap/BodyMap；
		// Endpoints 仅登记契约端点供注册表级 swagger 校验覆盖。
		Endpoints: []EndpointRef{
			{Method: "post", Path: uploadPresignPath},
			{Method: "post", Path: uploadConfirmPath},
		},
	}
}

// vulnUploadAttachment runs the three-step flow. Local validation
// (existence, extension, 10MB cap, readable) fails fast as *ArgError
// before any HTTP call; a backend/transport failure at any step aborts
// the flow and becomes the structured error payload with the failed
// step named in the message.
func vulnUploadAttachment(ctx context.Context, c *client.Client, args map[string]any) (any, bool, error) {
	filePath, _ := args["file_path"].(string)
	if filePath == "" {
		return nil, true, &ArgError{Msg: "缺少必填参数 file_path（本地图片文件路径，png/jpeg/webp/gif，≤10MB）"}
	}

	ext := strings.ToLower(filepath.Ext(filePath))
	contentType, ok := attachmentContentTypes[ext]
	if !ok {
		return nil, true, &ArgError{Msg: fmt.Sprintf(
			"文件 %s 的扩展名 %q 不受支持：仅支持 png/jpg/jpeg/webp/gif 图片（SVG 不允许）",
			filepath.Base(filePath), filepath.Ext(filePath))}
	}
	if declared, _ := args["content_type"].(string); declared != "" {
		if !supportedAttachmentContentType(declared) {
			return nil, true, &ArgError{Msg: fmt.Sprintf(
				"参数 content_type 取值 %q 不受支持：仅支持 image/png、image/jpeg、image/webp、image/gif", declared)}
		}
		contentType = declared
	}

	info, err := os.Stat(filePath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, true, &ArgError{Msg: fmt.Sprintf(
				"本地文件不存在：%s（读取的是 MCP server 所在机器的文件系统）", filePath)}
		}
		return nil, true, &ArgError{Msg: fmt.Sprintf("无法访问本地文件 %s：%v", filePath, err)}
	}
	if info.IsDir() {
		return nil, true, &ArgError{Msg: fmt.Sprintf("本地路径 %s 是目录，不是图片文件", filePath)}
	}
	if info.Size() > maxAttachmentBytes {
		return nil, true, &ArgError{Msg: fmt.Sprintf(
			"文件 %s 大小 %.1fMB，超过 10MB 上限", filepath.Base(filePath), float64(info.Size())/(1<<20))}
	}
	content, err := os.ReadFile(filePath)
	if err != nil {
		return nil, true, &ArgError{Msg: fmt.Sprintf("读取本地文件 %s 失败：%v", filePath, err)}
	}

	// ① 申请预签名（Content-Type 在此声明，第②步必须原样使用——
	// S3 预签名绑定 Content-Type，不一致会拒收）。
	var presign struct {
		AttachmentID uint64 `json:"attachment_id"`
		UploadURL    string `json:"upload_url"`
		Key          string `json:"key"`
		ExpiresIn    int64  `json:"expires_in"`
	}
	presignBody := map[string]any{
		"filename":     filepath.Base(filePath),
		"content_type": contentType,
	}
	if err := c.Do(ctx, http.MethodPost, uploadPresignPath, nil, presignBody, &presign); err != nil {
		return uploadStepFailure("第 1 步（申请预签名上传地址）", err)
	}
	if presign.AttachmentID == 0 || presign.UploadURL == "" {
		return uploadStepFailure("第 1 步（申请预签名上传地址）",
			fmt.Errorf("后端响应缺少 attachment_id 或 upload_url"))
	}

	// ② 直传字节到预签名地址（不带 X-API-Key，预签名本身即凭证）。
	if err := c.PutRaw(ctx, presign.UploadURL, content, contentType); err != nil {
		return uploadStepFailure("第 2 步（直传文件到对象存储）", err)
	}

	// ③ 确认（后端复核真实内容类型与 ≤10MB，不合规即删并拒绝）。
	confirmBody := map[string]any{"attachment_id": presign.AttachmentID}
	if err := c.Do(ctx, http.MethodPost, uploadConfirmPath, nil, confirmBody, nil); err != nil {
		return uploadStepFailure("第 3 步（确认附件）", err)
	}

	// 可观测性：stdout 只走 JSON-RPC，上传这种有外部副作用的操作留
	// 一行 stderr。不记文件内容、不记预签名 URL（含签名凭证）。
	fmt.Fprintf(os.Stderr, "security-center-mcp: tools: upload_vulnerability_attachment: confirmed attachment_id=%d key=%s (%d bytes, %s)\n",
		presign.AttachmentID, presign.Key, len(content), contentType)

	return map[string]any{
		"attachment_id": presign.AttachmentID,
		"key":           presign.Key,
	}, true, nil
}

// supportedAttachmentContentType reports whether ct is one of the four
// image content types the backend accepts.
func supportedAttachmentContentType(ct string) bool {
	for _, supported := range attachmentContentTypes {
		if ct == supported {
			return true
		}
	}
	return false
}

// uploadStepFailure maps a failed orchestration step to the dispatch
// triple: the same structured payload errorResult would build (numeric
// code kept for *client.APIError), with the failed step named in the
// message so the LLM can tell which of the three steps went wrong.
func uploadStepFailure(step string, err error) (any, bool, error) {
	payload, _ := errorResult(err).(map[string]any)
	if e, ok := payload["error"].(map[string]any); ok {
		e["message"] = fmt.Sprintf("上传附件%s失败：%v", step, e["message"])
	}
	return payload, true, nil
}
