// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

// Package v2 Gitea API v2
//
// This is the v2 API with improved error handling, batch operations,
// and AI-friendly endpoints. It uses structured error codes for
// machine-readable error handling.
//
//	Schemes: https, http
//	License: MIT http://opensource.org/licenses/MIT
//
//	Consumes:
//	- application/json
//
//	Produces:
//	- application/json
//	- application/x-ndjson
//
// swagger:meta
package v2

import (
	"net/http"
	"strings"

	auth_model "code.gitea.io/gitea/models/auth"
	access_model "code.gitea.io/gitea/models/perm/access"
	repo_model "code.gitea.io/gitea/models/repo"
	user_model "code.gitea.io/gitea/models/user"
	apierrors "code.gitea.io/gitea/modules/errors"
	"code.gitea.io/gitea/modules/graceful"
	"code.gitea.io/gitea/modules/idempotency"
	"code.gitea.io/gitea/modules/setting"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/util"
	"code.gitea.io/gitea/modules/web"
	"code.gitea.io/gitea/modules/web/middleware"
	"code.gitea.io/gitea/routers/common"
	"code.gitea.io/gitea/services/auth"
	"code.gitea.io/gitea/services/context"

	"github.com/go-chi/cors"
)

// Routes registers all v2 API routes to web application.
func Routes() *web.Router {
	m := web.NewRouter()

	m.Use(middleware.RequestID())
	m.Use(middleware.RateLimitInfo())
	m.Use(securityHeaders())

	// Idempotency middleware for POST/PUT/PATCH requests
	idempotencyMiddleware := idempotency.NewMiddleware(idempotency.GetDefaultStore())
	m.Use(idempotencyMiddleware.Handler)

	if setting.CORSConfig.Enabled {
		m.Use(cors.Handler(cors.Options{
			AllowedOrigins:   setting.CORSConfig.AllowDomain,
			AllowedMethods:   setting.CORSConfig.Methods,
			AllowCredentials: setting.CORSConfig.AllowCredentials,
			AllowedHeaders:   append([]string{"Authorization", "X-Gitea-OTP"}, setting.CORSConfig.Headers...),
			MaxAge:           int(setting.CORSConfig.MaxAge.Seconds()),
		}))
	}

	m.Use(context.APIContexter())

	// Get user from session if logged in
	m.Use(apiAuth(buildAuthGroup()))

	m.Group("", func() {
		// Public endpoints (no auth required)
		m.Get("/version", Version)

		// API Documentation (Scalar)
		m.Get("/docs", DocsScalar)
		m.Get("/swagger.json", SwaggerJSON)

		// Health check endpoints
		m.Group("/health", func() {
			m.Get("", HealthCheck)
			m.Get("/live", LivenessCheck)
			m.Get("/ready", ReadinessCheck)
			m.Get("/component/{component}", ComponentHealthCheck)
		})

		// Operation progress endpoints (SSE)
		m.Group("/operations", func() {
			m.Get("/{id}/progress", OperationProgress)
			m.Get("/{id}", GetOperation)
			m.Delete("/{id}", CancelOperation)
		})

		// Authenticated endpoints
		m.Group("", func() {
			// User info
			m.Get("/user", GetAuthenticatedUser)

			// Batch operations - efficient bulk requests
			m.Group("/batch", func() {
				m.Post("/files", BatchGetFiles)
				m.Post("/repos", BatchGetRepos)
			})

			// Streaming endpoints - NDJSON responses
			m.Group("/stream", func() {
				m.Post("/files", StreamFiles)
				m.Post("/commits", StreamCommits)
				m.Post("/issues", StreamIssues)
			})

			// AI context endpoints - rich context for AI tools
			m.Group("/ai", func() {
				m.Post("/repo/summary", GetAIRepoSummary)
				m.Post("/repo/navigation", GetAINavigation)
				m.Post("/issue/context", GetAIIssueContext)
			})
		}, reqToken())

		// Wiki v2 API - repository wiki endpoints
		m.Group("/repos/{owner}/{repo}/wiki", func() {
			// Public read endpoints (access checked in handler)
			m.Get("/pages", ListWikiPagesV2)
			m.Get("/pages/{pageName}", GetWikiPageV2)
			m.Get("/pages/{pageName}/revisions", GetWikiPageRevisionsV2)
			m.Get("/search", SearchWikiV2)
			m.Get("/graph", GetWikiGraphV2)
			m.Get("/stats", GetWikiStatsV2)

			// Write endpoints require authentication
			m.Group("", func() {
				m.Post("/pages", web.Bind(api.CreateWikiPageV2Option{}), CreateWikiPageV2)
				m.Put("/pages/{pageName}", web.Bind(api.UpdateWikiPageV2Option{}), UpdateWikiPageV2)
				m.Delete("/pages/{pageName}", DeleteWikiPageV2)
			}, reqToken())
		})

		// Actions v2 API - AI-friendly runner capability discovery
		m.Group("/repos/{owner}/{repo}/actions", func() {
			m.Get("/runners/capabilities", repoAssignment(), GetActionsCapabilities)
			m.Post("/workflows/validate", repoAssignment(), reqToken(), web.Bind(api.WorkflowValidationRequest{}), ValidateWorkflow)
		})

		// Releases v2 API - Enhanced releases with app update support
		// Supports public access for private repos with public_releases enabled
		m.Group("/repos/{owner}/{repo}/releases", func() {
			// App update endpoint - Electron/Squirrel compatible
			// Returns 200 with update info or 204 if no update available
			m.Get("/update", repoAssignmentWithPublicAccess(), CheckAppUpdate)

			// List and get releases
			m.Get("", repoAssignmentWithPublicAccess(), ListReleasesV2)
			m.Get("/latest", repoAssignmentWithPublicAccess(), GetLatestReleaseV2)
			m.Get("/{tag}", repoAssignmentWithPublicAccess(), GetReleaseV2)
		})

		// Public landing page API - for private repos with public_landing enabled
		m.Group("/repos/{owner}/{repo}/pages", func() {
			m.Get("/config", repoAssignmentWithPublicAccess(), GetPagesConfig)
			m.Get("/content", repoAssignmentWithPublicAccess(), GetPagesContent)
		})
	})

	return m
}

func securityHeaders() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			// CORS preflight
			if req.Method == http.MethodOptions {
				return
			}
			next.ServeHTTP(w, req)
		})
	}
}

func buildAuthGroup() *auth.Group {
	group := auth.NewGroup(
		&auth.OAuth2{},
		&auth.HTTPSign{},
		&auth.Basic{},
	)
	if setting.Service.EnableReverseProxyAuthAPI {
		group.Add(&auth.ReverseProxy{})
	}
	if setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext()) {
		group.Add(&auth.SSPI{})
	}
	return group
}

func apiAuth(authMethod auth.Method) func(*context.APIContext) {
	return func(ctx *context.APIContext) {
		ar, err := common.AuthShared(ctx.Base, nil, authMethod)
		if err != nil {
			msg, ok := auth.ErrAsUserAuthMessage(err)
			msg = util.Iif(ok, msg, "invalid username, password or token")
			ctx.APIErrorWithCodeAndMessage(apierrors.AuthInvalidCredentials, msg)
			return
		}
		ctx.Doer = ar.Doer
		ctx.IsSigned = ar.Doer != nil
		ctx.IsBasicAuth = ar.IsBasicAuth
	}
}

// reqToken requires authentication
func reqToken() func(ctx *context.APIContext) {
	return func(ctx *context.APIContext) {
		if !ctx.IsSigned {
			ctx.APIErrorWithCode(apierrors.AuthTokenMissing)
			return
		}
	}
}

// repoAssignment loads the repository from path parameters
func repoAssignment() func(ctx *context.APIContext) {
	return func(ctx *context.APIContext) {
		ownerName := ctx.PathParam("owner")
		repoName := ctx.PathParam("repo")

		var (
			owner *user_model.User
			err   error
		)

		// Check if the user is the same as the repository owner
		if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, ownerName) {
			owner = ctx.Doer
		} else {
			owner, err = user_model.GetUserByName(ctx, ownerName)
			if err != nil {
				if user_model.IsErrUserNotExist(err) {
					ctx.APIErrorNotFound("GetUserByName", err)
				} else {
					ctx.APIErrorInternal(err)
				}
				return
			}
		}
		ctx.Repo.Owner = owner
		ctx.ContextUser = owner

		// Get repository
		repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName)
		if err != nil {
			if repo_model.IsErrRepoNotExist(err) {
				ctx.APIErrorNotFound("GetRepositoryByName", err)
			} else {
				ctx.APIErrorInternal(err)
			}
			return
		}

		repo.Owner = owner
		ctx.Repo.Repository = repo

		// Get permissions
		ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
		if err != nil {
			ctx.APIErrorInternal(err)
			return
		}

		if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() {
			ctx.APIErrorNotFound("HasAnyUnitAccessOrPublicAccess")
			return
		}
	}
}
