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

package conan

import (
	std_ctx "context"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	auth_model "code.gitea.io/gitea/models/auth"
	"code.gitea.io/gitea/models/db"
	packages_model "code.gitea.io/gitea/models/packages"
	conan_model "code.gitea.io/gitea/models/packages/conan"
	"code.gitea.io/gitea/modules/container"
	"code.gitea.io/gitea/modules/json"
	"code.gitea.io/gitea/modules/log"
	packages_module "code.gitea.io/gitea/modules/packages"
	conan_module "code.gitea.io/gitea/modules/packages/conan"
	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/routers/api/packages/helper"
	auth_service "code.gitea.io/gitea/services/auth"
	"code.gitea.io/gitea/services/context"
	notify_service "code.gitea.io/gitea/services/notify"
	packages_service "code.gitea.io/gitea/services/packages"
)

const (
	conanfileFile = "conanfile.py"
	conaninfoFile = "conaninfo.txt"

	recipeReferenceKey  = "RecipeReference"
	packageReferenceKey = "PackageReference"
)

var (
	recipeFileList = container.SetOf(
		conanfileFile,
		"conanmanifest.txt",
		"conan_sources.tgz",
		"conan_export.tgz",
	)
	packageFileList = container.SetOf(
		conaninfoFile,
		"conanmanifest.txt",
		"conan_package.tgz",
	)
)

func jsonResponse(ctx *context.Context, status int, obj any) {
	// https://github.com/conan-io/conan/issues/6613
	ctx.Resp.Header().Set("Content-Type", "application/json")
	ctx.Status(status)
	if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
		log.Error("JSON encode: %v", err)
	}
}

func apiError(ctx *context.Context, status int, obj any) {
	helper.LogAndProcessError(ctx, status, obj, func(message string) {
		jsonResponse(ctx, status, map[string]string{
			"message": message,
		})
	})
}

func baseURL(ctx *context.Context) string {
	return setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/conan"
}

// ExtractPathParameters is a middleware to extract common parameters from path
func ExtractPathParameters(ctx *context.Context) {
	rref, err := conan_module.NewRecipeReference(
		ctx.PathParam("name"),
		ctx.PathParam("version"),
		ctx.PathParam("user"),
		ctx.PathParam("channel"),
		ctx.PathParam("recipe_revision"),
	)
	if err != nil {
		apiError(ctx, http.StatusBadRequest, err)
		return
	}

	ctx.Data[recipeReferenceKey] = rref

	reference := ctx.PathParam("package_reference")

	var pref *conan_module.PackageReference
	if reference != "" {
		pref, err = conan_module.NewPackageReference(
			rref,
			reference,
			ctx.PathParam("package_revision"),
		)
		if err != nil {
			apiError(ctx, http.StatusBadRequest, err)
			return
		}
	}

	ctx.Data[packageReferenceKey] = pref
}

// Ping reports the server capabilities
func Ping(ctx *context.Context) {
	ctx.RespHeader().Add("X-Conan-Server-Capabilities", "revisions") // complex_search,checksum_deploy,matrix_params

	ctx.Status(http.StatusOK)
}

// Authenticate creates an authentication token for the user
func Authenticate(ctx *context.Context) {
	if ctx.Doer == nil {
		apiError(ctx, http.StatusBadRequest, nil)
		return
	}

	packageScope := auth_service.GetAccessScope(ctx.Data)
	if has, err := packageScope.HasAnyScope(
		auth_model.AccessTokenScopeReadPackage,
		auth_model.AccessTokenScopeWritePackage,
		auth_model.AccessTokenScopeAll,
	); !has {
		if err != nil {
			log.Error("Error checking access scope: %v", err)
		}
		apiError(ctx, http.StatusForbidden, nil)
		return
	}

	token, err := packages_service.CreateAuthorizationToken(ctx.Doer, packageScope)
	if err != nil {
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}

	ctx.PlainText(http.StatusOK, token)
}

// CheckCredentials tests if the provided authentication token is valid
func CheckCredentials(ctx *context.Context) {
	if ctx.Doer == nil {
		ctx.Status(http.StatusUnauthorized)
		return
	}

	packageScope := auth_service.GetAccessScope(ctx.Data)
	if has, err := packageScope.HasAnyScope(
		auth_model.AccessTokenScopeReadPackage,
		auth_model.AccessTokenScopeWritePackage,
		auth_model.AccessTokenScopeAll,
	); !has {
		if err != nil {
			log.Error("Error checking access scope: %v", err)
		}
		ctx.Status(http.StatusForbidden)
		return
	}

	ctx.Status(http.StatusOK)
}

// RecipeSnapshot displays the recipe files with their md5 hash
func RecipeSnapshot(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	serveSnapshot(ctx, rref.AsKey())
}

// RecipeSnapshot displays the package files with their md5 hash
func PackageSnapshot(ctx *context.Context) {
	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)

	serveSnapshot(ctx, pref.AsKey())
}

func serveSnapshot(ctx *context.Context, fileKey string) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
	if err != nil {
		if err == packages_model.ErrPackageNotExist {
			apiError(ctx, http.StatusNotFound, err)
		} else {
			apiError(ctx, http.StatusInternalServerError, err)
		}
		return
	}

	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
		VersionID:    pv.ID,
		CompositeKey: fileKey,
	})
	if err != nil {
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}
	if len(pfs) == 0 {
		apiError(ctx, http.StatusNotFound, nil)
		return
	}

	files := make(map[string]string)
	for _, pf := range pfs {
		pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
		if err != nil {
			apiError(ctx, http.StatusInternalServerError, err)
			return
		}
		files[pf.Name] = pb.HashMD5
	}

	jsonResponse(ctx, http.StatusOK, files)
}

// RecipeDownloadURLs displays the recipe files with their download url
func RecipeDownloadURLs(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	serveDownloadURLs(
		ctx,
		rref.AsKey(),
		fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/recipe", rref.LinkName()),
	)
}

// PackageDownloadURLs displays the package files with their download url
func PackageDownloadURLs(ctx *context.Context) {
	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)

	serveDownloadURLs(
		ctx,
		pref.AsKey(),
		fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/package/%s", pref.Recipe.LinkName(), pref.LinkName()),
	)
}

func serveDownloadURLs(ctx *context.Context, fileKey, downloadURL string) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
	if err != nil {
		if err == packages_model.ErrPackageNotExist {
			apiError(ctx, http.StatusNotFound, err)
		} else {
			apiError(ctx, http.StatusInternalServerError, err)
		}
		return
	}

	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
		VersionID:    pv.ID,
		CompositeKey: fileKey,
	})
	if err != nil {
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}

	if len(pfs) == 0 {
		apiError(ctx, http.StatusNotFound, nil)
		return
	}

	urls := make(map[string]string)
	for _, pf := range pfs {
		urls[pf.Name] = fmt.Sprintf("%s/%s", downloadURL, pf.Name)
	}

	jsonResponse(ctx, http.StatusOK, urls)
}

// RecipeUploadURLs displays the upload urls for the provided recipe files
func RecipeUploadURLs(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	serveUploadURLs(
		ctx,
		recipeFileList,
		fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/recipe", rref.LinkName()),
	)
}

// PackageUploadURLs displays the upload urls for the provided package files
func PackageUploadURLs(ctx *context.Context) {
	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)

	serveUploadURLs(
		ctx,
		packageFileList,
		fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/package/%s", pref.Recipe.LinkName(), pref.LinkName()),
	)
}

func serveUploadURLs(ctx *context.Context, fileFilter container.Set[string], uploadURL string) {
	defer ctx.Req.Body.Close()

	var files map[string]int64
	if err := json.NewDecoder(ctx.Req.Body).Decode(&files); err != nil {
		apiError(ctx, http.StatusBadRequest, err)
		return
	}

	urls := make(map[string]string)
	for file := range files {
		if fileFilter.Contains(file) {
			urls[file] = fmt.Sprintf("%s/%s", uploadURL, file)
		}
	}

	jsonResponse(ctx, http.StatusOK, urls)
}

// UploadRecipeFile handles the upload of a recipe file
func UploadRecipeFile(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	uploadFile(ctx, recipeFileList, rref.AsKey())
}

// UploadPackageFile handles the upload of a package file
func UploadPackageFile(ctx *context.Context) {
	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)

	uploadFile(ctx, packageFileList, pref.AsKey())
}

func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)

	filename := ctx.PathParam("filename")
	if !fileFilter.Contains(filename) {
		apiError(ctx, http.StatusBadRequest, nil)
		return
	}

	upload, needToClose, err := ctx.UploadStream()
	if err != nil {
		apiError(ctx, http.StatusBadRequest, err)
		return
	}
	if needToClose {
		defer upload.Close()
	}

	buf, err := packages_module.CreateHashedBufferFromReader(upload)
	if err != nil {
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}
	defer buf.Close()

	isConanfileFile := filename == conanfileFile
	isConaninfoFile := filename == conaninfoFile

	pci := &packages_service.PackageCreationInfo{
		PackageInfo: packages_service.PackageInfo{
			Owner:       ctx.Package.Owner,
			PackageType: packages_model.TypeConan,
			Name:        rref.Name,
			Version:     rref.Version,
		},
		Creator: ctx.Doer,
	}
	pfci := &packages_service.PackageFileCreationInfo{
		PackageFileInfo: packages_service.PackageFileInfo{
			Filename:     strings.ToLower(filename),
			CompositeKey: fileKey,
		},
		Creator: ctx.Doer,
		Data:    buf,
		IsLead:  isConanfileFile,
		Properties: map[string]string{
			conan_module.PropertyRecipeUser:     rref.User,
			conan_module.PropertyRecipeChannel:  rref.Channel,
			conan_module.PropertyRecipeRevision: rref.RevisionOrDefault(),
		},
		OverwriteExisting: true,
	}

	if pref != nil {
		pfci.Properties[conan_module.PropertyPackageReference] = pref.Reference
		pfci.Properties[conan_module.PropertyPackageRevision] = pref.RevisionOrDefault()
	}

	if isConanfileFile || isConaninfoFile {
		if isConanfileFile {
			metadata, err := conan_module.ParseConanfile(buf)
			if err != nil {
				log.Error("Error parsing package metadata: %v", err)
				apiError(ctx, http.StatusInternalServerError, err)
				return
			}
			pv, err := packages_model.GetVersionByNameAndVersion(ctx, pci.Owner.ID, pci.PackageType, pci.Name, pci.Version)
			if err != nil && err != packages_model.ErrPackageNotExist {
				apiError(ctx, http.StatusInternalServerError, err)
				return
			}
			if pv != nil {
				raw, err := json.Marshal(metadata)
				if err != nil {
					apiError(ctx, http.StatusInternalServerError, err)
					return
				}
				pv.MetadataJSON = string(raw)
				if err := packages_model.UpdateVersion(ctx, pv); err != nil {
					apiError(ctx, http.StatusInternalServerError, err)
					return
				}
			} else {
				pci.Metadata = metadata
			}
		} else {
			info, err := conan_module.ParseConaninfo(buf)
			if err != nil {
				log.Error("Error parsing conan info: %v", err)
				apiError(ctx, http.StatusInternalServerError, err)
				return
			}
			raw, err := json.Marshal(info)
			if err != nil {
				apiError(ctx, http.StatusInternalServerError, err)
				return
			}
			pfci.Properties[conan_module.PropertyPackageInfo] = string(raw)
		}

		if _, err := buf.Seek(0, io.SeekStart); err != nil {
			apiError(ctx, http.StatusInternalServerError, err)
			return
		}
	}

	_, _, err = packages_service.CreatePackageOrAddFileToExisting(
		ctx,
		pci,
		pfci,
	)
	if err != nil {
		switch err {
		case packages_model.ErrDuplicatePackageFile:
			apiError(ctx, http.StatusConflict, err)
		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
			apiError(ctx, http.StatusForbidden, err)
		default:
			apiError(ctx, http.StatusInternalServerError, err)
		}
		return
	}

	ctx.Status(http.StatusCreated)
}

// DownloadRecipeFile serves the content of the requested recipe file
func DownloadRecipeFile(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	downloadFile(ctx, recipeFileList, rref.AsKey())
}

// DownloadPackageFile serves the content of the requested package file
func DownloadPackageFile(ctx *context.Context) {
	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)

	downloadFile(ctx, packageFileList, pref.AsKey())
}

func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	filename := ctx.PathParam("filename")
	if !fileFilter.Contains(filename) {
		apiError(ctx, http.StatusBadRequest, nil)
		return
	}

	s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
		ctx,
		&packages_service.PackageInfo{
			Owner:       ctx.Package.Owner,
			PackageType: packages_model.TypeConan,
			Name:        rref.Name,
			Version:     rref.Version,
		},
		&packages_service.PackageFileInfo{
			Filename:     filename,
			CompositeKey: fileKey,
		},
	)
	if err != nil {
		if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
			apiError(ctx, http.StatusNotFound, err)
			return
		}
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}

	helper.ServePackageFile(ctx, s, u, pf)
}

// DeleteRecipeV1 deletes the requested recipe(s)
func DeleteRecipeV1(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	if err := deleteRecipeOrPackage(ctx, rref, true, nil, false); err != nil {
		if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
			apiError(ctx, http.StatusNotFound, err)
		} else {
			apiError(ctx, http.StatusInternalServerError, err)
		}
		return
	}
	ctx.Status(http.StatusOK)
}

// DeleteRecipeV2 deletes the requested recipe(s) respecting its revisions
func DeleteRecipeV2(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	if err := deleteRecipeOrPackage(ctx, rref, rref.Revision == "", nil, false); err != nil {
		if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
			apiError(ctx, http.StatusNotFound, err)
		} else {
			apiError(ctx, http.StatusInternalServerError, err)
		}
		return
	}
	ctx.Status(http.StatusOK)
}

// DeletePackageV1 deletes the requested package(s)
func DeletePackageV1(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	type PackageReferences struct {
		References []string `json:"package_ids"`
	}

	var ids *PackageReferences
	if err := json.NewDecoder(ctx.Req.Body).Decode(&ids); err != nil {
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}

	revisions, err := conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref)
	if err != nil {
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}
	for _, revision := range revisions {
		currentRref := rref.WithRevision(revision.Value)

		var references []*conan_model.PropertyValue
		if len(ids.References) == 0 {
			if references, err = conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, currentRref); err != nil {
				apiError(ctx, http.StatusInternalServerError, err)
				return
			}
		} else {
			for _, reference := range ids.References {
				references = append(references, &conan_model.PropertyValue{Value: reference})
			}
		}

		for _, reference := range references {
			pref, _ := conan_module.NewPackageReference(currentRref, reference.Value, conan_module.DefaultRevision)
			if err := deleteRecipeOrPackage(ctx, currentRref, true, pref, true); err != nil {
				if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
					apiError(ctx, http.StatusNotFound, err)
				} else {
					apiError(ctx, http.StatusInternalServerError, err)
				}
				return
			}
		}
	}
	ctx.Status(http.StatusOK)
}

// DeletePackageV2 deletes the requested package(s) respecting its revisions
func DeletePackageV2(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)

	if pref != nil { // has package reference
		if err := deleteRecipeOrPackage(ctx, rref, false, pref, pref.Revision == ""); err != nil {
			if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
				apiError(ctx, http.StatusNotFound, err)
			} else {
				apiError(ctx, http.StatusInternalServerError, err)
			}
		} else {
			ctx.Status(http.StatusOK)
		}
		return
	}

	references, err := conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, rref)
	if err != nil {
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}
	if len(references) == 0 {
		apiError(ctx, http.StatusNotFound, conan_model.ErrPackageReferenceNotExist)
		return
	}

	for _, reference := range references {
		pref, _ := conan_module.NewPackageReference(rref, reference.Value, conan_module.DefaultRevision)

		if err := deleteRecipeOrPackage(ctx, rref, false, pref, true); err != nil {
			if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
				apiError(ctx, http.StatusNotFound, err)
			} else {
				apiError(ctx, http.StatusInternalServerError, err)
			}
			return
		}
	}

	ctx.Status(http.StatusOK)
}

func deleteRecipeOrPackage(apictx *context.Context, rref *conan_module.RecipeReference, ignoreRecipeRevision bool, pref *conan_module.PackageReference, ignorePackageRevision bool) error {
	var pd *packages_model.PackageDescriptor
	versionDeleted := false

	err := db.WithTx(apictx, func(ctx std_ctx.Context) error {
		pv, err := packages_model.GetVersionByNameAndVersion(ctx, apictx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
		if err != nil {
			return err
		}

		pd, err = packages_model.GetPackageDescriptor(ctx, pv)
		if err != nil {
			return err
		}

		filter := map[string]string{
			conan_module.PropertyRecipeUser:    rref.User,
			conan_module.PropertyRecipeChannel: rref.Channel,
		}
		if !ignoreRecipeRevision {
			filter[conan_module.PropertyRecipeRevision] = rref.RevisionOrDefault()
		}
		if pref != nil {
			filter[conan_module.PropertyPackageReference] = pref.Reference
			if !ignorePackageRevision {
				filter[conan_module.PropertyPackageRevision] = pref.RevisionOrDefault()
			}
		}

		pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
			VersionID:  pv.ID,
			Properties: filter,
		})
		if err != nil {
			return err
		}
		if len(pfs) == 0 {
			return conan_model.ErrPackageReferenceNotExist
		}

		for _, pf := range pfs {
			if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
				return err
			}
		}
		has, err := packages_model.HasVersionFileReferences(ctx, pv.ID)
		if err != nil {
			return err
		}
		if !has {
			versionDeleted = true

			return packages_service.DeletePackageVersionAndReferences(ctx, pv)
		}
		return nil
	})
	if err != nil {
		return err
	}

	if versionDeleted {
		notify_service.PackageDelete(apictx, apictx.Doer, pd)
	}

	return nil
}

// ListRecipeRevisions gets a list of all recipe revisions
func ListRecipeRevisions(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	revisions, err := conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref)
	if err != nil {
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}

	listRevisions(ctx, revisions)
}

// ListPackageRevisions gets a list of all package revisions
func ListPackageRevisions(ctx *context.Context) {
	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)

	revisions, err := conan_model.GetPackageRevisions(ctx, ctx.Package.Owner.ID, pref)
	if err != nil {
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}

	listRevisions(ctx, revisions)
}

type revisionInfo struct {
	Revision string    `json:"revision"`
	Time     time.Time `json:"time"`
}

func listRevisions(ctx *context.Context, revisions []*conan_model.PropertyValue) {
	if len(revisions) == 0 {
		apiError(ctx, http.StatusNotFound, conan_model.ErrRecipeReferenceNotExist)
		return
	}

	type RevisionList struct {
		Revisions []*revisionInfo `json:"revisions"`
	}

	revs := make([]*revisionInfo, 0, len(revisions))
	for _, rev := range revisions {
		revs = append(revs, &revisionInfo{Revision: rev.Value, Time: rev.CreatedUnix.AsLocalTime()})
	}

	jsonResponse(ctx, http.StatusOK, &RevisionList{revs})
}

// LatestRecipeRevision gets the latest recipe revision
func LatestRecipeRevision(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	revision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref)
	if err != nil {
		if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist {
			apiError(ctx, http.StatusNotFound, err)
		} else {
			apiError(ctx, http.StatusInternalServerError, err)
		}
		return
	}

	jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: revision.CreatedUnix.AsLocalTime()})
}

// LatestPackageRevision gets the latest package revision
func LatestPackageRevision(ctx *context.Context) {
	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)

	revision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref)
	if err != nil {
		if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist {
			apiError(ctx, http.StatusNotFound, err)
		} else {
			apiError(ctx, http.StatusInternalServerError, err)
		}
		return
	}

	jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: revision.CreatedUnix.AsLocalTime()})
}

// ListRecipeRevisionFiles gets a list of all recipe revision files
func ListRecipeRevisionFiles(ctx *context.Context) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	listRevisionFiles(ctx, rref.AsKey())
}

// ListPackageRevisionFiles gets a list of all package revision files
func ListPackageRevisionFiles(ctx *context.Context) {
	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)

	listRevisionFiles(ctx, pref.AsKey())
}

func listRevisionFiles(ctx *context.Context, fileKey string) {
	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)

	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
	if err != nil {
		if err == packages_model.ErrPackageNotExist {
			apiError(ctx, http.StatusNotFound, err)
		} else {
			apiError(ctx, http.StatusInternalServerError, err)
		}
		return
	}

	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
		VersionID:    pv.ID,
		CompositeKey: fileKey,
	})
	if err != nil {
		apiError(ctx, http.StatusInternalServerError, err)
		return
	}
	if len(pfs) == 0 {
		apiError(ctx, http.StatusNotFound, nil)
		return
	}

	files := make(map[string]any)
	for _, pf := range pfs {
		files[pf.Name] = nil
	}

	type FileList struct {
		Files map[string]any `json:"files"`
	}

	jsonResponse(ctx, http.StatusOK, &FileList{
		Files: files,
	})
}
