package wiki

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gitlink-org/gitlink-cli/internal/auth"
	"github.com/gitlink-org/gitlink-cli/internal/client"
	clierrors "github.com/gitlink-org/gitlink-cli/internal/errors"
	"github.com/gitlink-org/gitlink-cli/internal/output"
	"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)

var projectIDCache sync.Map

func wikiPath(endpoint string) string {
	return "/wiki/open/" + endpoint
}

// getGatewayClient returns a client targeting the Wiki API gateway.
// The BaseURL is resolved from RuntimeContext.GatewayBaseURL, which in turn
// honours (in order): GITLINK_GATEWAY_URL env > config gateway_base_url > default.
// HTTP client falls back to auth.NewHTTPClient() when ctx.GatewayHTTPClient is nil.
func getGatewayClient(ctx *common.RuntimeContext) *client.Client {
	baseURL := ctx.GatewayBaseURL
	if baseURL == "" {
		baseURL = "https://gateway.gitlink.org.cn/api"
	}
	httpClient := ctx.GatewayHTTPClient
	if httpClient == nil {
		httpClient = auth.NewHTTPClient()
	}
	return &client.Client{
		HTTP:           httpClient,
		BaseURL:        baseURL,
		SkipJSONSuffix: true,
		Debug:          ctx.Client.Debug,
	}
}

func callWikiAPI(ctx *common.RuntimeContext, method, path string, body interface{}) (*output.Envelope, error) {
	gc := getGatewayClient(ctx)
	env, err := gc.Do(method, path, body, nil)
	if err != nil {
		return nil, err
	}
	return unwrapGatewayResponse(env)
}

func callWikiAPIWithQuery(ctx *common.RuntimeContext, method, path string, query url.Values) (*output.Envelope, error) {
	gc := getGatewayClient(ctx)
	env, err := gc.Do(method, path, nil, query)
	if err != nil {
		return nil, err
	}
	return unwrapGatewayResponse(env)
}

func unwrapGatewayResponse(env *output.Envelope) (*output.Envelope, error) {
	resp, ok := env.Data.(map[string]interface{})
	if !ok {
		return env, nil
	}
	if code, ok := resp["code"]; ok {
		switch v := code.(type) {
		case float64:
			// HTTP 2xx 全部视为成功（200 OK / 201 Created / 202 Accepted / 204 No Content 等）
			// 之前只接受 200/201，导致 DELETE 返回 204 时被误判为失败
			if v < 200 || v >= 300 {
				msg, _ := resp["msg"].(string)
				// 必须返回 *clierrors.CLIError，否则 shortcuts/common.TryPrintError
				// 的 errors.As 无法识别，错误就不会按 envelope 格式输出，
				// 会回退到 stderr 的纯文本输出（破坏 --format json/table/yaml）。
				kind := clierrors.KindServer
				if int(v) == 404 {
					kind = clierrors.KindNotFound
				} else if int(v) == 401 || int(v) == 403 {
					kind = clierrors.KindForbidden
				}
				return nil, clierrors.New(kind, msg,
					"检查 owner/repo 是否正确，或确认仓库已在 GitLink 网页端开启 Wiki 功能")
			}
		}
	}
	if innerData, ok := resp["data"]; ok {
		return output.SuccessEnvelope(innerData, env.Meta), nil
	}
	return env, nil
}

func resolveProjectID(ctx *common.RuntimeContext) (string, error) {
	key := ctx.Owner + "/" + ctx.Repo
	if cached, ok := projectIDCache.Load(key); ok {
		return cached.(string), nil
	}

	path := fmt.Sprintf("/%s/%s/detail", ctx.Owner, ctx.Repo)
	env, err := ctx.CallAPI("GET", path, nil)
	if err != nil {
		return "", fmt.Errorf("failed to fetch project details (needed for projectId): %w", err)
	}

	data, ok := env.Data.(map[string]interface{})
	if !ok {
		return "", fmt.Errorf("unexpected response from project detail API")
	}

	pid, ok := data["project_id"]
	if !ok {
		return "", fmt.Errorf("project_id not found in project detail response")
	}

	var pidStr string
	switch v := pid.(type) {
	case float64:
		pidStr = fmt.Sprintf("%.0f", v)
	case int:
		pidStr = fmt.Sprintf("%d", v)
	default:
		pidStr = fmt.Sprintf("%v", v)
	}

	projectIDCache.Store(key, pidStr)
	return pidStr, nil
}

func parseProjectIDInt(pid string) int {
	n, _ := strconv.Atoi(pid)
	return n
}

func resolveUpdateContent(ctx *common.RuntimeContext, text, filePath string) (string, error) {
	if text != "" {
		return text, nil
	}
	if filePath != "" {
		data, err := os.ReadFile(filePath)
		if err != nil {
			return "", fmt.Errorf("failed to read file %s: %w", filePath, err)
		}
		return string(data), nil
	}
	return "", fmt.Errorf("no content provided")
}

// fetchPageContent 获取 wiki 页面明文内容。
//
// 自动重试策略：GitLink 后端创建 wiki 时会自动给 sub_url 追加 ".-" 后缀，
// 而 wiki +list 返回的 title 不带后缀。若首次用原始 pageName 查询失败且
// pageName 不带 ".-" 后缀，自动用 pageName+".-" 重试一次。
//
// 返回值：
//   - content: 解码后的明文 markdown
//   - actualPageName: 实际查询成功的 pageName（可能带 ".-" 后缀），供调用方做后续写操作
//   - err: 错误信息
func fetchPageContent(ctx *common.RuntimeContext, projectID, pageName string) (content string, actualPageName string, err error) {
	c, actual, err := fetchPageContentOnce(ctx, projectID, pageName)
	if err == nil {
		return c, actual, nil
	}
	// 首次失败且 pageName 不带 ".-" 后缀：自动重试一次
	if !strings.HasSuffix(pageName, ".-") {
		c2, actual2, err2 := fetchPageContentOnce(ctx, projectID, pageName+".-")
		if err2 == nil {
			return c2, actual2, nil
		}
	}
	return "", "", fmt.Errorf("获取 Wiki 页面现有内容失败: %w", err)
}

// fetchPageContentOnce 单次尝试获取 wiki 页面内容（不做重试）。
func fetchPageContentOnce(ctx *common.RuntimeContext, projectID, pageName string) (string, string, error) {
	q := url.Values{}
	q.Set("owner", ctx.Owner)
	q.Set("repo", ctx.Repo)
	q.Set("projectId", projectID)
	q.Set("pageName", pageName)
	env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q)
	if err != nil {
		return "", "", err
	}
	data, ok := env.Data.(map[string]interface{})
	if !ok {
		return "", "", fmt.Errorf("unexpected response from getWiki")
	}
	b64, _ := data["content_base64"].(string)
	if b64 == "" {
		return "", pageName, nil
	}
	decoded, err := base64.StdEncoding.DecodeString(b64)
	if err != nil {
		return "", "", fmt.Errorf("failed to decode page content: %w", err)
	}
	return string(decoded), pageName, nil
}

// fetchWikiPage 按 pageName 查询 wiki 页面，返回完整的 envelope。
// 调用方负责处理错误（包括 404）和后续的 sub_url 重试逻辑。
func fetchWikiPage(ctx *common.RuntimeContext, projectID, pageName string) (*output.Envelope, error) {
	q := url.Values{}
	q.Set("owner", ctx.Owner)
	q.Set("repo", ctx.Repo)
	q.Set("projectId", projectID)
	q.Set("pageName", pageName)
	return callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q)
}

func resolveContent(ctx *common.RuntimeContext) (string, error) {
	if content := ctx.Arg("content"); content != "" {
		return content, nil
	}
	if filePath := ctx.Arg("file"); filePath != "" {
		data, err := os.ReadFile(filePath)
		if err != nil {
			return "", fmt.Errorf("failed to read file %s: %w", filePath, err)
		}
		return string(data), nil
	}
	return "", fmt.Errorf("--content or --file is required to provide wiki page content")
}

func cleanWikiList(env *output.Envelope) {
	items, ok := env.Data.([]interface{})
	if !ok {
		return
	}
	for _, item := range items {
		m, ok := item.(map[string]interface{})
		if !ok {
			continue
		}
		delete(m, "wiki_clone_link")
		if raw, ok := m["sub_url"].(string); ok {
			if decoded, err := url.QueryUnescape(raw); err == nil {
				m["sub_url"] = decoded
			}
		}
	}
}

// outputWithDecodedContent 解码 wiki 响应中的所有 base64 字段，用明文替换原始乱码。
//
// 设计权衡（agent 友好性）:
//   - 后端返回的字段（content_base64、sidebar、footer）都是 base64 编码，对 agent 不可读
//   - 解码后用明文替换/移除原始字段，agent 可直接阅读、抽取、总结
//   - 节省 ~33% token（base64 编码膨胀部分）
//
// 已知字段映射：
//   - content_base64 → content（重命名，删除原字段）
//   - sidebar        → sidebar（原地替换，仅当解码成功）
//   - footer         → footer（原地替换，仅当解码成功）
//
// 安全策略：仅当 base64.StdEncoding.DecodeString 成功时才替换；
// 若后端某天改为明文，解码失败会自动跳过，不影响兼容性。
func outputWithDecodedContent(ctx *common.RuntimeContext, env *output.Envelope) error {
	data := env.Data

	// 后端某些端点（如 createWiki/updateWiki）把 data 返回为 JSON 字符串，
	// client.go 会把它解析为 json.RawMessage（而非 map）；这里先转回 map 再处理。
	// view/getWiki 端点直接返回 JSON 对象，data 已是 map[string]interface{}。
	if raw, ok := data.(json.RawMessage); ok {
		var m map[string]interface{}
		if err := json.Unmarshal(raw, &m); err == nil {
			data = m
			env.Data = m
		}
	}

	m, ok := data.(map[string]interface{})
	if !ok {
		return ctx.Output(env)
	}
	// content_base64 → content（重命名）
	if b64, ok := m["content_base64"].(string); ok && b64 != "" {
		if decoded, err := base64.StdEncoding.DecodeString(b64); err == nil {
			m["content"] = string(decoded)
			delete(m, "content_base64")
		}
	}
	// sidebar / footer：原地替换（仅当能解码为 base64 时）
	for _, field := range []string{"sidebar", "footer"} {
		if b64, ok := m[field].(string); ok && b64 != "" {
			if decoded, err := base64.StdEncoding.DecodeString(b64); err == nil {
				m[field] = string(decoded)
			}
		}
	}
	return ctx.Output(env)
}

// --- lint types and implementation ---

type LintIssue struct {
	Page    string `json:"page"`
	Level   string `json:"level"`
	Check   string `json:"check"`
	Message string `json:"message"`
}

type LintSummary struct {
	Repository  string      `json:"repository"`
	TotalPages  int         `json:"total_pages"`
	TotalIssues int         `json:"total_issues"`
	Errors      int         `json:"errors"`
	Warnings    int         `json:"warnings"`
	Results     []LintIssue `json:"results"`
}

var (
	mdLinkRe    = regexp.MustCompile(`\[([^\]]*)\]\(([^)]+)\)`)
	imageLinkRe = regexp.MustCompile(`!\[([^\]]*)\]\((https?://[^)]+)\)`)
)

func isCheckEnabled(checkFilter, name string) bool {
	if checkFilter == "" {
		return true
	}
	for _, c := range strings.Split(checkFilter, ",") {
		if strings.TrimSpace(c) == name {
			return true
		}
	}
	return false
}

func checkEmpty(page, content string) []LintIssue {
	if strings.TrimSpace(content) == "" {
		return []LintIssue{{Page: page, Level: "error", Check: "empty", Message: "page is empty"}}
	}
	return nil
}

func checkHeading(page, content string) []LintIssue {
	if content != "" && !strings.HasPrefix(strings.TrimSpace(content), "# ") {
		return []LintIssue{{Page: page, Level: "warning", Check: "headings", Message: "missing H1 heading"}}
	}
	return nil
}

func checkShort(page, content string) []LintIssue {
	if content != "" && len(content) < 50 {
		return []LintIssue{{Page: page, Level: "warning", Check: "short", Message: fmt.Sprintf("content too short (%d chars)", len(content))}}
	}
	return nil
}

func checkDeadLinks(page, content string, knownTitles map[string]bool) []LintIssue {
	var issues []LintIssue
	for _, m := range mdLinkRe.FindAllStringSubmatch(content, -1) {
		if len(m) < 3 {
			continue
		}
		target := m[2]
		// skip external links and anchors
		if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "#") {
			continue
		}
		decoded, _ := url.PathUnescape(target)
		if decoded == "" {
			decoded = target
		}
		if !knownTitles[decoded] && !knownTitles[target] {
			issues = append(issues, LintIssue{
				Page:    page,
				Level:   "error",
				Check:   "links",
				Message: fmt.Sprintf("dead link: [%s](%s) -> page %q not found", m[1], target, decoded),
			})
		}
	}
	return issues
}

func checkImages(page, content string, httpClient *http.Client) []LintIssue {
	var issues []LintIssue
	for _, m := range imageLinkRe.FindAllStringSubmatch(content, -1) {
		if len(m) < 3 {
			continue
		}
		imgURL := m[2]
		req, err := http.NewRequest("HEAD", imgURL, nil)
		if err != nil {
			issues = append(issues, LintIssue{Page: page, Level: "warning", Check: "images", Message: fmt.Sprintf("broken image: %s -> invalid URL", imgURL)})
			continue
		}
		resp, err := httpClient.Do(req)
		if err != nil {
			issues = append(issues, LintIssue{Page: page, Level: "warning", Check: "images", Message: fmt.Sprintf("broken image: %s -> unreachable", imgURL)})
			continue
		}
		resp.Body.Close()
		if resp.StatusCode >= 400 {
			issues = append(issues, LintIssue{Page: page, Level: "warning", Check: "images", Message: fmt.Sprintf("broken image: %s -> %d", imgURL, resp.StatusCode)})
		}
	}
	return issues
}

func runLint(ctx *common.RuntimeContext) error {
	if err := ctx.ResolveOwnerRepo(); err != nil {
		return err
	}

	checkFilter := ctx.Arg("check")
	fmt.Fprintf(os.Stderr, "Linting wiki pages for %s/%s...\n\n", ctx.Owner, ctx.Repo)

	projectID, err := resolveProjectID(ctx)
	if err != nil {
		return err
	}

	// Fetch page list
	q := url.Values{}
	q.Set("owner", ctx.Owner)
	q.Set("repo", ctx.Repo)
	q.Set("projectId", projectID)
	env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("wikiPages"), q)
	if err != nil {
		return fmt.Errorf("failed to list wiki pages: %w", err)
	}

	// Parse pages: title for display/link-check, sub_url for fetching
	type pageInfo struct {
		title  string
		subURL string
	}
	knownTitles := make(map[string]bool)
	var pages []pageInfo
	if items, ok := env.Data.([]interface{}); ok {
		for _, item := range items {
			m, ok := item.(map[string]interface{})
			if !ok {
				continue
			}
			title, _ := m["title"].(string)
			subURL, _ := m["sub_url"].(string)
			if title != "" {
				knownTitles[title] = true
				if subURL == "" {
					subURL = title
				}
				pages = append(pages, pageInfo{title: title, subURL: subURL})
			}
		}
	}

	if len(pages) == 0 {
		fmt.Fprintln(os.Stderr, "No wiki pages found.")
		return ctx.OutputData(LintSummary{
			Repository: ctx.Owner + "/" + ctx.Repo,
			Results:    []LintIssue{},
		})
	}

	// HTTP client for image checks with 5s timeout
	httpClient := &http.Client{Timeout: 5 * time.Second}

	var allIssues []LintIssue

	for _, p := range pages {
		// Skip system pages (e.g. _Sidebar, _Footer, _Header)
		if strings.HasPrefix(p.title, "_") {
			continue
		}
		content, _, err := fetchPageContent(ctx, projectID, p.subURL)
		if err != nil {
			allIssues = append(allIssues, LintIssue{Page: p.title, Level: "error", Check: "fetch", Message: fmt.Sprintf("failed to fetch: %v", err)})
			continue
		}

		if isCheckEnabled(checkFilter, "empty") {
			allIssues = append(allIssues, checkEmpty(p.title, content)...)
		}
		if isCheckEnabled(checkFilter, "headings") {
			allIssues = append(allIssues, checkHeading(p.title, content)...)
		}
		if isCheckEnabled(checkFilter, "short") {
			allIssues = append(allIssues, checkShort(p.title, content)...)
		}
		if isCheckEnabled(checkFilter, "links") {
			allIssues = append(allIssues, checkDeadLinks(p.title, content, knownTitles)...)
		}
		if isCheckEnabled(checkFilter, "images") {
			allIssues = append(allIssues, checkImages(p.title, content, httpClient)...)
		}
	}

	// Count errors/warnings
	var errCount, warnCount int
	for _, issue := range allIssues {
		if issue.Level == "error" {
			errCount++
		} else {
			warnCount++
		}
	}

	// Print issues to stderr
	for _, issue := range allIssues {
		if issue.Level == "error" {
			fmt.Fprintf(os.Stderr, "  ✗ %s - %s\n", issue.Page, issue.Message)
		} else {
			fmt.Fprintf(os.Stderr, "  ⚠ %s - %s\n", issue.Page, issue.Message)
		}
	}
	fmt.Fprintf(os.Stderr, "\nSummary: %d pages, %d errors, %d warnings\n", len(pages), errCount, warnCount)

	return ctx.OutputData(LintSummary{
		Repository:  ctx.Owner + "/" + ctx.Repo,
		TotalPages:  len(pages),
		TotalIssues: len(allIssues),
		Errors:      errCount,
		Warnings:    warnCount,
		Results:     allIssues,
	})
}

func Shortcuts() []*common.Shortcut {
	return []*common.Shortcut{
		{
			Name:        "list",
			Description: "List all wiki pages",
			Run: func(ctx *common.RuntimeContext) error {
				if err := ctx.ResolveOwnerRepo(); err != nil {
					return err
				}
				projectID, err := resolveProjectID(ctx)
				if err != nil {
					return err
				}
				q := url.Values{}
				q.Set("owner", ctx.Owner)
				q.Set("repo", ctx.Repo)
				q.Set("projectId", projectID)
				env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("wikiPages"), q)
				if err != nil {
					// wiki +list 的 404 几乎总是意味着仓库未在 GitLink 网页端开启 Wiki 功能。
					// 这种情况下不要再提示"用 +list 查看"（循环引用），改为引导用户去开启 Wiki。
					var cliErr *clierrors.CLIError
					if errors.As(err, &cliErr) && cliErr.Kind == clierrors.KindNotFound {
						return clierrors.New(clierrors.KindNotFound,
							fmt.Sprintf("仓库 %s/%s 没有 Wiki 页面", ctx.Owner, ctx.Repo),
							fmt.Sprintf("请前往 GitLink 网页端 → 仓库 %s/%s → 设置 → 开启 Wiki 功能，开启后再创建页面", ctx.Owner, ctx.Repo))
					}
					return fmt.Errorf("获取 Wiki 页面列表失败: %w", err)
				}
				cleanWikiList(env)
				return ctx.Output(env)
			},
		},
		{
			Name:        "view",
			Description: "View a wiki page",
			Flags: []common.Flag{
				{Name: "title", Short: "t", Usage: "Page title", Required: true},
			},
			Run: func(ctx *common.RuntimeContext) error {
				if err := ctx.ResolveOwnerRepo(); err != nil {
					return err
				}
				title, err := ctx.RequireArg("title", `--title "Home Page"`)
				if err != nil {
					return err
				}
				projectID, err := resolveProjectID(ctx)
				if err != nil {
					return err
				}
				env, err := fetchWikiPage(ctx, projectID, title)
				if err != nil {
					// GitLink 后端命名规则: 创建 wiki 时会自动给 sub_url 追加 ".-" 后缀，
					// 而 wiki +list 返回的 title 不带后缀。若 title 不带后缀且首次查询失败，
					// 自动用 title + ".-" 重试一次。
					if !strings.HasSuffix(title, ".-") {
						env, err = fetchWikiPage(ctx, projectID, title+".-")
					}
				}
				if err != nil {
					// 用 CLIError 包装，保留底层错误类型，让 TryPrintError 能按 envelope 输出。
					// suggestion 同时涵盖两种常见根因：(1) 仓库未开启 Wiki；(2) 页面名拼错。
					return clierrors.Wrap(clierrors.KindNotFound,
						fmt.Sprintf("Wiki 页面 %q 不存在", title),
						fmt.Sprintf("请确认：(1) 仓库 %s/%s 已在 GitLink 网页端开启 Wiki 功能；(2) 页面名拼写正确。可用 `gitlink-cli wiki +list --owner %s --repo %s` 查看实际存在的页面",
							ctx.Owner, ctx.Repo, ctx.Owner, ctx.Repo),
						err)
				}
				return outputWithDecodedContent(ctx, env)
			},
		},
		{
			Name:        "create",
			Description: "Create a wiki page",
			DryRun:      true,
			DryRunHint: func(ctx *common.RuntimeContext) (string, error) {
				title := ctx.Arg("title")
				return fmt.Sprintf("Create wiki page: %s", title), nil
			},
			Flags: []common.Flag{
				{Name: "title", Short: "t", Usage: "Page title", Required: true},
				{Name: "content", Short: "c", Usage: "Wiki page content (plain text, will be base64-encoded)"},
				{Name: "file", Short: "f", Usage: "Read content from file"},
				{Name: "message", Short: "m", Usage: "Commit message"},
			},
			Run: func(ctx *common.RuntimeContext) error {
				if err := ctx.ResolveOwnerRepo(); err != nil {
					return err
				}
				title, err := ctx.RequireArg("title", `--title "Home Page"`)
				if err != nil {
					return err
				}
				projectID, err := resolveProjectID(ctx)
				if err != nil {
					return err
				}

				content, err := resolveContent(ctx)
				if err != nil {
					return err
				}

				body := map[string]interface{}{
					"owner":          ctx.Owner,
					"repo":           ctx.Repo,
					"projectId":      parseProjectIDInt(projectID),
					"pageName":       title,
					"title":          title,
					"content_base64": base64.StdEncoding.EncodeToString([]byte(content)),
				}
				if msg := ctx.Arg("message"); msg != "" {
					body["message"] = msg
				}

				env, err := callWikiAPI(ctx, "POST", wikiPath("createWiki"), body)
				if err != nil {
					return fmt.Errorf("创建 Wiki 页面失败: %w", err)
				}
				return outputWithDecodedContent(ctx, env)
			},
		},
		{
			Name:        "update",
			Description: "Update a wiki page",
			DryRun:      true,
			DryRunHint: func(ctx *common.RuntimeContext) (string, error) {
				title := ctx.Arg("title")
				return fmt.Sprintf("Update wiki page: %s", title), nil
			},
			Flags: []common.Flag{
				{Name: "page", Short: "p", Usage: "Current page title to find (defaults to --title)"},
				{Name: "title", Short: "t", Usage: "New page title", Required: true},
				{Name: "cover", Short: "c", Usage: "Replace entire page content with this text"},
				{Name: "add", Short: "a", Usage: "Append text to existing page content"},
				{Name: "file", Short: "f", Usage: "Read content from file (used with --cover or --add)"},
				{Name: "message", Short: "m", Usage: "Commit message"},
			},
			Run: func(ctx *common.RuntimeContext) error {
				if err := ctx.ResolveOwnerRepo(); err != nil {
					return err
				}
				title, err := ctx.RequireArg("title", `--title "Home Page"`)
				if err != nil {
					return err
				}
				pageName := ctx.Arg("page")
				if pageName == "" {
					pageName = title
				}
				projectID, err := resolveProjectID(ctx)
				if err != nil {
					return err
				}

				coverText := ctx.Arg("cover")
				addText := ctx.Arg("add")
				filePath := ctx.Arg("file")
				message := ctx.Arg("message")

				var finalContent string
				if coverText != "" || filePath != "" && coverText == "" && addText == "" {
					// --cover or --file alone: overwrite
					content, err := resolveUpdateContent(ctx, coverText, filePath)
					if err != nil {
						return err
					}
					finalContent = content
				} else if addText != "" {
					// --add: append to existing content
					newPart, err := resolveUpdateContent(ctx, addText, filePath)
					if err != nil {
						return err
					}
					existing, actualPageName, err := fetchPageContent(ctx, projectID, pageName)
					if err != nil {
						return fmt.Errorf("failed to fetch existing page content for append: %w", err)
					}
					// fetchPageContent 可能因 GitLink 后端 ".-" 命名规则触发自动重试，
					// 用实际成功的 pageName（可能带 .- 后缀）作为 PUT 目标，否则后端会再次 404。
					pageName = actualPageName
					finalContent = existing + newPart
				}

				body := map[string]interface{}{
					"owner":     ctx.Owner,
					"repo":      ctx.Repo,
					"projectId": parseProjectIDInt(projectID),
					"pageName":  url.QueryEscape(pageName),
					"title":     title,
					"message":   message,
				}
				if finalContent != "" {
					body["content_base64"] = base64.StdEncoding.EncodeToString([]byte(finalContent))
				}

				env, err := callWikiAPI(ctx, "PUT", wikiPath("updateWiki"), body)
				if err != nil {
					return fmt.Errorf("更新 Wiki 页面失败: %w", err)
				}
				return outputWithDecodedContent(ctx, env)
			},
		},
		{
			Name:        "delete",
			Description: "Delete a wiki page",
			DryRun:      true,
			DryRunHint: func(ctx *common.RuntimeContext) (string, error) {
				title := ctx.Arg("title")
				return fmt.Sprintf("Delete wiki page: %s", title), nil
			},
			Flags: []common.Flag{
				{Name: "title", Short: "t", Usage: "Page title", Required: true},
			},
			Run: func(ctx *common.RuntimeContext) error {
				if err := ctx.ResolveOwnerRepo(); err != nil {
					return err
				}
				title, err := ctx.RequireArg("title", `--title "Home Page"`)
				if err != nil {
					return err
				}
				projectID, err := resolveProjectID(ctx)
				if err != nil {
					return err
				}

				// 1. 解析实际 pageName（fetchPageContent 内部自动重试 ".-" 后缀）
				//    不做这步直接用 title DELETE，会导致后端"接受了请求但没真删"
				//    （后端期望的 pageName 是 "X.-" 而非 "X"）
				_, actualPageName, err := fetchPageContent(ctx, projectID, title)
				if err != nil {
					// 页面查不到 — 后端 getWiki API 返回 404。
					// suggestion 同时涵盖两种常见根因：(1) 仓库未开启 Wiki；(2) 页面名拼错。
					// 注意：GitLink 网页端对任意 ?wiki=xxx 都会渲染 SPA 壳子，
					// 不代表页面真实存在；以 wiki +list 的结果为准。
					return clierrors.New(clierrors.KindNotFound,
						fmt.Sprintf("Wiki 页面 %q 不存在", title),
						fmt.Sprintf("请确认：(1) 仓库 %s/%s 已在 GitLink 网页端开启 Wiki 功能；(2) 页面名拼写正确。可用 `gitlink-cli wiki +list --owner %s --repo %s` 查看实际存在的页面",
							ctx.Owner, ctx.Repo, ctx.Owner, ctx.Repo))
				}

				// 2. 用实际 pageName 调用 DELETE
				body := map[string]interface{}{
					"owner":     ctx.Owner,
					"repo":      ctx.Repo,
					"projectId": parseProjectIDInt(projectID),
					"pageName":  actualPageName,
					"message":   "",
				}
				if _, delErr := callWikiAPI(ctx, "DELETE", wikiPath("deleteWiki"), body); delErr != nil {
					return fmt.Errorf("删除 Wiki 页面失败: %w", delErr)
				}

				// 3. 删除后强制验证（GitLink deleteWiki 端点不可靠：即使返回 200，
				//    页面有时仍然存在）。GET 一次确认页面真的没了。
				q := url.Values{}
				q.Set("owner", ctx.Owner)
				q.Set("repo", ctx.Repo)
				q.Set("projectId", projectID)
				q.Set("pageName", actualPageName)
				if _, viewErr := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q); viewErr == nil {
					// GET 还能查到 — 后端接受了 DELETE 但没真删
					return fmt.Errorf("删除 Wiki 页面失败：后端接受了请求但页面仍存在 (pageName=%s)", actualPageName)
				}
				// 删除成功 — 返回结构化数据，让用户/agent 能自助验证。
				// 说明三种用户常见的"以为没删干净"的现象：
				//   1) 网页端 ?wiki=xxx 仍可访问 → GitLink SPA 占位符（任何参数都渲染壳子）
				//   2) git log 仍能看到删除 commit → git 设计就是保留历史
				//   3) edit 跳转到 wiki=undefined → 网页端前端未正确处理 404
				// 这些都不是 CLI 删除不彻底，是 GitLink 网页端的 UX 问题。
				return ctx.OutputData(map[string]interface{}{
					"message":          "Wiki page deleted successfully",
					"deleted_title":    title,
					"actual_page_name": actualPageName,
					"wiki_repo":        fmt.Sprintf("https://gitlink.org.cn/%s/%s.wiki.git", ctx.Owner, ctx.Repo),
					"notes": []string{
						"页面已从 wiki 仓库 HEAD 彻底删除（git 工作树无残留）",
						"网页端 ?wiki=xxx URL 仍可访问是 SPA 占位符，不代表页面存在",
						"git 历史 commits 仍保留删除记录（git 的正常行为，非残留）",
					},
					"verify_commands": []string{
						fmt.Sprintf("gitlink-cli wiki +list --owner %s --repo %s", ctx.Owner, ctx.Repo),
						fmt.Sprintf("git clone https://gitlink.org.cn/%s/%s.wiki.git /tmp/wiki-check && git -C /tmp/wiki-check ls-files", ctx.Owner, ctx.Repo),
					},
				})
			},
		},
		{
			Name:        "lint",
			Description: "Check wiki pages for quality issues",
			Flags: []common.Flag{
				{Name: "check", Usage: "Specific checks to run (comma-separated): links,headings,images,empty. Default: all"},
			},
			Run: runLint,
		},
	}
}
