package wiki

import (
	"encoding/base64"
	"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"
	"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:
			if v != 200 && v != 201 {
				msg, _ := resp["msg"].(string)
				return nil, fmt.Errorf("[%.0f] %s", v, msg)
			}
		}
	}
	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")
}

func fetchPageContent(ctx *common.RuntimeContext, projectID, pageName 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 "", fmt.Errorf("获取 Wiki 页面现有内容失败: %w", 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 "", nil
	}
	decoded, err := base64.StdEncoding.DecodeString(b64)
	if err != nil {
		return "", fmt.Errorf("failed to decode page content: %w", err)
	}
	return string(decoded), 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
			}
		}
	}
}

func outputWithDecodedContent(ctx *common.RuntimeContext, env *output.Envelope) error {
	data, ok := env.Data.(map[string]interface{})
	if !ok {
		return ctx.Output(env)
	}
	if b64, ok := data["content_base64"].(string); ok && b64 != "" {
		decoded, err := base64.StdEncoding.DecodeString(b64)
		if err == nil {
			data["content_decoded"] = 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 {
					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 {
					return fmt.Errorf("查看 Wiki 页面失败: %w", 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 ctx.Output(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, err := fetchPageContent(ctx, projectID, pageName)
					if err != nil {
						return fmt.Errorf("failed to fetch existing page content for append: %w", err)
					}
					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 ctx.Output(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
				}

				body := map[string]interface{}{
					"owner":     ctx.Owner,
					"repo":      ctx.Repo,
					"projectId": parseProjectIDInt(projectID),
					"pageName":  title,
					"message":   "",
				}

				_, delErr := callWikiAPI(ctx, "DELETE", wikiPath("deleteWiki"), body)
				if delErr != nil {
					q := url.Values{}
					q.Set("owner", ctx.Owner)
					q.Set("repo", ctx.Repo)
					q.Set("projectId", projectID)
					q.Set("pageName", title)
					_, viewErr := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q)
					if viewErr != nil {
						return ctx.OutputData(map[string]string{
							"message": "Wiki page deleted successfully",
						})
					}
					return fmt.Errorf("删除 Wiki 页面失败: %w", delErr)
				}
				return ctx.OutputData(map[string]string{
					"message": "Wiki page deleted successfully",
				})
			},
		},
		{
			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,
		},
	}
}
