// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT

package v2

import (
	"net/http"
	"strings"
	"time"

	repo_model "code.gitea.io/gitea/models/repo"
	"code.gitea.io/gitea/modules/charset"
	apierrors "code.gitea.io/gitea/modules/errors"
	"code.gitea.io/gitea/modules/git"
	"code.gitea.io/gitea/modules/gitrepo"
	"code.gitea.io/gitea/modules/json"
	"code.gitea.io/gitea/modules/markup"
	"code.gitea.io/gitea/modules/markup/markdown"
	"code.gitea.io/gitea/modules/setting"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/web"
	"code.gitea.io/gitea/services/context"
	notify_service "code.gitea.io/gitea/services/notify"
	wiki_service "code.gitea.io/gitea/services/wiki"
)

// WikiV2 handles all v2 wiki API endpoints

// ListWikiPagesV2 lists all wiki pages with enhanced metadata
func ListWikiPagesV2(ctx *context.APIContext) {
	owner := ctx.PathParam("owner")
	repoName := ctx.PathParam("repo")

	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
	if err != nil {
		if repo_model.IsErrRepoNotExist(err) {
			ctx.APIErrorWithCode(apierrors.RepoNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Check access
	if repo.IsPrivate && !ctx.IsSigned {
		ctx.APIErrorWithCode(apierrors.PermAccessDenied)
		return
	}

	// Open wiki repo
	wikiRepo, commit, err := findWikiRepoCommitV2(ctx, repo)
	if err != nil {
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}
	if wikiRepo == nil {
		ctx.JSON(http.StatusOK, &api.WikiPageListV2{
			Pages:      []*api.WikiPageV2{},
			TotalCount: 0,
			HasMore:    false,
		})
		return
	}
	defer wikiRepo.Close()

	if commit == nil {
		ctx.JSON(http.StatusOK, &api.WikiPageListV2{
			Pages:      []*api.WikiPageV2{},
			TotalCount: 0,
			HasMore:    false,
		})
		return
	}

	// Pagination
	page := max(ctx.FormInt("page"), 1)
	limit := ctx.FormInt("limit")
	if limit <= 0 {
		limit = setting.API.DefaultPagingNum
	}
	if limit > 100 {
		limit = 100
	}

	entries, err := commit.ListEntries()
	if err != nil {
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Filter to .md files
	var mdEntries []*git.TreeEntry
	for _, entry := range entries {
		if entry.IsRegular() && strings.HasSuffix(entry.Name(), ".md") {
			mdEntries = append(mdEntries, entry)
		}
	}

	totalCount := int64(len(mdEntries))
	skip := (page - 1) * limit
	end := min(skip+limit, len(mdEntries))

	pages := make([]*api.WikiPageV2, 0, limit)
	for i := skip; i < end; i++ {
		entry := mdEntries[i]
		wikiName, err := wiki_service.GitPathToWebPath(entry.Name())
		if err != nil {
			continue
		}

		// Get last commit for this page
		lastCommit, _ := wikiRepo.GetCommitByPath(entry.Name())

		_, pageTitle := wiki_service.WebPathToUserTitle(wikiName)
		page := &api.WikiPageV2{
			Name:    string(wikiName),
			Title:   pageTitle,
			Path:    entry.Name(),
			URL:     setting.AppURL + repo.FullName() + "/wiki/" + string(wikiName),
			HTMLURL: setting.AppURL + repo.FullName() + "/wiki/" + string(wikiName),
		}

		// Get word count from index if available
		if idx, _ := repo_model.GetWikiIndex(ctx, repo.ID, string(wikiName)); idx != nil {
			page.WordCount = idx.WordCount
		}

		if lastCommit != nil {
			page.LastCommit = &api.WikiCommitV2{
				SHA:     lastCommit.ID.String(),
				Message: strings.Split(lastCommit.CommitMessage, "\n")[0],
				Date:    lastCommit.Author.When,
				Author: &api.WikiAuthorV2{
					Name:  lastCommit.Author.Name,
					Email: lastCommit.Author.Email,
				},
			}
		}

		pages = append(pages, page)
	}

	ctx.JSON(http.StatusOK, &api.WikiPageListV2{
		Pages:      pages,
		TotalCount: totalCount,
		HasMore:    int64(end) < totalCount,
	})
}

// GetWikiPageV2 gets a single wiki page with full content and metadata
func GetWikiPageV2(ctx *context.APIContext) {
	owner := ctx.PathParam("owner")
	repoName := ctx.PathParam("repo")
	pageName := ctx.PathParam("pageName")

	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
	if err != nil {
		if repo_model.IsErrRepoNotExist(err) {
			ctx.APIErrorWithCode(apierrors.RepoNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Check access
	if repo.IsPrivate && !ctx.IsSigned {
		ctx.APIErrorWithCode(apierrors.PermAccessDenied)
		return
	}

	wikiName := wiki_service.WebPathFromRequest(pageName)

	wikiRepo, commit, err := findWikiRepoCommitV2(ctx, repo)
	if err != nil {
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}
	if wikiRepo == nil || commit == nil {
		ctx.APIErrorWithCode(apierrors.WikiPageNotFound)
		return
	}
	defer wikiRepo.Close()

	// Get page content
	gitFilename := wiki_service.WebPathToGitPath(wikiName)
	entry, err := commit.GetTreeEntryByPath(gitFilename)
	if err != nil {
		if git.IsErrNotExist(err) {
			ctx.APIErrorWithCode(apierrors.WikiPageNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	blob := entry.Blob()
	content, err := blob.GetBlobContent(1024 * 1024) // 1MB max
	if err != nil {
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Render HTML
	var htmlContent string
	rd := charset.ToUTF8WithFallbackReader(strings.NewReader(content), charset.ConvertOpts{})
	buf := new(strings.Builder)
	if err := markdown.Render(markup.NewRenderContext(ctx).WithRelativePath(gitFilename), rd, buf); err == nil {
		htmlContent = buf.String()
	}

	// Get last commit
	lastCommit, _ := wikiRepo.GetCommitByPath(gitFilename)

	// Get commit count
	commitsCount, _ := gitrepo.FileCommitsCount(ctx, repo.WikiStorageRepo(), repo.DefaultWikiBranch, gitFilename)

	// Get sidebar and footer
	sidebarContent, _ := getWikiContentByName(commit, "_Sidebar")
	footerContent, _ := getWikiContentByName(commit, "_Footer")

	// Get links from index
	var linksOut, linksIn []string
	if idx, _ := repo_model.GetWikiIndex(ctx, repo.ID, string(wikiName)); idx != nil {
		if idx.LinksOut != "" {
			_ = json.Unmarshal([]byte(idx.LinksOut), &linksOut)
		}
	}

	// Get incoming links
	if incoming, _ := wiki_service.GetWikiIncomingLinks(ctx, repo.ID, string(wikiName)); incoming != nil {
		linksIn = incoming
	}

	_, pageTitle := wiki_service.WebPathToUserTitle(wikiName)
	page := &api.WikiPageV2{
		Name:        string(wikiName),
		Title:       pageTitle,
		Path:        gitFilename,
		URL:         setting.AppURL + "api/v2/repos/" + repo.FullName() + "/wiki/pages/" + string(wikiName),
		HTMLURL:     setting.AppURL + repo.FullName() + "/wiki/" + string(wikiName),
		Content:     content,
		ContentHTML: htmlContent,
		WordCount:   len(strings.Fields(content)),
		LinksOut:    linksOut,
		LinksIn:     linksIn,
		Sidebar:     sidebarContent,
		Footer:      footerContent,
		HistoryURL:  setting.AppURL + "api/v2/repos/" + repo.FullName() + "/wiki/pages/" + string(wikiName) + "/revisions",
	}

	if lastCommit != nil {
		page.LastCommit = &api.WikiCommitV2{
			SHA:     lastCommit.ID.String(),
			Message: strings.Split(lastCommit.CommitMessage, "\n")[0],
			Date:    lastCommit.Author.When,
			Author: &api.WikiAuthorV2{
				Name:  lastCommit.Author.Name,
				Email: lastCommit.Author.Email,
			},
		}
		if lastCommit.Committer != nil {
			page.LastCommit.Committer = &api.WikiAuthorV2{
				Name:  lastCommit.Committer.Name,
				Email: lastCommit.Committer.Email,
			}
		}
	}

	// Update index in background
	go func() {
		_ = wiki_service.IndexWikiPage(ctx, repo, string(wikiName))
	}()

	ctx.SetTotalCountHeader(commitsCount)
	ctx.JSON(http.StatusOK, page)
}

// CreateWikiPageV2 creates a new wiki page
func CreateWikiPageV2(ctx *context.APIContext) {
	owner := ctx.PathParam("owner")
	repoName := ctx.PathParam("repo")

	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
	if err != nil {
		if repo_model.IsErrRepoNotExist(err) {
			ctx.APIErrorWithCode(apierrors.RepoNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Check write access
	if !ctx.IsSigned {
		ctx.APIErrorWithCode(apierrors.AuthTokenMissing)
		return
	}

	// Check if archived
	if repo.IsArchived {
		ctx.APIErrorWithCode(apierrors.RepoArchived)
		return
	}

	form := web.GetForm(ctx).(*api.CreateWikiPageV2Option)
	if form.Name == "" {
		ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
			"field": "name",
			"error": "name is required",
		})
		return
	}

	wikiName := wiki_service.UserTitleToWebPath("", form.Name)
	message := form.Message
	if message == "" {
		message = "Add page: " + form.Name
	}

	if err := wiki_service.AddWikiPage(ctx, ctx.Doer, repo, wikiName, form.Content, message); err != nil {
		if repo_model.IsErrWikiReservedName(err) {
			ctx.APIErrorWithCode(apierrors.WikiReservedName)
			return
		}
		if repo_model.IsErrWikiAlreadyExist(err) {
			ctx.APIErrorWithCode(apierrors.WikiPageAlreadyExists)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Index the new page
	_ = wiki_service.IndexWikiPage(ctx, repo, string(wikiName))

	notify_service.NewWikiPage(ctx, ctx.Doer, repo, string(wikiName), message)

	// Return the created page
	ctx.Redirect(setting.AppURL + "api/v2/repos/" + repo.FullName() + "/wiki/pages/" + string(wikiName))
}

// UpdateWikiPageV2 updates an existing wiki page
func UpdateWikiPageV2(ctx *context.APIContext) {
	owner := ctx.PathParam("owner")
	repoName := ctx.PathParam("repo")
	pageName := ctx.PathParam("pageName")

	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
	if err != nil {
		if repo_model.IsErrRepoNotExist(err) {
			ctx.APIErrorWithCode(apierrors.RepoNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Check write access
	if !ctx.IsSigned {
		ctx.APIErrorWithCode(apierrors.AuthTokenMissing)
		return
	}

	// Check if archived
	if repo.IsArchived {
		ctx.APIErrorWithCode(apierrors.RepoArchived)
		return
	}

	form := web.GetForm(ctx).(*api.UpdateWikiPageV2Option)
	oldWikiName := wiki_service.WebPathFromRequest(pageName)
	newWikiName := oldWikiName

	if form.RenameTo != "" {
		newWikiName = wiki_service.UserTitleToWebPath("", form.RenameTo)
	}

	message := form.Message
	if message == "" {
		if oldWikiName != newWikiName {
			message = "Rename page: " + string(oldWikiName) + " to " + string(newWikiName)
		} else {
			message = "Update page: " + string(oldWikiName)
		}
	}

	if err := wiki_service.EditWikiPage(ctx, ctx.Doer, repo, oldWikiName, newWikiName, form.Content, message); err != nil {
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Update index
	if oldWikiName != newWikiName {
		_ = wiki_service.RemoveWikiPageFromIndex(ctx, repo.ID, string(oldWikiName))
	}
	_ = wiki_service.IndexWikiPage(ctx, repo, string(newWikiName))

	notify_service.EditWikiPage(ctx, ctx.Doer, repo, string(newWikiName), message)

	// Return the updated page
	ctx.Redirect(setting.AppURL + "api/v2/repos/" + repo.FullName() + "/wiki/pages/" + string(newWikiName))
}

// DeleteWikiPageV2 deletes a wiki page
func DeleteWikiPageV2(ctx *context.APIContext) {
	owner := ctx.PathParam("owner")
	repoName := ctx.PathParam("repo")
	pageName := ctx.PathParam("pageName")

	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
	if err != nil {
		if repo_model.IsErrRepoNotExist(err) {
			ctx.APIErrorWithCode(apierrors.RepoNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Check write access
	if !ctx.IsSigned {
		ctx.APIErrorWithCode(apierrors.AuthTokenMissing)
		return
	}

	// Check if archived
	if repo.IsArchived {
		ctx.APIErrorWithCode(apierrors.RepoArchived)
		return
	}

	wikiName := wiki_service.WebPathFromRequest(pageName)

	if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, repo, wikiName); err != nil {
		if strings.Contains(err.Error(), "not exist") {
			ctx.APIErrorWithCode(apierrors.WikiPageNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Remove from index
	_ = wiki_service.RemoveWikiPageFromIndex(ctx, repo.ID, string(wikiName))

	notify_service.DeleteWikiPage(ctx, ctx.Doer, repo, string(wikiName))

	ctx.JSON(http.StatusOK, &api.WikiDeleteResponseV2{Success: true})
}

// SearchWikiV2 searches wiki pages
func SearchWikiV2(ctx *context.APIContext) {
	owner := ctx.PathParam("owner")
	repoName := ctx.PathParam("repo")
	query := ctx.FormString("q")

	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
	if err != nil {
		if repo_model.IsErrRepoNotExist(err) {
			ctx.APIErrorWithCode(apierrors.RepoNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Check access
	if repo.IsPrivate && !ctx.IsSigned {
		ctx.APIErrorWithCode(apierrors.PermAccessDenied)
		return
	}

	// Ensure wiki is indexed
	go func() {
		_ = wiki_service.IndexAllWikiPages(ctx, repo)
	}()

	limit := ctx.FormInt("limit")
	if limit <= 0 {
		limit = 20
	}
	if limit > 100 {
		limit = 100
	}

	opts := &repo_model.SearchWikiOptions{
		RepoID: repo.ID,
		Query:  query,
		Limit:  limit,
		Offset: ctx.FormInt("offset"),
	}

	results, total, err := repo_model.SearchWikiPages(ctx, opts)
	if err != nil {
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	searchResults := make([]*api.WikiSearchResultV2, 0, len(results))
	for _, idx := range results {
		// Create snippet
		snippet := createSearchSnippet(idx.Content, query, 200)

		searchResults = append(searchResults, &api.WikiSearchResultV2{
			Name:        idx.PageName,
			Title:       idx.Title,
			Snippet:     snippet,
			Score:       calculateSearchScore(idx, query),
			WordCount:   idx.WordCount,
			LastUpdated: idx.UpdatedUnix.AsTime(),
		})
	}

	ctx.JSON(http.StatusOK, &api.WikiSearchResponseV2{
		Query:      query,
		Results:    searchResults,
		TotalCount: total,
	})
}

// GetWikiGraphV2 returns the wiki link graph
func GetWikiGraphV2(ctx *context.APIContext) {
	owner := ctx.PathParam("owner")
	repoName := ctx.PathParam("repo")

	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
	if err != nil {
		if repo_model.IsErrRepoNotExist(err) {
			ctx.APIErrorWithCode(apierrors.RepoNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Check access
	if repo.IsPrivate && !ctx.IsSigned {
		ctx.APIErrorWithCode(apierrors.PermAccessDenied)
		return
	}

	// Ensure wiki is indexed
	_ = wiki_service.IndexAllWikiPages(ctx, repo)

	nodes, edges, err := wiki_service.GetWikiGraph(ctx, repo.ID)
	if err != nil {
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Convert to API types
	apiNodes := make([]*api.WikiGraphNodeV2, 0, len(nodes))
	for _, n := range nodes {
		apiNodes = append(apiNodes, &api.WikiGraphNodeV2{
			Name:      n["name"].(string),
			Title:     n["title"].(string),
			WordCount: n["word_count"].(int),
		})
	}

	apiEdges := make([]*api.WikiGraphEdgeV2, 0, len(edges))
	for _, e := range edges {
		apiEdges = append(apiEdges, &api.WikiGraphEdgeV2{
			Source: e["source"].(string),
			Target: e["target"].(string),
		})
	}

	ctx.JSON(http.StatusOK, &api.WikiGraphV2{
		Nodes: apiNodes,
		Edges: apiEdges,
	})
}

// GetWikiStatsV2 returns wiki statistics
func GetWikiStatsV2(ctx *context.APIContext) {
	owner := ctx.PathParam("owner")
	repoName := ctx.PathParam("repo")

	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
	if err != nil {
		if repo_model.IsErrRepoNotExist(err) {
			ctx.APIErrorWithCode(apierrors.RepoNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Check access
	if repo.IsPrivate && !ctx.IsSigned {
		ctx.APIErrorWithCode(apierrors.PermAccessDenied)
		return
	}

	// Ensure wiki is indexed
	_ = wiki_service.IndexAllWikiPages(ctx, repo)

	// Get basic stats
	totalPages, _ := repo_model.GetWikiIndexCount(ctx, repo.ID)
	totalWords, _ := repo_model.GetWikiTotalWordCount(ctx, repo.ID)

	// Get health info
	orphaned, _ := wiki_service.GetOrphanedPages(ctx, repo.ID)
	deadLinks, _ := wiki_service.GetDeadLinks(ctx, repo.ID)

	// Get indexes for top linked calculation
	indexes, _ := repo_model.GetWikiIndexByRepo(ctx, repo.ID)

	// Calculate incoming links for each page
	linkCounts := make(map[string]int)
	for _, idx := range indexes {
		var links []string
		if idx.LinksOut != "" {
			_ = json.Unmarshal([]byte(idx.LinksOut), &links)
		}
		for _, link := range links {
			linkCounts[link]++
		}
	}

	// Find top linked pages
	topLinked := make([]*api.WikiTopLinkedPageV2, 0)
	for name, count := range linkCounts {
		if count > 1 { // Only include pages with multiple links
			topLinked = append(topLinked, &api.WikiTopLinkedPageV2{
				Name:          name,
				IncomingLinks: count,
			})
		}
	}

	// Sort by incoming links (simple bubble sort for small lists)
	for i := 0; i < len(topLinked); i++ {
		for j := i + 1; j < len(topLinked); j++ {
			if topLinked[j].IncomingLinks > topLinked[i].IncomingLinks {
				topLinked[i], topLinked[j] = topLinked[j], topLinked[i]
			}
		}
	}
	if len(topLinked) > 10 {
		topLinked = topLinked[:10]
	}

	// Build health info
	health := &api.WikiHealthV2{
		OrphanedPages: make([]*api.WikiOrphanedPageV2, 0),
		DeadLinks:     make([]*api.WikiDeadLinkV2, 0),
		OutdatedPages: make([]*api.WikiOutdatedPageV2, 0),
		ShortPages:    make([]*api.WikiShortPageV2, 0),
	}

	for _, o := range orphaned {
		health.OrphanedPages = append(health.OrphanedPages, &api.WikiOrphanedPageV2{
			Name:      o.PageName,
			WordCount: o.WordCount,
		})
	}

	for _, d := range deadLinks {
		health.DeadLinks = append(health.DeadLinks, &api.WikiDeadLinkV2{
			Page:       d["page"],
			BrokenLink: d["broken_link"],
		})
	}

	// Find short pages (< 100 words)
	for _, idx := range indexes {
		if idx.WordCount < 100 && idx.PageName != "_Sidebar" && idx.PageName != "_Footer" {
			health.ShortPages = append(health.ShortPages, &api.WikiShortPageV2{
				Name:      idx.PageName,
				WordCount: idx.WordCount,
			})
		}
	}

	// Find outdated pages (> 180 days old)
	sixMonthsAgo := time.Now().AddDate(0, -6, 0)
	for _, idx := range indexes {
		lastEdit := idx.UpdatedUnix.AsTime()
		if lastEdit.Before(sixMonthsAgo) {
			daysOld := int(time.Since(lastEdit).Hours() / 24)
			health.OutdatedPages = append(health.OutdatedPages, &api.WikiOutdatedPageV2{
				Name:     idx.PageName,
				LastEdit: lastEdit,
				DaysOld:  daysOld,
			})
		}
	}

	// Get last updated time
	var lastUpdated time.Time
	for _, idx := range indexes {
		if idx.UpdatedUnix.AsTime().After(lastUpdated) {
			lastUpdated = idx.UpdatedUnix.AsTime()
		}
	}

	// Get commit count and contributors from git
	var totalCommits int64
	var contributors int64
	wikiRepo, _, err := findWikiRepoCommitV2(ctx, repo)
	if err == nil && wikiRepo != nil {
		defer wikiRepo.Close()
		// Get commit count
		if count, err := gitrepo.CommitsCount(ctx, repo.WikiStorageRepo(), gitrepo.CommitsCountOptions{
			Revision: []string{repo.DefaultWikiBranch},
		}); err == nil {
			totalCommits = count
		}
		// Contributors - simplified as unique committers
		contributors = int64(len(linkCounts)) // Approximate
	}

	ctx.JSON(http.StatusOK, &api.WikiStatsV2{
		TotalPages:   totalPages,
		TotalWords:   totalWords,
		TotalCommits: totalCommits,
		LastUpdated:  lastUpdated,
		Contributors: contributors,
		Health:       health,
		TopLinked:    topLinked,
	})
}

// GetWikiPageRevisionsV2 returns revision history for a wiki page
func GetWikiPageRevisionsV2(ctx *context.APIContext) {
	owner := ctx.PathParam("owner")
	repoName := ctx.PathParam("repo")
	pageName := ctx.PathParam("pageName")

	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
	if err != nil {
		if repo_model.IsErrRepoNotExist(err) {
			ctx.APIErrorWithCode(apierrors.RepoNotFound)
			return
		}
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Check access
	if repo.IsPrivate && !ctx.IsSigned {
		ctx.APIErrorWithCode(apierrors.PermAccessDenied)
		return
	}

	wikiName := wiki_service.WebPathFromRequest(pageName)
	gitFilename := wiki_service.WebPathToGitPath(wikiName)

	wikiRepo, commit, err := findWikiRepoCommitV2(ctx, repo)
	if err != nil || wikiRepo == nil || commit == nil {
		ctx.APIErrorWithCode(apierrors.WikiPageNotFound)
		return
	}
	defer wikiRepo.Close()

	// Check if page exists
	if _, err := commit.GetTreeEntryByPath(gitFilename); err != nil {
		ctx.APIErrorWithCode(apierrors.WikiPageNotFound)
		return
	}

	// Get commits for this file
	page := max(ctx.FormInt("page"), 1)
	commits, err := wikiRepo.CommitsByFileAndRange(git.CommitsByFileAndRangeOptions{
		Revision: repo.DefaultWikiBranch,
		File:     gitFilename,
		Page:     page,
	})
	if err != nil {
		ctx.APIErrorWithCode(apierrors.InternalError)
		return
	}

	// Get total count
	totalCount, _ := gitrepo.FileCommitsCount(ctx, repo.WikiStorageRepo(), repo.DefaultWikiBranch, gitFilename)

	revisions := make([]*api.WikiRevisionV2, 0, len(commits))
	for _, c := range commits {
		rev := &api.WikiRevisionV2{
			SHA:     c.ID.String(),
			Message: strings.Split(c.CommitMessage, "\n")[0],
			Date:    c.Author.When,
			Author: &api.WikiAuthorV2{
				Name:  c.Author.Name,
				Email: c.Author.Email,
			},
		}
		revisions = append(revisions, rev)
	}

	ctx.JSON(http.StatusOK, &api.WikiRevisionsV2{
		PageName:   string(wikiName),
		Revisions:  revisions,
		TotalCount: totalCount,
	})
}

// Helper functions

func findWikiRepoCommitV2(ctx *context.APIContext, repo *repo_model.Repository) (*git.Repository, *git.Commit, error) {
	wikiRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
	if err != nil {
		if git.IsErrNotExist(err) || strings.Contains(err.Error(), "no such file or directory") {
			return nil, nil, nil
		}
		return nil, nil, err
	}

	branch := repo.DefaultWikiBranch
	if branch == "" {
		branch = "master"
	}

	commit, err := wikiRepo.GetBranchCommit(branch)
	if err != nil {
		wikiRepo.Close()
		if git.IsErrNotExist(err) {
			return nil, nil, nil
		}
		return nil, nil, err
	}

	return wikiRepo, commit, nil
}

func getWikiContentByName(commit *git.Commit, name string) (string, error) {
	wikiPath := wiki_service.WebPathToGitPath(wiki_service.WebPath(name))
	entry, err := commit.GetTreeEntryByPath(wikiPath)
	if err != nil {
		return "", err
	}
	blob := entry.Blob()
	content, err := blob.GetBlobContent(1024 * 1024)
	if err != nil {
		return "", err
	}
	return content, nil
}

func createSearchSnippet(content, query string, maxLen int) string {
	lowerContent := strings.ToLower(content)
	lowerQuery := strings.ToLower(query)

	idx := strings.Index(lowerContent, lowerQuery)
	if idx == -1 {
		// Query not found, return beginning of content
		if len(content) > maxLen {
			return content[:maxLen] + "..."
		}
		return content
	}

	// Find start position
	start := max(idx-maxLen/4, 0)

	// Find end position
	end := min(start+maxLen, len(content))

	snippet := content[start:end]
	if start > 0 {
		snippet = "..." + snippet
	}
	if end < len(content) {
		snippet += "..."
	}

	return snippet
}

func calculateSearchScore(idx *repo_model.WikiIndex, query string) float32 {
	score := float32(0.0)
	lowerQuery := strings.ToLower(query)

	// Title match is highest priority
	if strings.Contains(strings.ToLower(idx.Title), lowerQuery) {
		score += 10.0
	}

	// Page name match
	if strings.Contains(strings.ToLower(idx.PageName), lowerQuery) {
		score += 8.0
	}

	// Content match - count occurrences
	lowerContent := strings.ToLower(idx.Content)
	count := strings.Count(lowerContent, lowerQuery)
	score += float32(count) * 0.5

	// Longer pages might have more matches but aren't necessarily more relevant
	// Normalize by word count
	if idx.WordCount > 0 {
		score /= float32(idx.WordCount) / 100.0
	}

	return score
}
