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

package v2

import (
	"fmt"
	"net/http"
	"runtime"
	"strings"

	"code.gitea.io/gitea/models/db"
	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/optional"
	"code.gitea.io/gitea/modules/setting"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/services/context"
	pages_service "code.gitea.io/gitea/services/pages"

	"github.com/Masterminds/semver/v3"
)

// AppUpdateResponse represents the response for an app update check
// Compatible with Electron autoUpdater (Squirrel format)
type AppUpdateResponse struct {
	// URL to download the update
	URL string `json:"url"`
	// Version name (semver)
	Name string `json:"name"`
	// Release notes (markdown)
	Notes string `json:"notes"`
	// Publication date (RFC3339)
	PubDate string `json:"pub_date"`
	// Whether this is a mandatory/priority update
	Mandatory bool `json:"mandatory,omitempty"`
	// Additional platform-specific info
	Platform *PlatformInfo `json:"platform,omitempty"`
}

// PlatformInfo contains platform-specific update information
type PlatformInfo struct {
	// For Windows: URL to RELEASES file
	ReleasesURL string `json:"releases_url,omitempty"`
	// For Windows: URL to nupkg file
	NupkgURL string `json:"nupkg_url,omitempty"`
	// Signature/checksum for verification
	Signature string `json:"signature,omitempty"`
	// File size in bytes
	Size int64 `json:"size,omitempty"`
}

// CheckAppUpdate checks if an update is available for an app
// This endpoint is designed for Electron apps using autoUpdater
// GET /api/v2/repos/{owner}/{repo}/releases/update?version=1.0.0&platform=darwin&arch=arm64
func CheckAppUpdate(ctx *context.APIContext) {
	repo := ctx.Repo.Repository
	if repo == nil {
		ctx.APIErrorNotFound("Repository not found")
		return
	}

	// Get query parameters
	currentVersion := ctx.FormString("version")
	platform := ctx.FormString("platform")
	arch := ctx.FormString("arch")
	channel := ctx.FormString("channel")

	// Default to current runtime if not specified
	if platform == "" {
		platform = runtime.GOOS
	}
	if arch == "" {
		arch = runtime.GOARCH
		if arch == "amd64" {
			arch = "x64"
		}
	}
	if channel == "" {
		channel = "stable"
	}

	// Parse current version
	current, err := semver.NewVersion(strings.TrimPrefix(currentVersion, "v"))
	if err != nil {
		ctx.APIErrorWithCodeAndMessage(apierrors.ValInvalidInput, "Invalid version format: "+currentVersion)
		return
	}

	// Build find options
	opts := repo_model.FindReleasesOptions{
		ListOptions:   db.ListOptions{PageSize: 50},
		RepoID:        repo.ID,
		IncludeDrafts: false,
		IncludeTags:   false,
	}
	if channel == "stable" {
		opts.IsPreRelease = optional.Some(false)
	}

	// Get releases
	releases, err := db.Find[repo_model.Release](ctx, opts)
	if err != nil {
		ctx.APIErrorInternal(err)
		return
	}

	// Find the latest release newer than current version
	var latestRelease *repo_model.Release
	var latestVersion *semver.Version

	for _, release := range releases {
		if release.IsDraft {
			continue
		}

		// Skip prereleases unless on beta/alpha channel
		if release.IsPrerelease && channel == "stable" {
			continue
		}

		tagVersion := strings.TrimPrefix(release.TagName, "v")
		ver, err := semver.NewVersion(tagVersion)
		if err != nil {
			continue // Skip invalid versions
		}

		// Check if this version is newer than current
		if ver.GreaterThan(current) {
			if latestVersion == nil || ver.GreaterThan(latestVersion) {
				latestVersion = ver
				latestRelease = release
			}
		}
	}

	// No update available
	if latestRelease == nil {
		// Return 204 No Content for no update (Squirrel convention)
		ctx.Status(http.StatusNoContent)
		return
	}

	// Load release attachments
	if err := repo_model.GetReleaseAttachments(ctx, latestRelease); err != nil {
		ctx.APIErrorInternal(err)
		return
	}

	// Find the appropriate asset for this platform/arch
	downloadURL, platformInfo := findUpdateAsset(latestRelease, platform, arch)
	if downloadURL == "" {
		// No compatible asset found
		ctx.Status(http.StatusNoContent)
		return
	}

	response := &AppUpdateResponse{
		URL:      downloadURL,
		Name:     latestRelease.TagName,
		Notes:    latestRelease.Note,
		PubDate:  latestRelease.CreatedUnix.AsTime().Format("2006-01-02T15:04:05Z07:00"),
		Platform: platformInfo,
	}

	ctx.JSON(http.StatusOK, response)
}

// findUpdateAsset finds the appropriate download asset for the given platform and architecture
func findUpdateAsset(release *repo_model.Release, platform, arch string) (string, *PlatformInfo) {
	if len(release.Attachments) == 0 {
		return "", nil
	}

	var platformInfo *PlatformInfo

	// Platform-specific asset patterns
	patterns := getAssetPatterns(platform, arch)

	for _, pattern := range patterns {
		for _, asset := range release.Attachments {
			name := strings.ToLower(asset.Name)
			if matchesPattern(name, pattern) {
				// Build direct download URL
				directURL := fmt.Sprintf("%s%s/%s/releases/download/%s/%s",
					setting.AppURL,
					release.Repo.OwnerName,
					release.Repo.Name,
					release.TagName,
					asset.Name,
				)

				platformInfo = &PlatformInfo{
					Size: asset.Size,
				}

				// For Windows, also look for RELEASES file
				if platform == "windows" {
					for _, a := range release.Attachments {
						if strings.EqualFold(a.Name, "RELEASES") {
							platformInfo.ReleasesURL = fmt.Sprintf("%s%s/%s/releases/download/%s/%s",
								setting.AppURL,
								release.Repo.OwnerName,
								release.Repo.Name,
								release.TagName,
								a.Name,
							)
						}
						if strings.HasSuffix(strings.ToLower(a.Name), ".nupkg") {
							platformInfo.NupkgURL = fmt.Sprintf("%s%s/%s/releases/download/%s/%s",
								setting.AppURL,
								release.Repo.OwnerName,
								release.Repo.Name,
								release.TagName,
								a.Name,
							)
						}
					}
				}

				return directURL, platformInfo
			}
		}
	}

	return "", nil
}

// getAssetPatterns returns file patterns to match for the given platform/arch
func getAssetPatterns(platform, arch string) []string {
	switch platform {
	case "darwin", "macos":
		if arch == "arm64" {
			return []string{
				"arm64.zip",
				"darwin-arm64.zip",
				"macos-arm64.zip",
				"osx-arm64.zip",
				"universal.zip",
				".zip", // Fallback
			}
		}
		return []string{
			"x64.zip",
			"darwin-x64.zip",
			"macos-x64.zip",
			"osx-x64.zip",
			"intel.zip",
			"universal.zip",
			".zip", // Fallback
		}
	case "windows", "win32":
		if arch == "arm64" {
			return []string{
				"arm64.exe",
				"win-arm64.exe",
				"windows-arm64.exe",
				"setup-arm64.exe",
			}
		}
		return []string{
			"x64.exe",
			"win-x64.exe",
			"windows-x64.exe",
			"setup-x64.exe",
			"setup.exe", // Fallback
			".exe",
		}
	case "linux":
		if arch == "arm64" {
			return []string{
				"arm64.appimage",
				"linux-arm64.appimage",
				"aarch64.appimage",
				"arm64.deb",
				"arm64.rpm",
			}
		}
		return []string{
			"x86_64.appimage",
			"linux-x64.appimage",
			"amd64.appimage",
			"amd64.deb",
			"x86_64.rpm",
			".appimage",
			".deb",
		}
	}
	return nil
}

// matchesPattern checks if a filename matches a pattern (case-insensitive suffix)
func matchesPattern(name, pattern string) bool {
	return strings.HasSuffix(name, pattern)
}

// ListReleasesV2 lists releases with enhanced filtering
// GET /api/v2/repos/{owner}/{repo}/releases
func ListReleasesV2(ctx *context.APIContext) {
	repo := ctx.Repo.Repository
	if repo == nil {
		ctx.APIErrorNotFound("Repository not found")
		return
	}

	// Get query parameters
	page := ctx.FormInt("page")
	if page <= 0 {
		page = 1
	}
	limit := ctx.FormInt("limit")
	if limit <= 0 || limit > 100 {
		limit = 30
	}
	includePrereleases := ctx.FormBool("prereleases")
	includeDrafts := ctx.FormBool("drafts") && ctx.Repo.Permission.IsAdmin()

	opts := repo_model.FindReleasesOptions{
		ListOptions: db.ListOptions{
			Page:     page,
			PageSize: limit,
		},
		RepoID:        repo.ID,
		IncludeDrafts: includeDrafts,
		IncludeTags:   false,
	}

	if !includePrereleases {
		opts.IsPreRelease = optional.Some(false)
	}

	releases, err := db.Find[repo_model.Release](ctx, opts)
	if err != nil {
		ctx.APIErrorInternal(err)
		return
	}

	// Load attachments for all releases
	if err := repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
		ctx.APIErrorInternal(err)
		return
	}

	// Convert to API format
	apiReleases := make([]*api.Release, 0, len(releases))
	for _, release := range releases {
		apiReleases = append(apiReleases, convertToAPIRelease(repo, release))
	}

	ctx.JSON(http.StatusOK, apiReleases)
}

// GetReleaseV2 gets a specific release by tag or ID
// GET /api/v2/repos/{owner}/{repo}/releases/{tag}
func GetReleaseV2(ctx *context.APIContext) {
	repo := ctx.Repo.Repository
	if repo == nil {
		ctx.APIErrorNotFound("Repository not found")
		return
	}

	tag := ctx.PathParam("tag")

	var release *repo_model.Release
	var err error

	// Try to parse as ID first
	if id := ctx.PathParamInt64("tag"); id > 0 {
		release, err = repo_model.GetReleaseByID(ctx, id)
	} else {
		// Try as tag name
		release, err = repo_model.GetRelease(ctx, repo.ID, tag)
	}

	if err != nil {
		if repo_model.IsErrReleaseNotExist(err) {
			ctx.APIErrorNotFound("Release not found")
		} else {
			ctx.APIErrorInternal(err)
		}
		return
	}

	if err := repo_model.GetReleaseAttachments(ctx, release); err != nil {
		ctx.APIErrorInternal(err)
		return
	}

	ctx.JSON(http.StatusOK, convertToAPIRelease(repo, release))
}

// GetLatestReleaseV2 gets the latest release
// GET /api/v2/repos/{owner}/{repo}/releases/latest
func GetLatestReleaseV2(ctx *context.APIContext) {
	repo := ctx.Repo.Repository
	if repo == nil {
		ctx.APIErrorNotFound("Repository not found")
		return
	}

	channel := ctx.FormString("channel")
	if channel == "" {
		channel = "stable"
	}

	opts := repo_model.FindReleasesOptions{
		ListOptions: db.ListOptions{
			Page:     1,
			PageSize: 1,
		},
		RepoID:        repo.ID,
		IncludeDrafts: false,
		IncludeTags:   false,
	}

	if channel == "stable" {
		opts.IsPreRelease = optional.Some(false)
	}

	releases, err := db.Find[repo_model.Release](ctx, opts)
	if err != nil {
		ctx.APIErrorInternal(err)
		return
	}

	if len(releases) == 0 {
		ctx.APIErrorNotFound("No releases found")
		return
	}

	release := releases[0]
	if err := repo_model.GetReleaseAttachments(ctx, release); err != nil {
		ctx.APIErrorInternal(err)
		return
	}

	ctx.JSON(http.StatusOK, convertToAPIRelease(repo, release))
}

// convertToAPIRelease converts a repo_model.Release to api.Release
func convertToAPIRelease(repo *repo_model.Repository, release *repo_model.Release) *api.Release {
	assets := make([]*api.Attachment, 0, len(release.Attachments))
	for _, attachment := range release.Attachments {
		assets = append(assets, &api.Attachment{
			ID:            attachment.ID,
			Name:          attachment.Name,
			Size:          attachment.Size,
			DownloadCount: attachment.DownloadCount,
			Created:       attachment.CreatedUnix.AsTime(),
			UUID:          attachment.UUID,
			DownloadURL: fmt.Sprintf("%s%s/%s/releases/download/%s/%s",
				setting.AppURL,
				repo.OwnerName,
				repo.Name,
				release.TagName,
				attachment.Name,
			),
		})
	}

	return &api.Release{
		ID:           release.ID,
		TagName:      release.TagName,
		Target:       release.Target,
		Title:        release.Title,
		Note:         release.Note,
		URL:          release.HTMLURL(),
		HTMLURL:      release.HTMLURL(),
		TarURL:       release.TarURL(),
		ZipURL:       release.ZipURL(),
		IsDraft:      release.IsDraft,
		IsPrerelease: release.IsPrerelease,
		CreatedAt:    release.CreatedUnix.AsTime(),
		PublishedAt:  release.CreatedUnix.AsTime(),
		Attachments:  assets,
	}
}

// repoAssignmentWithPublicAccess is a variant of repoAssignment that allows
// public access for repos with public_landing or public_releases enabled
func repoAssignmentWithPublicAccess() func(ctx *context.APIContext) {
	return func(ctx *context.APIContext) {
		ownerName := ctx.PathParam("owner")
		repoName := ctx.PathParam("repo")

		// Get owner
		var owner *user_model.User
		var err error

		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

		// Check if repo is public
		if !repo.IsPrivate {
			// Get permissions for public repo
			ctx.Repo.Permission, _ = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
			return // Public repo, allow access
		}

		// For private repos, check if public landing/releases is enabled
		if pages_service.HasPublicLanding(ctx, repo) || pages_service.HasPublicReleases(ctx, repo) {
			// Allow read-only access for public landing/releases
			ctx.Repo.Permission, _ = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
			return
		}

		// Otherwise, require authentication
		if !ctx.IsSigned {
			ctx.APIErrorWithCode(apierrors.AuthTokenMissing)
			return
		}

		// Check permission
		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
		}
	}
}
