package weldr

import (
	"archive/tar"
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/json"
	"errors"
	errors_package "errors"
	"fmt"
	"io"
	"log"
	"math"
	"math/big"
	"net"
	"net/http"
	"net/url"
	"os"
	"path"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/BurntSushi/toml"
	"github.com/gobwas/glob"
	"github.com/google/uuid"
	"github.com/julienschmidt/httprouter"
	"github.com/osbuild/osbuild-composer/pkg/jobqueue"

	"github.com/osbuild/images/pkg/arch"
	"github.com/osbuild/images/pkg/container"
	"github.com/osbuild/images/pkg/distro"
	"github.com/osbuild/images/pkg/distrofactory"
	"github.com/osbuild/images/pkg/distroidparser"
	"github.com/osbuild/images/pkg/dnfjson"
	"github.com/osbuild/images/pkg/osbuild"
	"github.com/osbuild/images/pkg/ostree"
	"github.com/osbuild/images/pkg/reporegistry"
	"github.com/osbuild/images/pkg/rhsm/facts"
	"github.com/osbuild/images/pkg/rpmmd"
	"github.com/osbuild/osbuild-composer/internal/blueprint"
	"github.com/osbuild/osbuild-composer/internal/common"
	"github.com/osbuild/osbuild-composer/internal/store"
	"github.com/osbuild/osbuild-composer/internal/target"
	"github.com/osbuild/osbuild-composer/internal/worker"
)

type API struct {
	store   *store.Store
	workers *worker.Server

	solver       *dnfjson.BaseSolver
	hostArch     string
	repoRegistry *reporegistry.RepoRegistry

	logger *log.Logger
	router *httprouter.Router
	server http.Server

	compatOutputDir string

	hostDistroName string                 // Name of the host distro
	distroFactory  *distrofactory.Factory // Available distros

	//  List of ImageType names, which should not be exposed by the API
	distrosImageTypeDenylist map[string][]string
}

type ComposeState int

const (
	ComposeWaiting ComposeState = iota
	ComposeRunning
	ComposeFinished
	ComposeFailed
)

// ToString converts ImageBuildState into a human readable string
func (cs ComposeState) ToString() string {
	switch cs {
	case ComposeWaiting:
		return "WAITING"
	case ComposeRunning:
		return "RUNNING"
	case ComposeFinished:
		return "FINISHED"
	case ComposeFailed:
		return "FAILED"
	default:
		panic("invalid ComposeState value")
	}
}

// systemRepoNames returns a list of the system repos
// NOTE: The system repos have no concept of id vs. name so the id is returned
func (api *API) systemRepoNames() (names []string) {
	repos, err := api.repoRegistry.ReposByArchName(api.hostDistroName, api.hostArch, false)
	if err == nil {
		for _, repo := range repos {
			names = append(names, repo.Name)
		}
	}
	return names
}

// validDistros returns a sorted list of distributions that also have
// repositories defined for the given architecture
func (api *API) validDistros(arch string) []string {
	distros := []string{}
	for _, distroName := range api.repoRegistry.ListDistros() {
		distro := api.distroFactory.GetDistro(distroName)
		if distro == nil {
			if api.logger != nil {
				api.logger.Printf("Distro %s has repositories defined, but it's not supported. Skipping.", distroName)
			}
			continue
		}

		_, err := api.repoRegistry.DistroHasRepos(distroName, arch)
		if err == nil {
			distros = append(distros, distroName)
		} else {
			if api.logger != nil {
				api.logger.Printf("Distro %s has no repositories defined for %s architecture, skipping.", distroName, arch)
			}
		}
	}

	sort.Strings(distros)
	return distros
}

var ValidBlueprintName = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)

// NewTestAPI is used for the test framework, sets up a single distro
func NewTestAPI(solver *dnfjson.BaseSolver, rr *reporegistry.RepoRegistry,
	logger *log.Logger, storeFixture *store.Fixture, workers *worker.Server,
	compatOutputDir string, distrosImageTypeDenylist map[string][]string) *API {

	api := &API{
		store:                    storeFixture.Store,
		workers:                  workers,
		solver:                   solver,
		hostArch:                 storeFixture.HostArchName,
		repoRegistry:             rr,
		logger:                   logger,
		compatOutputDir:          compatOutputDir,
		hostDistroName:           storeFixture.HostDistroName,
		distroFactory:            storeFixture.Factory,
		distrosImageTypeDenylist: distrosImageTypeDenylist,
	}
	return setupRouter(api)
}

func New(repoPaths []string, stateDir string, solver *dnfjson.BaseSolver, df *distrofactory.Factory,
	logger *log.Logger, workers *worker.Server, distrosImageTypeDenylist map[string][]string) (*API, error) {
	if logger == nil {
		logger = log.New(os.Stdout, "", 0)
	}

	hostDistroName, err := distro.GetHostDistroName()
	if err != nil {
		return nil, fmt.Errorf("failed to read host distro information")
	}
	hostArch := arch.Current().String()

	rr, err := reporegistry.New(repoPaths)
	if err != nil {
		return nil, fmt.Errorf("error loading repository definitions: %v", err)
	}

	// Clean up the cache, removes unknown distros and files
	solver.CleanupOldCacheDirs(rr.ListDistros())

	hostDistro := df.GetDistro(hostDistroName)
	if hostDistro != nil {
		// get canonical distro name if the host distro is supported
		hostDistroName = hostDistro.Name()

		_, err = hostDistro.GetArch(hostArch)
		if err != nil {
			return nil, fmt.Errorf("Host distro does not support host architecture: %v", err)
		}

		// Check if repositories for the host distro and arch were loaded
		_, err = rr.ReposByArchName(hostDistroName, hostArch, false)
		if err != nil {
			log.Printf("loaded repository definitions don't contain any for the host distro/arch: %v", err)
		}

	} else {
		log.Printf("host distro %q is not supported: only cross-distro builds are available", hostDistroName)
	}

	store := store.New(&stateDir, df, logger)
	compatOutputDir := path.Join(stateDir, "outputs")

	api := &API{
		store:                    store,
		workers:                  workers,
		solver:                   solver,
		hostArch:                 hostArch,
		repoRegistry:             rr,
		logger:                   logger,
		compatOutputDir:          compatOutputDir,
		hostDistroName:           hostDistroName,
		distroFactory:            df,
		distrosImageTypeDenylist: distrosImageTypeDenylist,
	}
	return setupRouter(api), nil
}

func setupRouter(api *API) *API {
	api.router = httprouter.New()
	api.router.RedirectTrailingSlash = false
	api.router.RedirectFixedPath = false
	api.router.MethodNotAllowed = http.HandlerFunc(methodNotAllowedHandler)
	api.router.NotFound = http.HandlerFunc(notFoundHandler)

	api.router.GET("/api/status", api.statusHandler)
	api.router.GET("/api/v:version/projects/source/list", api.sourceListHandler)
	api.router.GET("/api/v:version/projects/source/info/", api.sourceEmptyInfoHandler)
	api.router.GET("/api/v:version/projects/source/info/:sources", api.sourceInfoHandler)
	api.router.POST("/api/v:version/projects/source/new", api.sourceNewHandler)
	api.router.DELETE("/api/v:version/projects/source/delete/*source", api.sourceDeleteHandler)

	api.router.GET("/api/v:version/projects/depsolve", api.projectsDepsolveHandler)
	api.router.GET("/api/v:version/projects/depsolve/*projects", api.projectsDepsolveHandler)

	api.router.GET("/api/v:version/modules/list", api.modulesListHandler)
	api.router.GET("/api/v:version/modules/list/*modules", api.modulesListHandler)
	api.router.GET("/api/v:version/projects/list", api.projectsListHandler)
	api.router.GET("/api/v:version/projects/list/", api.projectsListHandler)

	// these are the same, except that modules/info also includes dependencies
	api.router.GET("/api/v:version/modules/info", api.modulesInfoHandler)
	api.router.GET("/api/v:version/modules/info/*modules", api.modulesInfoHandler)
	api.router.GET("/api/v:version/projects/info", api.modulesInfoHandler)
	api.router.GET("/api/v:version/projects/info/*modules", api.modulesInfoHandler)

	api.router.GET("/api/v:version/blueprints/list", api.blueprintsListHandler)
	api.router.GET("/api/v:version/blueprints/info/*blueprints", api.blueprintsInfoHandler)
	api.router.GET("/api/v:version/blueprints/depsolve/*blueprints", api.blueprintsDepsolveHandler)
	api.router.GET("/api/v:version/blueprints/freeze/*blueprints", api.blueprintsFreezeHandler)
	api.router.GET("/api/v:version/blueprints/diff/:blueprint/:from/:to", api.blueprintsDiffHandler)
	api.router.GET("/api/v:version/blueprints/change/:blueprint/:commit", api.blueprintsChangeHandler)
	api.router.GET("/api/v:version/blueprints/changes/*blueprints", api.blueprintsChangesHandler)
	api.router.POST("/api/v:version/blueprints/new", api.blueprintsNewHandler)
	api.router.POST("/api/v:version/blueprints/workspace", api.blueprintsWorkspaceHandler)
	api.router.POST("/api/v:version/blueprints/undo/:blueprint/:commit", api.blueprintUndoHandler)
	api.router.POST("/api/v:version/blueprints/tag/:blueprint", api.blueprintsTagHandler)
	api.router.DELETE("/api/v:version/blueprints/delete/:blueprint", api.blueprintDeleteHandler)
	api.router.DELETE("/api/v:version/blueprints/workspace/:blueprint", api.blueprintDeleteWorkspaceHandler)

	api.router.POST("/api/v:version/compose", api.composeHandler)
	api.router.DELETE("/api/v:version/compose/delete/:uuids", api.composeDeleteHandler)
	api.router.GET("/api/v:version/compose/types", api.composeTypesHandler)
	api.router.GET("/api/v:version/compose/queue", api.composeQueueHandler)
	api.router.GET("/api/v:version/compose/status/:uuids", api.composeStatusHandler)
	api.router.GET("/api/v:version/compose/info/:uuid", api.composeInfoHandler)
	api.router.GET("/api/v:version/compose/finished", api.composeFinishedHandler)
	api.router.GET("/api/v:version/compose/failed", api.composeFailedHandler)
	api.router.GET("/api/v:version/compose/image/:uuid", api.composeImageHandler)
	api.router.GET("/api/v:version/compose/metadata/:uuid", api.composeMetadataHandler)
	api.router.GET("/api/v:version/compose/results/:uuid", api.composeResultsHandler)
	api.router.GET("/api/v:version/compose/logs/:uuid", api.composeLogsHandler)
	api.router.GET("/api/v:version/compose/log/:uuid", api.composeLogHandler)
	api.router.POST("/api/v:version/compose/uploads/schedule/:uuid", api.uploadsScheduleHandler)
	api.router.DELETE("/api/v:version/compose/cancel/:uuid", api.composeCancelHandler)

	api.router.DELETE("/api/v:version/upload/delete/:uuid", api.uploadsDeleteHandler)
	api.router.GET("/api/v:version/upload/info/:uuid", api.uploadsInfoHandler)
	api.router.GET("/api/v:version/upload/log/:uuid", api.uploadsLogHandler)
	api.router.POST("/api/v:version/upload/reset/:uuid", api.uploadsResetHandler)
	api.router.DELETE("/api/v:version/upload/cancel/:uuid", api.uploadsCancelHandler)

	api.router.GET("/api/v:version/upload/providers", api.providersHandler)
	api.router.POST("/api/v:version/upload/providers/save", api.providersSaveHandler)
	api.router.DELETE("/api/v:version/upload/providers/delete/:provider/:profile", api.providersDeleteHandler)

	api.router.GET("/api/v:version/distros/list", api.distrosListHandler)
	return api
}

func (api *API) Serve(listener net.Listener) error {
	api.server = http.Server{
		Handler:           api,
		ReadHeaderTimeout: 5 * time.Second,
	}

	err := api.server.Serve(listener)
	if err != nil && err != http.ErrServerClosed {
		return err
	}

	return nil
}

func (api *API) Shutdown(ctx context.Context) error {
	return api.server.Shutdown(ctx)
}

func (api *API) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	if api.logger != nil {
		log.Println(request.Method, request.URL.Path)
	}

	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
	api.router.ServeHTTP(writer, request)
}

// PreloadMetadata loads the metadata for all supported distros
// This starts a background depsolve for all known distros in order to preload the
// metadata.
func (api *API) PreloadMetadata() {
	log.Printf("Starting metadata preload goroutines")
	for _, distro := range api.validDistros(api.hostArch) {
		go func(distro string) {
			startTime := time.Now()
			d := api.getDistro(distro, api.hostArch)
			if d == nil {
				log.Printf("GetDistro - unknown distribution: %s", distro)
				return
			}

			repos, err := api.allRepositories(distro, api.hostArch)
			if err != nil {
				log.Printf("Error getting repositories for distro %s: %s", distro, err)
				return
			}

			solver := api.solver.NewWithConfig(d.ModulePlatformID(), d.Releasever(), api.hostArch, d.Name())
			_, err = solver.Depsolve([]rpmmd.PackageSet{{Include: []string{"filesystem"}, Repositories: repos}})
			if err != nil {
				log.Printf("Problem preloading distro metadata for %s: %s", distro, err)
			}
			log.Printf("Finished preload of %s in %v", distro, time.Since(startTime))
		}(distro)
	}
}

type composeStatus struct {
	State    ComposeState
	Queued   time.Time
	Started  time.Time
	Finished time.Time
	Result   *osbuild.Result
}

func composeStateFromJobStatus(js *worker.JobStatus, result *worker.OSBuildJobResult) ComposeState {
	if js.Canceled {
		return ComposeFailed
	}

	if js.Started.IsZero() {
		return ComposeWaiting
	}

	if js.Finished.IsZero() {
		return ComposeRunning
	}

	if result.Success {
		return ComposeFinished
	}

	return ComposeFailed
}

// Returns the state of the image in `compose` and the times the job was
// queued, started, and finished. Assumes that there's only one image in the
// compose.
func (api *API) getComposeStatus(compose store.Compose) (*composeStatus, error) {
	jobId := compose.ImageBuild.JobID

	// backwards compatibility: composes that were around before splitting
	// the job queue from the store still contain their valid status and
	// times. Return those here as a fallback.
	if jobId == uuid.Nil {
		var state ComposeState
		switch compose.ImageBuild.QueueStatus {
		case common.IBWaiting:
			state = ComposeWaiting
		case common.IBRunning:
			state = ComposeRunning
		case common.IBFinished:
			state = ComposeFinished
		case common.IBFailed:
			state = ComposeFailed
		}
		return &composeStatus{
			State:    state,
			Queued:   compose.ImageBuild.JobCreated,
			Started:  compose.ImageBuild.JobStarted,
			Finished: compose.ImageBuild.JobFinished,
			Result:   &osbuild.Result{},
		}, nil
	}

	// All jobs are "osbuild" jobs.
	var result worker.OSBuildJobResult
	jobInfo, err := api.workers.OSBuildJobInfo(jobId, &result)
	if err != nil {
		return nil, err
	}

	return &composeStatus{
		State:    composeStateFromJobStatus(jobInfo.JobStatus, &result),
		Queued:   jobInfo.JobStatus.Queued,
		Started:  jobInfo.JobStatus.Started,
		Finished: jobInfo.JobStatus.Finished,
		Result:   result.OSBuildOutput,
	}, nil
}

// Opens the image file for `compose`. This asks the worker server for the
// artifact first, and then falls back to looking in
// `{outputs}/{composeId}/{imageBuildId}` for backwards compatibility.
func (api *API) openImageFile(composeId uuid.UUID, compose store.Compose) (io.Reader, int64, error) {
	name := compose.ImageBuild.ImageType.Filename()

	reader, size, err := api.workers.JobArtifact(compose.ImageBuild.JobID, name)
	if err != nil {
		if api.compatOutputDir == "" || err != jobqueue.ErrNotExist {
			return nil, 0, err
		}

		p := path.Join(api.compatOutputDir, composeId.String(), "0", name)
		f, err := os.Open(p)
		if err != nil {
			return nil, 0, err
		}

		info, err := f.Stat()
		if err != nil {
			return nil, 0, err
		}

		reader = f
		size = info.Size()
	}

	return reader, size, nil
}

// isImageTypeAllowed checks the given ImageType and Distro names against
// the distro-specific ImageType Denylist provided to the API from configuration.
// If the given ImageType is not allowed the method returns an `false`.
// Otherwise `true` is returned.
func (api *API) isImageTypeAllowed(distroName, imageType string) (bool, error) {
	for deniedDistro, deniedImgTypes := range api.distrosImageTypeDenylist {
		deniedDistroPattern, err := glob.Compile(deniedDistro)
		if err != nil {
			// the bool return value here does not have any real meaning
			return true, err
		}
		if deniedDistroPattern.Match(distroName) {
			for _, deniedImgType := range deniedImgTypes {
				deniedImageTypePattern, err := glob.Compile(deniedImgType)
				if err != nil {
					// the bool return value here does not have any real meaning
					return true, err
				}

				if deniedImageTypePattern.Match(imageType) {
					return false, nil
				}
			}
		}
	}

	return true, nil
}

// getImageType returns the ImageType for the selected distro
// This is necessary because different distros support different image types, and the image
// type may have a different package set than other distros.
func (api *API) getImageType(distroName, imageType, archName string) (distro.ImageType, error) {
	imgAllowed, err := api.isImageTypeAllowed(distroName, imageType)
	if err != nil {
		return nil, fmt.Errorf("error while checking if image type is allowed: %v", err)
	}
	if !imgAllowed {
		return nil, fmt.Errorf("image type %q for distro %q is denied by configuration", imageType, distroName)
	}

	distro := api.getDistro(distroName, archName)
	if distro == nil {
		return nil, fmt.Errorf("GetDistro - unknown distribution: %s", distroName)
	}

	arch, err := distro.GetArch(archName)
	if err != nil {
		return nil, err
	}
	return arch.GetImageType(imageType)
}

func (api *API) parseDistro(query url.Values, arch string) (string, error) {
	if distro := query.Get("distro"); distro != "" {
		if common.IsStringInSortedSlice(api.validDistros(arch), distro) {
			return distro, nil
		}
		return "", errors_package.New("Invalid distro: " + distro)
	}
	return api.hostDistroName, nil
}

// getDistro returns the named distro or nil
// It excludes unsupported distros by first checking the api.distros list
func (api *API) getDistro(name, arch string) distro.Distro {
	if !common.IsStringInSortedSlice(api.validDistros(arch), name) {
		return nil
	}
	return api.distroFactory.GetDistro(name)
}

func verifyRequestVersion(writer http.ResponseWriter, params httprouter.Params, minVersion uint) bool {
	versionString := params.ByName("version")

	version, err := strconv.ParseUint(versionString, 10, 0)

	var MaxApiVersion uint = 1

	if err != nil || uint(version) < minVersion || uint(version) > MaxApiVersion {
		notFoundHandler(writer, nil)
		return false
	}

	return true
}

func isRequestVersionAtLeast(params httprouter.Params, minVersion uint) bool {
	versionString := params.ByName("version")

	version, err := strconv.ParseUint(versionString, 10, 0)

	common.PanicOnError(err)

	return uint(version) >= minVersion
}

func methodNotAllowedHandler(writer http.ResponseWriter, request *http.Request) {
	writer.WriteHeader(http.StatusMethodNotAllowed)
}

func notFoundHandler(writer http.ResponseWriter, request *http.Request) {
	errors := responseError{
		Code: http.StatusNotFound,
		ID:   "HTTPError",
		Msg:  "Not Found",
	}
	statusResponseError(writer, http.StatusNotFound, errors)
}

func notImplementedHandler(writer http.ResponseWriter, httpRequest *http.Request, _ httprouter.Params) {
	writer.WriteHeader(http.StatusNotImplemented)
}

func statusResponseOK(writer http.ResponseWriter) {
	type reply struct {
		Status bool `json:"status"`
	}

	writer.WriteHeader(http.StatusOK)
	err := json.NewEncoder(writer).Encode(reply{true})
	common.PanicOnError(err)
}

type responseError struct {
	Code int    `json:"code,omitempty"`
	ID   string `json:"id"`
	Msg  string `json:"msg"`
}

// verifyStringsWithRegex checks a slice of strings against a regex of allowed characters
// it writes the InvalidChars error to the writer and returns false if any of them fail the check
// It will also return an error if the string is empty
func verifyStringsWithRegex(writer http.ResponseWriter, strings []string, re *regexp.Regexp) bool {
	for _, s := range strings {
		if len(s) > 0 && re.MatchString(s) {
			continue
		}
		errors := responseError{
			ID:  "InvalidChars",
			Msg: "Invalid characters in API path",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return false
	}
	return true
}

func statusResponseError(writer http.ResponseWriter, code int, errors ...responseError) {
	type reply struct {
		Status bool            `json:"status"`
		Errors []responseError `json:"errors,omitempty"`
	}

	writer.WriteHeader(code)
	err := json.NewEncoder(writer).Encode(reply{false, errors})
	common.PanicOnError(err)
}

func (api *API) statusHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
	type reply struct {
		API           string   `json:"api"`
		DBSupported   bool     `json:"db_supported"`
		DBVersion     string   `json:"db_version"`
		SchemaVersion string   `json:"schema_version"`
		Backend       string   `json:"backend"`
		Build         string   `json:"build"`
		Messages      []string `json:"msgs"`
	}

	err := json.NewEncoder(writer).Encode(reply{
		API:           "1",
		DBSupported:   true,
		DBVersion:     "0",
		SchemaVersion: "0",
		Backend:       "osbuild-composer",
		Build:         common.BuildVersion(),
		Messages:      make([]string, 0),
	})
	common.PanicOnError(err)
}

func (api *API) sourceListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type reply struct {
		Sources []string `json:"sources"`
	}

	// The v0 API used the repo Name, a descriptive string, as the key
	// In the v1 API this was changed to separate the Name and the Id (a short identifier)
	var names []string
	if isRequestVersionAtLeast(params, 1) {
		names = api.store.ListSourcesById()
	} else {
		names = api.store.ListSourcesByName()
	}
	names = append(names, api.systemRepoNames()...)

	err := json.NewEncoder(writer).Encode(reply{
		Sources: names,
	})
	common.PanicOnError(err)
}

func (api *API) sourceEmptyInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	errors := responseError{
		Code: http.StatusNotFound,
		ID:   "HTTPError",
		Msg:  "Not Found",
	}
	statusResponseError(writer, http.StatusNotFound, errors)
}

// getSourceConfigs retrieves the list of sources from the system repos an store
// Returning a list of store.SourceConfig entries indexed by the id of the source
func (api *API) getSourceConfigs(params httprouter.Params) (map[string]store.SourceConfig, []responseError) {
	names := params.ByName("sources")

	sources := map[string]store.SourceConfig{}
	errors := []responseError{}

	repos, err := api.repoRegistry.ReposByArchName(api.hostDistroName, api.hostArch, false)
	if err != nil {
		error := responseError{
			ID:  "InternalError",
			Msg: fmt.Sprintf("error while getting system repos: %v", err),
		}
		errors = append(errors, error)
	}

	// if names is "*" we want all sources
	if names == "*" {
		sources = api.store.GetAllSourcesByID()
		for _, repo := range repos {
			sources[repo.Name] = store.NewSourceConfig(repo, true)
		}
	} else {
		for _, name := range strings.Split(names, ",") {
			// check if the source is one of the base repos
			found := false
			for _, repo := range repos {
				if name == repo.Name {
					sources[repo.Name] = store.NewSourceConfig(repo, true)
					found = true
					break
				}
			}
			if found {
				continue
			}
			// check if the source is in the store
			if source := api.store.GetSource(name); source != nil {
				sources[name] = *source
			} else {
				error := responseError{
					ID:  "UnknownSource",
					Msg: fmt.Sprintf("%s is not a valid source", name),
				}
				errors = append(errors, error)
			}
		}
	}

	return sources, errors
}

// sourceInfoHandler routes the call to the correct API version handler
func (api *API) sourceInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	versionString := params.ByName("version")
	version, err := strconv.ParseUint(versionString, 10, 0)
	if err != nil {
		notFoundHandler(writer, nil)
		return
	}
	switch version {
	case 0:
		api.sourceInfoHandlerV0(writer, request, params)
	case 1:
		api.sourceInfoHandlerV1(writer, request, params)
	default:
		notFoundHandler(writer, nil)
	}
}

// sourceInfoHandlerV0 handles the API v0 response
func (api *API) sourceInfoHandlerV0(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	sources, errors := api.getSourceConfigs(params)

	// V0 responses use the source name as the key
	v0Sources := make(map[string]SourceConfigV0, len(sources))
	for _, s := range sources {
		v0Sources[s.Name] = NewSourceConfigV0(s)
	}

	q, err := url.ParseQuery(request.URL.RawQuery)
	if err != nil {
		errors := responseError{
			ID:  "InvalidChars",
			Msg: fmt.Sprintf("invalid query string: %v", err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	format := q.Get("format")
	if format == "json" || format == "" {
		err := json.NewEncoder(writer).Encode(SourceInfoResponseV0{
			Sources: v0Sources,
			Errors:  errors,
		})
		common.PanicOnError(err)
	} else if format == "toml" {
		encoder := toml.NewEncoder(writer)
		encoder.Indent = ""
		err := encoder.Encode(sources)
		common.PanicOnError(err)
	} else {
		errors := responseError{
			ID:  "InvalidChars",
			Msg: fmt.Sprintf("invalid format parameter: %s", format),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
	}
}

// sourceInfoHandlerV1 handles the API v0 response
func (api *API) sourceInfoHandlerV1(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	sources, errors := api.getSourceConfigs(params)

	// V1 responses use the source id as the key
	v1Sources := make(map[string]SourceConfigV1, len(sources))
	for id, s := range sources {
		v1Sources[id] = NewSourceConfigV1(id, s)
	}

	q, err := url.ParseQuery(request.URL.RawQuery)
	if err != nil {
		errors := responseError{
			ID:  "InvalidChars",
			Msg: fmt.Sprintf("invalid query string: %v", err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	format := q.Get("format")
	if format == "json" || format == "" {
		err := json.NewEncoder(writer).Encode(SourceInfoResponseV1{
			Sources: v1Sources,
			Errors:  errors,
		})
		common.PanicOnError(err)
	} else if format == "toml" {
		encoder := toml.NewEncoder(writer)
		encoder.Indent = ""
		err := encoder.Encode(sources)
		common.PanicOnError(err)
	} else {
		errors := responseError{
			ID:  "InvalidChars",
			Msg: fmt.Sprintf("invalid format parameter: %s", format),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
	}
}

// DecodeSourceConfigV0 parses a request.Body into a SourceConfigV0
func DecodeSourceConfigV0(body io.Reader, contentType string) (source SourceConfigV0, err error) {
	if contentType == "application/json" {
		err = json.NewDecoder(body).Decode(&source)
	} else if contentType == "text/x-toml" {
		// Read all of body in case it needs to be parsed twice
		var data []byte
		data, err = io.ReadAll(body)
		if err != nil {
			return source, err
		}

		// First try to parse [ID]\n...SOURCE... form
		var srcMap map[string]SourceConfigV0
		err = toml.Unmarshal(data, &srcMap)
		if err != nil {
			// Failed, parse it again without [ID] mapping
			err = toml.Unmarshal(data, &source)
		} else {
			// It is possible more than 1 source could be posted. We only use the first, after sorting
			keys := make([]string, 0, len(srcMap))
			for k := range srcMap {
				keys = append(keys, k)
			}
			sort.Strings(keys)
			source = srcMap[keys[0]]
		}
	} else {
		err = errors_package.New("blueprint must be in json or toml format")
	}
	return source, err
}

// DecodeSourceConfigV1 parses a request.Body into a SourceConfigV1
func DecodeSourceConfigV1(body io.Reader, contentType string) (source SourceConfigV1, err error) {
	if contentType == "application/json" {
		err = json.NewDecoder(body).Decode(&source)
	} else if contentType == "text/x-toml" {
		// Read all of body in case it needs to be parsed twice
		var data []byte
		data, err = io.ReadAll(body)
		if err != nil {
			return source, err
		}

		// First try to parse [ID]\n...SOURCE... form
		var srcMap map[string]SourceConfigV1
		err = toml.Unmarshal(data, &srcMap)
		if err != nil {
			// Failed, parse it without [ID]
			err = toml.Unmarshal(data, &source)
		} else {
			// It is possible more than 1 source could be posted. We only use the first, after sorting
			keys := make([]string, 0, len(srcMap))
			for k := range srcMap {
				keys = append(keys, k)
			}
			sort.Strings(keys)
			source = srcMap[keys[0]]

			// If no id was set, use the ID from the map
			if len(source.ID) == 0 {
				source.ID = keys[0]
			}
		}
	} else {
		err = errors_package.New("blueprint must be in json or toml format")
	}

	if err == nil && len(source.GetKey()) == 0 {
		err = errors_package.New("'id' field is missing from request")
	}

	return source, err
}

func (api *API) sourceNewHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) { // TODO: version 1 API
		return
	}

	contentType := request.Header["Content-Type"]
	if len(contentType) == 0 {
		errors := responseError{
			ID:  "HTTPError",
			Msg: "malformed Content-Type header",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	if request.ContentLength == 0 {
		errors := responseError{
			ID:  "ProjectsError",
			Msg: "Missing source",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	var source SourceConfig
	var err error
	if isRequestVersionAtLeast(params, 1) {
		source, err = DecodeSourceConfigV1(request.Body, contentType[0])
	} else {
		source, err = DecodeSourceConfigV0(request.Body, contentType[0])
	}

	// Basic check of the source, should at least have a name and type
	if err == nil {
		if len(source.GetName()) == 0 {
			err = errors_package.New("'name' field is missing from request")
		} else if len(source.GetType()) == 0 {
			err = errors_package.New("'type' field is missing from request")
		} else if len(source.SourceConfig().URL) == 0 {
			err = errors_package.New("'url' field is missing from request")
		}
	}
	if err != nil {
		errors := responseError{
			ID:  "ProjectsError",
			Msg: "Problem parsing POST body: " + err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	// If there is a list of distros, check to make sure they are valid
	invalid := []string{}
	for _, d := range source.SourceConfig().Distros {
		if !common.IsStringInSortedSlice(api.validDistros(api.hostArch), d) {
			invalid = append(invalid, d)
		}
	}
	if len(invalid) > 0 {
		errors := responseError{
			ID:  "ProjectsError",
			Msg: "Invalid distributions: " + strings.Join(invalid, ","),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	// Is there an existing System Repo using this id?
	for _, n := range api.systemRepoNames() {
		if n == source.GetKey() {
			// Users cannot replace system repos
			errors := responseError{
				ID:  "SystemSource",
				Msg: fmt.Sprintf("%s is a system source, it cannot be changed.", source.GetKey()),
			}
			statusResponseError(writer, http.StatusBadRequest, errors)
			return
		}
	}

	api.store.PushSource(source.GetKey(), source.SourceConfig())

	statusResponseOK(writer)
}

func (api *API) sourceDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	name := strings.Split(params.ByName("source"), ",")

	if name[0] == "/" {
		errors := responseError{
			Code: http.StatusNotFound,
			ID:   "HTTPError",
			Msg:  "Not Found",
		}
		statusResponseError(writer, http.StatusNotFound, errors)
		return
	}

	// Check for system repos and return an error
	for _, id := range api.systemRepoNames() {
		if id == name[0][1:] {
			errors := responseError{
				ID:  "SystemSource",
				Msg: id + " is a system source, it cannot be deleted.",
			}
			statusResponseError(writer, http.StatusBadRequest, errors)
			return
		}
	}

	// Return an error for unknown sources
	s := api.store.GetSource(name[0][1:])
	if s == nil {
		errors := responseError{
			ID:  "UnknownSource",
			Msg: name[0][1:] + " is not a valid source.",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	// Only delete the first name, which will have a / at the start because of the /*source route
	if isRequestVersionAtLeast(params, 1) {
		api.store.DeleteSourceByID(name[0][1:])
	} else {
		api.store.DeleteSourceByName(name[0][1:])
	}

	statusResponseOK(writer)
}

func (api *API) modulesListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type module struct {
		Name      string `json:"name"`
		GroupType string `json:"group_type"`
	}

	type reply struct {
		Total   uint     `json:"total"`
		Offset  uint     `json:"offset"`
		Limit   uint     `json:"limit"`
		Modules []module `json:"modules"`
	}

	offset, limit, err := parseOffsetAndLimit(request.URL.Query())
	if err != nil {
		errors := responseError{
			ID:  "BadLimitOrOffset",
			Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
		}

		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	modulesParam := params.ByName("modules")

	archName := api.hostArch
	if queryArch := request.URL.Query().Get("arch"); queryArch != "" {
		archName = queryArch
	}

	// Optional distro parameter
	// If it is empty it will return api.hostDistroName
	distroName, err := api.parseDistro(request.URL.Query(), archName)
	if err != nil {
		errors := responseError{
			ID:  "DistroError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	var names []string
	if modulesParam != "" && modulesParam != "/" {
		// we have modules for search

		// remove leading /
		modulesParam = modulesParam[1:]

		names = strings.Split(modulesParam, ",")
	}
	packages, err := api.fetchPackageList(distroName, archName, names)

	if err != nil {
		errors := responseError{
			ID:  "ModulesError",
			Msg: fmt.Sprintf("msg: %s", err.Error()),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	if len(packages) == 0 {
		errors := responseError{
			ID:  "UnknownModule",
			Msg: "No packages have been found.",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	packageInfos := packages.ToPackageInfos()

	total := uint(len(packageInfos))
	start := min(offset, total)
	n := min(limit, total-start)

	modules := make([]module, n)
	for i := uint(0); i < n; i++ {
		modules[i] = module{packageInfos[start+i].Name, "rpm"}
	}

	err = json.NewEncoder(writer).Encode(reply{
		Total:   total,
		Offset:  offset,
		Limit:   limit,
		Modules: modules,
	})
	common.PanicOnError(err)
}

func (api *API) projectsListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type reply struct {
		Total    uint                `json:"total"`
		Offset   uint                `json:"offset"`
		Limit    uint                `json:"limit"`
		Projects []rpmmd.PackageInfo `json:"projects"`
	}

	offset, limit, err := parseOffsetAndLimit(request.URL.Query())
	if err != nil {
		errors := responseError{
			ID:  "BadLimitOrOffset",
			Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	archName := api.hostArch
	if queryArch := request.URL.Query().Get("arch"); queryArch != "" {
		archName = queryArch
	}

	// Optional distro parameter
	// If it is empty it will return api.hostDistroName
	distroName, err := api.parseDistro(request.URL.Query(), archName)
	if err != nil {
		errors := responseError{
			ID:  "DistroError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}
	availablePackages, err := api.fetchPackageList(distroName, archName, []string{})

	if err != nil {
		errors := responseError{
			ID:  "ProjectsError",
			Msg: fmt.Sprintf("msg: %s", err.Error()),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	packageInfos := availablePackages.ToPackageInfos()

	total := uint(len(packageInfos))
	start := min(offset, total)
	n := min(limit, total-start)

	packages := make([]rpmmd.PackageInfo, n)
	for i := uint(0); i < n; i++ {
		packages[i] = packageInfos[start+i]
	}

	err = json.NewEncoder(writer).Encode(reply{
		Total:    total,
		Offset:   offset,
		Limit:    limit,
		Projects: packages,
	})
	common.PanicOnError(err)
}

func (api *API) modulesInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type projectsReply struct {
		Projects []rpmmd.PackageInfo `json:"projects"`
	}
	type modulesReply struct {
		Modules []rpmmd.PackageInfo `json:"modules"`
	}

	// handle both projects/info and modules/info, the latter includes dependencies
	modulesRequested := strings.HasPrefix(request.URL.Path, "/api/v0/modules") ||
		strings.HasPrefix(request.URL.Path, "/api/v1/modules")

	var errorId, unknownErrorId string
	if modulesRequested {
		errorId = "ModulesError"
		unknownErrorId = "UnknownModule"
	} else {
		errorId = "ProjectsError"
		unknownErrorId = "UnknownProject"
	}

	modules := params.ByName("modules")

	if modules == "" || modules == "/" {
		errors := responseError{
			ID:  unknownErrorId,
			Msg: "No packages specified.",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	// remove leading /
	modules = modules[1:]

	names := strings.Split(modules, ",")

	archName := api.hostArch
	if queryArch := request.URL.Query().Get("arch"); queryArch != "" {
		archName = queryArch
	}

	// Optional distro parameter
	// If it is empty it will return api.hostDistroName
	distroName, err := api.parseDistro(request.URL.Query(), archName)
	if err != nil {
		errors := responseError{
			ID:  "DistroError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}
	foundPackages, err := api.fetchPackageList(distroName, archName, names)

	if err != nil {
		errors := responseError{
			ID:  errorId,
			Msg: fmt.Sprintf("msg: %s", err.Error()),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	if len(foundPackages) == 0 {
		errors := responseError{
			ID:  unknownErrorId,
			Msg: "No packages have been found.",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	packageInfos := foundPackages.ToPackageInfos()

	if modulesRequested {
		repos, err := api.allRepositories(distroName, archName)
		if err != nil {
			errors := responseError{
				ID:  "InternalError",
				Msg: fmt.Sprintf("error while getting system repos: %v", err),
			}
			statusResponseError(writer, http.StatusBadRequest, errors)
			return
		}
		d := api.getDistro(distroName, archName)
		if d == nil {
			errors := responseError{
				ID:  "DistroError",
				Msg: fmt.Sprintf("Unknown distribution: %s", distroName),
			}
			statusResponseError(writer, http.StatusBadRequest, errors)
			return
		}

		solver := api.solver.NewWithConfig(d.ModulePlatformID(), d.Releasever(), archName, d.Name())
		for i := range packageInfos {
			pkgName := packageInfos[i].Name
			solved, err := solver.Depsolve([]rpmmd.PackageSet{{Include: []string{pkgName}, Repositories: repos}})
			if err != nil {
				errors := responseError{
					ID:  errorId,
					Msg: fmt.Sprintf("Cannot depsolve package %s: %s", packageInfos[i].Name, err.Error()),
				}
				statusResponseError(writer, http.StatusBadRequest, errors)
				return
			}
			packageInfos[i].Dependencies = solved
		}
		if err := solver.CleanCache(); err != nil {
			// log and ignore
			log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
		}
	}

	if modulesRequested {
		err = json.NewEncoder(writer).Encode(modulesReply{packageInfos})
	} else {
		err = json.NewEncoder(writer).Encode(projectsReply{packageInfos})
	}
	common.PanicOnError(err)
}

func (api *API) projectsDepsolveHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type reply struct {
		Projects []rpmmd.PackageSpec `json:"projects"`
	}

	projects := params.ByName("projects")

	if projects == "" || projects == "/" {
		errors := responseError{
			ID:  "UnknownProject",
			Msg: "No packages specified.",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	// remove leading /
	projects = projects[1:]
	names := strings.Split(projects, ",")

	archName := api.hostArch
	if queryArch := request.URL.Query().Get("arch"); queryArch != "" {
		archName = queryArch
	}

	// Optional distro parameter
	// If it is empty it will return api.hostDistroName
	distroName, err := api.parseDistro(request.URL.Query(), archName)
	if err != nil {
		errors := responseError{
			ID:  "DistroError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	d := api.getDistro(distroName, archName)
	if d == nil {
		errors := responseError{
			ID:  "DistroError",
			Msg: fmt.Sprintf("Unknown distribution: %s", distroName),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	repos, err := api.allRepositories(distroName, archName)
	if err != nil {
		errors := responseError{
			ID:  "ProjectsError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	solver := api.solver.NewWithConfig(d.ModulePlatformID(), d.Releasever(), archName, d.Name())
	deps, err := solver.Depsolve([]rpmmd.PackageSet{{Include: names, Repositories: repos}})
	if err != nil {
		errors := responseError{
			ID:  "ProjectsError",
			Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}
	if err := solver.CleanCache(); err != nil {
		// log and ignore
		log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
	}
	err = json.NewEncoder(writer).Encode(reply{Projects: deps})
	common.PanicOnError(err)
}

func (api *API) blueprintsListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type reply struct {
		Total      uint     `json:"total"`
		Offset     uint     `json:"offset"`
		Limit      uint     `json:"limit"`
		Blueprints []string `json:"blueprints"`
	}

	offset, limit, err := parseOffsetAndLimit(request.URL.Query())
	if err != nil {
		errors := responseError{
			ID:  "BadLimitOrOffset",
			Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	names := api.store.ListBlueprints()
	total := uint(len(names))
	offset = min(offset, total)
	limit = min(limit, total-offset)

	err = json.NewEncoder(writer).Encode(reply{
		Total:      total,
		Offset:     offset,
		Limit:      limit,
		Blueprints: names[offset : offset+limit],
	})
	common.PanicOnError(err)
}

func (api *API) blueprintsInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type change struct {
		Changed bool   `json:"changed"`
		Name    string `json:"name"`
	}
	type reply struct {
		Blueprints []blueprint.Blueprint `json:"blueprints"`
		Changes    []change              `json:"changes"`
		Errors     []responseError       `json:"errors"`
	}

	names := strings.Split(params.ByName("blueprints"), ",")
	if names[0] == "/" {
		errors := responseError{
			Code: http.StatusNotFound,
			ID:   "HTTPError",
			Msg:  "Not Found",
		}
		statusResponseError(writer, http.StatusNotFound, errors)
		return
	}

	// Remove the leading / from the first entry (check above ensures it is not just a /
	names[0] = names[0][1:]

	if !verifyStringsWithRegex(writer, names, ValidBlueprintName) {
		return
	}

	query, err := url.ParseQuery(request.URL.RawQuery)
	if err != nil {
		errors := responseError{
			ID:  "InvalidChars",
			Msg: fmt.Sprintf("invalid query string: %v", err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	blueprints := []blueprint.Blueprint{}
	changes := []change{}
	blueprintErrors := []responseError{}

	for _, name := range names {
		blueprint, changed := api.store.GetBlueprint(name)
		if blueprint == nil {
			blueprintErrors = append(blueprintErrors, responseError{
				ID:  "UnknownBlueprint",
				Msg: fmt.Sprintf("%s: ", name),
			})
			continue
		}
		blueprints = append(blueprints, *blueprint)
		changes = append(changes, change{changed, blueprint.Name})
	}

	format := query.Get("format")
	if format == "json" || format == "" {
		err := json.NewEncoder(writer).Encode(reply{
			Blueprints: blueprints,
			Changes:    changes,
			Errors:     blueprintErrors,
		})
		common.PanicOnError(err)
	} else if format == "toml" {
		// lorax concatenates multiple blueprints with `\n\n` here,
		// which is never useful. Deviate by only returning the first
		// blueprint.
		if len(blueprints) > 1 {
			errors := responseError{
				ID:  "HTTPError",
				Msg: "toml format only supported when requesting one blueprint",
			}
			statusResponseError(writer, http.StatusBadRequest, errors)
			return
		}
		if len(blueprintErrors) > 0 {
			statusResponseError(writer, http.StatusBadRequest, blueprintErrors...)
			return
		}
		encoder := toml.NewEncoder(writer)
		encoder.Indent = ""
		err := encoder.Encode(blueprints[0])
		common.PanicOnError(err)
	} else {
		errors := responseError{
			ID:  "InvalidChars",
			Msg: fmt.Sprintf("invalid format parameter: %s", format),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}
}

func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type entry struct {
		Blueprint    blueprint.Blueprint `json:"blueprint"`
		Dependencies []rpmmd.PackageSpec `json:"dependencies"`
	}
	type reply struct {
		Blueprints []entry         `json:"blueprints"`
		Errors     []responseError `json:"errors"`
	}

	names := strings.Split(params.ByName("blueprints"), ",")
	if names[0] == "/" {
		errors := responseError{
			Code: http.StatusNotFound,
			ID:   "HTTPError",
			Msg:  "Not Found",
		}
		statusResponseError(writer, http.StatusNotFound, errors)
		return
	}

	// Remove the leading / from the first entry (check above ensures it is not just a /
	names[0] = names[0][1:]

	if !verifyStringsWithRegex(writer, names, ValidBlueprintName) {
		return
	}

	blueprints := []entry{}
	blueprintsErrors := []responseError{}
	for _, name := range names {
		blueprint, _ := api.store.GetBlueprint(name)
		if blueprint == nil {
			blueprintsErrors = append(blueprintsErrors, responseError{
				ID:  "UnknownBlueprint",
				Msg: fmt.Sprintf("%s: blueprint not found", name),
			})
			continue
		}

		dependencies, err := api.depsolveBlueprint(*blueprint)

		if err != nil {
			blueprintsErrors = append(blueprintsErrors, responseError{
				ID:  "BlueprintsError",
				Msg: fmt.Sprintf("%s: %s", name, err.Error()),
			})
			dependencies = []rpmmd.PackageSpec{}
		}

		blueprints = append(blueprints, entry{*blueprint, dependencies})
	}

	err := json.NewEncoder(writer).Encode(reply{
		Blueprints: blueprints,
		Errors:     blueprintsErrors,
	})
	common.PanicOnError(err)
}

// expandBlueprintGlobs will expand package name globs and versions using the depsolve results
// The result is a sorted list of Package structs with the full package name and version
// It will return an error if it cannot find a non-glob package name in the dependency list
func expandBlueprintGlobs(dependencies []rpmmd.PackageSpec, packages []blueprint.Package) ([]blueprint.Package, error) {
	newPackages := make(map[string]blueprint.Package)

	for _, pkg := range packages {
		if !strings.ContainsAny(pkg.Name, "*?") {
			// No glob characters, find an exact match in dependencies
			i := sort.Search(len(dependencies), func(i int) bool {
				return dependencies[i].Name >= pkg.Name
			})
			if i >= len(dependencies) || dependencies[i].Name != pkg.Name {
				// Packages should not be missing from the depsolve results
				return nil, fmt.Errorf("%s missing from depsolve results", pkg.Name)
			}
			newPackages[dependencies[i].GetNEVRA()] = blueprint.Package{
				Name:    dependencies[i].Name,
				Version: dependencies[i].GetEVRA(),
			}
		} else {
			// Add all the packages matching the glob
			g, err := glob.Compile(pkg.Name)
			if err != nil {
				return nil, err
			}

			for _, d := range dependencies {
				if g.Match(d.Name) {
					newPackages[d.GetNEVRA()] = blueprint.Package{
						Name:    d.Name,
						Version: d.GetEVRA(),
					}
				}
			}
		}
	}

	// Return a sorted slice of the Packages
	np := make([]blueprint.Package, 0, len(newPackages))
	for _, pkg := range newPackages {
		np = append(np, pkg)
	}
	sort.Slice(np, func(i, j int) bool {
		return np[i].Name < np[j].Name
	})

	return np, nil
}

func (api *API) blueprintsFreezeHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type blueprintFrozen struct {
		Blueprint blueprint.Blueprint `json:"blueprint"`
	}

	type reply struct {
		Blueprints []blueprintFrozen `json:"blueprints"`
		Errors     []responseError   `json:"errors"`
	}

	names := strings.Split(params.ByName("blueprints"), ",")
	if names[0] == "/" {
		errors := responseError{
			Code: http.StatusNotFound,
			ID:   "HTTPError",
			Msg:  "Not Found",
		}
		statusResponseError(writer, http.StatusNotFound, errors)
		return
	}

	// Remove the leading / from the first entry (check above ensures it is not just a /
	names[0] = names[0][1:]

	if !verifyStringsWithRegex(writer, names, ValidBlueprintName) {
		return
	}

	blueprints := []blueprintFrozen{}
	errors := []responseError{}
	for _, name := range names {
		bp, _ := api.store.GetBlueprint(name)
		if bp == nil {
			rerr := responseError{
				ID:  "UnknownBlueprint",
				Msg: fmt.Sprintf("%s: blueprint not found", name),
			}
			errors = append(errors, rerr)
			break
		}
		// Make a copy of the blueprint since we will be replacing the version globs
		blueprint := bp.DeepCopy()
		dependencies, err := api.depsolveBlueprint(blueprint)
		if err != nil {
			rerr := responseError{
				ID:  "BlueprintsError",
				Msg: fmt.Sprintf("%s: %s", name, err.Error()),
			}
			errors = append(errors, rerr)
			break
		}
		// Sort dependencies by Name (names should be unique so no need to sort by EVRA)
		sort.Slice(dependencies, func(i, j int) bool {
			return dependencies[i].Name < dependencies[j].Name
		})

		// Expand any package name globs and set the package version
		newPackages, err := expandBlueprintGlobs(dependencies, blueprint.Packages)
		if err != nil {
			rerr := responseError{
				ID:  "BlueprintsError",
				Msg: fmt.Sprintf("%s: %s", name, err.Error()),
			}
			errors = append(errors, rerr)
			break
		}
		blueprint.Packages = newPackages

		// Expand any module name globs and set the module version
		newModules, err := expandBlueprintGlobs(dependencies, blueprint.Modules)
		if err != nil {
			rerr := responseError{
				ID:  "BlueprintsError",
				Msg: fmt.Sprintf("%s: %s", name, err.Error()),
			}
			errors = append(errors, rerr)
			break
		}
		blueprint.Modules = newModules
		blueprints = append(blueprints, blueprintFrozen{blueprint})
	}

	format := request.URL.Query().Get("format")

	if format == "toml" {
		// lorax concatenates multiple blueprints with `\n\n` here,
		// which is never useful. Deviate by only returning the first
		// blueprint.
		if len(blueprints) == 0 {
			// lorax-composer just outputs an empty string if there were no blueprints
			writer.WriteHeader(http.StatusOK)
			fmt.Fprintf(writer, "")
			return
		} else if len(blueprints) > 1 {
			errors := responseError{
				ID:  "HTTPError",
				Msg: "toml format only supported when requesting one blueprint",
			}
			statusResponseError(writer, http.StatusBadRequest, errors)
			return
		}

		encoder := toml.NewEncoder(writer)
		encoder.Indent = ""
		err := encoder.Encode(blueprints[0].Blueprint)
		common.PanicOnError(err)
	} else {
		err := json.NewEncoder(writer).Encode(reply{
			Blueprints: blueprints,
			Errors:     errors,
		})
		common.PanicOnError(err)
	}
}

func (api *API) blueprintsDiffHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type pack struct {
		Package blueprint.Package `json:"Package"`
	}

	type diff struct {
		New *pack `json:"new"`
		Old *pack `json:"old"`
	}

	type reply struct {
		Diffs []diff `json:"diff"`
	}

	name := params.ByName("blueprint")
	if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
		return
	}

	fromCommit := params.ByName("from")
	if !verifyStringsWithRegex(writer, []string{fromCommit}, ValidBlueprintName) {
		return
	}

	toCommit := params.ByName("to")
	if !verifyStringsWithRegex(writer, []string{toCommit}, ValidBlueprintName) {
		return
	}

	if len(name) == 0 || len(fromCommit) == 0 || len(toCommit) == 0 {
		errors := responseError{
			Code: http.StatusNotFound,
			ID:   "HTTPError",
			Msg:  "Not Found",
		}
		statusResponseError(writer, http.StatusNotFound, errors)
		return
	}
	if fromCommit != "NEWEST" {
		errors := responseError{
			ID:  "UnknownCommit",
			Msg: fmt.Sprintf("ggit-error: revspec '%s' not found (-3)", fromCommit),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}
	if toCommit != "WORKSPACE" {
		errors := responseError{
			ID:  "UnknownCommit",
			Msg: fmt.Sprintf("ggit-error: revspec '%s' not found (-3)", toCommit),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	// Fetch old and new blueprint details from store and return error if not found
	oldBlueprint := api.store.GetBlueprintCommitted(name)
	newBlueprint, _ := api.store.GetBlueprint(name)
	if oldBlueprint == nil || newBlueprint == nil {
		errors := responseError{
			ID:  "UnknownBlueprint",
			Msg: fmt.Sprintf("Unknown blueprint name: %s", name),
		}
		statusResponseError(writer, http.StatusNotFound, errors)
		return
	}

	newSlice := newBlueprint.Packages
	oldMap := make(map[string]blueprint.Package)
	diffs := []diff{}

	for _, oldPackage := range oldBlueprint.Packages {
		oldMap[oldPackage.Name] = oldPackage
	}

	// For each package in new blueprint check if the old one contains it
	for _, newPackage := range newSlice {
		oldPackage, found := oldMap[newPackage.Name]
		// If found remove from old packages map but otherwise create a diff with the added package
		if found {
			delete(oldMap, oldPackage.Name)
			// Create a diff if the versions changed
			if oldPackage.Version != newPackage.Version {
				diffs = append(diffs, diff{Old: &pack{oldPackage}, New: &pack{newPackage}})
			}
		} else {
			diffs = append(diffs, diff{Old: nil, New: &pack{newPackage}})
		}
	}

	// All packages remaining in the old packages map have been removed in the new blueprint so create a diff
	for _, oldPackage := range oldMap {
		diffs = append(diffs, diff{Old: &pack{oldPackage}, New: nil})
	}

	err := json.NewEncoder(writer).Encode(reply{diffs})
	common.PanicOnError(err)
}

// blueprintsChangeHandler returns a specific change to a blueprint
func (api *API) blueprintsChangeHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}
	name := params.ByName("blueprint")
	if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
		return
	}

	commit := params.ByName("commit")
	if !verifyStringsWithRegex(writer, []string{commit}, ValidBlueprintName) {
		return
	}

	bpChange, err := api.store.GetBlueprintChange(name, commit)
	if err != nil {
		errors := responseError{
			ID:  "UnknownCommit",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	bp := bpChange.Blueprint
	if len(bpChange.Blueprint.Name) == 0 {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: fmt.Sprintf("no blueprint found for commit %s", commit),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	query, err := url.ParseQuery(request.URL.RawQuery)
	if err != nil {
		errors := responseError{
			ID:  "InvalidChars",
			Msg: fmt.Sprintf("invalid query string: %v", err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	format := query.Get("format")
	if format == "json" || format == "" {
		err := json.NewEncoder(writer).Encode(bp)
		common.PanicOnError(err)
	} else if format == "toml" {
		encoder := toml.NewEncoder(writer)
		encoder.Indent = ""
		err := encoder.Encode(bp)
		common.PanicOnError(err)
	} else {
		errors := responseError{
			ID:  "InvalidChars",
			Msg: fmt.Sprintf("invalid format parameter: %s", format),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}
}

func (api *API) blueprintsChangesHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type change struct {
		Changes []blueprint.Change `json:"changes"`
		Name    string             `json:"name"`
		Total   int                `json:"total"`
	}

	type reply struct {
		BlueprintsChanges []change        `json:"blueprints"`
		Errors            []responseError `json:"errors"`
		Limit             uint            `json:"limit"`
		Offset            uint            `json:"offset"`
	}

	names := strings.Split(params.ByName("blueprints"), ",")
	if names[0] == "/" {
		errors := responseError{
			Code: http.StatusNotFound,
			ID:   "HTTPError",
			Msg:  "Not Found",
		}
		statusResponseError(writer, http.StatusNotFound, errors)
		return
	}

	// Remove the leading / from the first entry (check above ensures it is not just a /
	names[0] = names[0][1:]

	if !verifyStringsWithRegex(writer, names, ValidBlueprintName) {
		return
	}

	offset, limit, err := parseOffsetAndLimit(request.URL.Query())
	if err != nil {
		errors := responseError{
			ID:  "BadLimitOrOffset",
			Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	allChanges := []change{}
	errors := []responseError{}
	for _, name := range names {
		bpChanges := api.store.GetBlueprintChanges(name)
		// Reverse the changes, newest first
		reversed := make([]blueprint.Change, 0, len(bpChanges))
		for i := len(bpChanges) - 1; i >= 0; i-- {
			reversed = append(reversed, bpChanges[i])
		}
		if bpChanges != nil {
			change := change{
				Changes: reversed,
				Name:    name,
				Total:   len(bpChanges),
			}
			allChanges = append(allChanges, change)
		} else {
			error := responseError{
				ID:  "UnknownBlueprint",
				Msg: name,
			}
			errors = append(errors, error)
		}
	}

	err = json.NewEncoder(writer).Encode(reply{
		BlueprintsChanges: allChanges,
		Errors:            errors,
		Offset:            offset,
		Limit:             limit,
	})
	common.PanicOnError(err)
}

func (api *API) blueprintsNewHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	contentType := request.Header["Content-Type"]
	if len(contentType) == 0 {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: "missing Content-Type header",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	if request.ContentLength == 0 {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: "Missing blueprint",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	var blueprint blueprint.Blueprint
	var err error
	if contentType[0] == "application/json" {
		err = json.NewDecoder(request.Body).Decode(&blueprint)
	} else if contentType[0] == "text/x-toml" {
		_, err = toml.NewDecoder(request.Body).Decode(&blueprint)
	} else {
		err = errors_package.New("blueprint must be in json or toml format")
	}

	if err != nil {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: "400 Bad Request: The browser (or proxy) sent a request that this server could not understand: " + err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	if !verifyStringsWithRegex(writer, []string{blueprint.Name}, ValidBlueprintName) {
		return
	}

	// Check the blueprint's distro to make sure it is valid
	if len(blueprint.Distro) > 0 {
		// NB: For backward compatibility, try to to standardize the distro name,
		// because it may be missing a dot to separate major and minor verion.
		// If it fails, just use the original name.
		if distroStandardized, err := distroidparser.DefaultParser.Standardize(blueprint.Distro); err == nil {
			blueprint.Distro = distroStandardized
		}

		arch := blueprint.Arch
		if arch == "" {
			arch = api.hostArch
		}
		if !common.IsStringInSortedSlice(api.validDistros(arch), blueprint.Distro) {
			errors := responseError{
				ID:  "BlueprintsError",
				Msg: fmt.Sprintf("'%s' is not a valid distribution (architecture '%s')", blueprint.Distro, arch),
			}
			statusResponseError(writer, http.StatusBadRequest, errors)
			return
		}
	}

	// Make sure the blueprint has default values and that the version is valid
	err = blueprint.Initialize()
	if err != nil {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	commitMsg := "Recipe " + blueprint.Name + ", version " + blueprint.Version + " saved."
	err = api.store.PushBlueprint(blueprint, commitMsg)
	if err != nil {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	statusResponseOK(writer)
}

func (api *API) blueprintsWorkspaceHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	contentType := request.Header["Content-Type"]
	if len(contentType) == 0 {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: "missing Content-Type header",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	if request.ContentLength == 0 {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: "Missing blueprint",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	var blueprint blueprint.Blueprint
	var err error
	if contentType[0] == "application/json" {
		err = json.NewDecoder(request.Body).Decode(&blueprint)
	} else if contentType[0] == "text/x-toml" {
		_, err = toml.NewDecoder(request.Body).Decode(&blueprint)
	} else {
		err = errors_package.New("blueprint must be in json or toml format")
	}

	if err != nil {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: "400 Bad Request: The browser (or proxy) sent a request that this server could not understand: " + err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	if !verifyStringsWithRegex(writer, []string{blueprint.Name}, ValidBlueprintName) {
		return
	}

	err = api.store.PushBlueprintToWorkspace(blueprint)
	if err != nil {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	statusResponseOK(writer)
}

func (api *API) blueprintUndoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	name := params.ByName("blueprint")
	if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
		return
	}

	commit := params.ByName("commit")
	if !verifyStringsWithRegex(writer, []string{commit}, ValidBlueprintName) {
		return
	}

	bpChange, err := api.store.GetBlueprintChange(name, commit)
	if err != nil {
		errors := responseError{
			ID:  "UnknownCommit",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	bp := bpChange.Blueprint
	if len(bpChange.Blueprint.Name) == 0 {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: fmt.Sprintf("no blueprint found for commit %s", commit),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	commitMsg := name + ".toml reverted to commit " + commit
	err = api.store.PushBlueprint(bp, commitMsg)
	if err != nil {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}
	statusResponseOK(writer)
}

func (api *API) blueprintDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	name := params.ByName("blueprint")
	if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
		return
	}

	if err := api.store.DeleteBlueprint(name); err != nil {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}
	statusResponseOK(writer)
}

func (api *API) blueprintDeleteWorkspaceHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	name := params.ByName("blueprint")
	if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
		return
	}

	if err := api.store.DeleteBlueprintFromWorkspace(name); err != nil {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}
	statusResponseOK(writer)
}

// blueprintsTagHandler tags the current blueprint commit as a new revision
func (api *API) blueprintsTagHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	name := params.ByName("blueprint")
	if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
		return
	}

	err := api.store.TagBlueprint(name)
	if err != nil {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}
	statusResponseOK(writer)
}

// depsolve handles depsolving package sets required for serializing a manifest for a given distribution.
//
// Distro name is not determined from the provided architecture, but has to be provided explicitly.
// The reason is that a distro name alias may have been used to get the distro object as well as the
// repositories for the depsolving. The actual distro object name may not correspond to the alias.
// Since the solver uses the distro name to namespace cache, it is important to use the same distro
// name as the one used to get the repositories.
func (api *API) depsolve(packageSets map[string][]rpmmd.PackageSet, distroName string, arch distro.Arch) (map[string][]rpmmd.PackageSpec, error) {

	distro := arch.Distro()
	platformID := distro.ModulePlatformID()
	releasever := distro.Releasever()
	solver := api.solver.NewWithConfig(platformID, releasever, arch.Name(), distroName)

	depsolvedSets := make(map[string][]rpmmd.PackageSpec, len(packageSets))

	for name, pkgSet := range packageSets {
		res, err := solver.Depsolve(pkgSet)
		if err != nil {
			return nil, err
		}
		depsolvedSets[name] = res
	}
	if err := solver.CleanCache(); err != nil {
		// log and ignore
		log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
	}
	return depsolvedSets, nil
}

func (api *API) resolveContainers(sourceSpecs map[string][]container.SourceSpec, archName string) (map[string][]container.Spec, error) {

	specs := make(map[string][]container.Spec, len(sourceSpecs))

	// shortcut
	if len(sourceSpecs) == 0 {
		return specs, nil
	}

	// Run one job for each value in the sourceSpecs in order.
	// Currently this should still only be one job, but if containers are added
	// to multiple pipelines at any point, this should work.
	for name, sources := range sourceSpecs {
		job := worker.ContainerResolveJob{
			Arch:  archName,
			Specs: make([]worker.ContainerSpec, len(sources)),
		}

		for i, c := range sources {
			job.Specs[i] = worker.ContainerSpec{
				Source:    c.Source,
				Name:      c.Name,
				TLSVerify: c.TLSVerify,
			}
		}

		jobId, err := api.workers.EnqueueContainerResolveJob(&job, "")

		if err != nil {
			return specs, err
		}

		var result worker.ContainerResolveJobResult

		for {
			jobInfo, err := api.workers.ContainerResolveJobInfo(jobId, &result)

			if err != nil {
				return specs, err
			}

			if result.JobError != nil {
				return specs, errors.New(result.JobError.Reason)
			} else if jobInfo.JobStatus.Canceled {
				return specs, fmt.Errorf("Failed to resolve containers: job cancelled")
			} else if !jobInfo.JobStatus.Finished.IsZero() {
				break
			}

			time.Sleep(time.Millisecond * 250)
		}

		if len(result.Specs) != len(sources) {
			panic("programming error: input / output length don't match")
		}

		specs[name] = make([]container.Spec, len(sources))
		for i, s := range result.Specs {
			specs[name][i].Source = s.Source
			specs[name][i].Digest = s.Digest
			specs[name][i].LocalName = s.Name
			specs[name][i].TLSVerify = s.TLSVerify
			specs[name][i].ImageID = s.ImageID
			specs[name][i].ListDigest = s.ListDigest
		}
	}

	return specs, nil
}

func (api *API) resolveOSTreeCommits(sourceSpecs map[string][]ostree.SourceSpec, test bool) (map[string][]ostree.CommitSpec, error) {
	commitSpecs := make(map[string][]ostree.CommitSpec, len(sourceSpecs))
	for name, sources := range sourceSpecs {
		commits := make([]ostree.CommitSpec, len(sources))
		for idx, source := range sources {
			if test {
				checksum := fmt.Sprintf("%x", sha256.Sum256([]byte(source.URL+source.Ref)))
				commits[idx] = ostree.CommitSpec{
					Ref:      source.Ref,
					URL:      source.URL,
					Checksum: checksum,
				}
			} else {
				commit, err := ostree.Resolve(source)
				if err != nil {
					return nil, err
				}
				commits[idx] = commit
			}
		}
		commitSpecs[name] = commits
	}
	return commitSpecs, nil
}

// Schedule new compose by first translating the appropriate blueprint into a pipeline and then
// pushing it into the channel for waiting builds.
func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	// https://weldr.io/lorax/pylorax.api.html#pylorax.api.v0.v0_compose_start
	type ComposeRequest struct {
		BlueprintName string              `json:"blueprint_name"`
		ComposeType   string              `json:"compose_type"`
		Size          uint64              `json:"size"`
		OSTree        ostree.ImageOptions `json:"ostree"`
		Branch        string              `json:"branch"`
		Upload        *uploadRequest      `json:"upload"`
	}
	type ComposeReply struct {
		BuildID  uuid.UUID `json:"build_id"`
		Status   bool      `json:"status"`
		Warnings []string  `json:"warnings"`
	}

	contentType := request.Header["Content-Type"]
	if len(contentType) != 1 || contentType[0] != "application/json" {
		errors := responseError{
			ID:  "MissingPost",
			Msg: "blueprint must be json",
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	var cr ComposeRequest
	err := json.NewDecoder(request.Body).Decode(&cr)
	if err != nil {
		errors := responseError{
			Code: http.StatusNotFound,
			ID:   "HTTPError",
			Msg:  "Not Found",
		}
		statusResponseError(writer, http.StatusNotFound, errors)
		return
	}

	if !verifyStringsWithRegex(writer, []string{cr.BlueprintName}, ValidBlueprintName) {
		return
	}

	bp := api.store.GetBlueprintCommitted(cr.BlueprintName)
	if bp == nil {
		errors := responseError{
			ID:  "UnknownBlueprint",
			Msg: fmt.Sprintf("Unknown blueprint name: %s", cr.BlueprintName),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	distroName := bp.Distro
	if distroName == "" {
		distroName = api.hostDistroName
	}

	archName := bp.Arch
	if archName == "" {
		archName = api.hostArch
	}

	if api.getDistro(distroName, archName) == nil {
		errors := responseError{
			ID:  "DistroError",
			Msg: fmt.Sprintf("Unknown distribution: %s for arch %s", distroName, archName),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	// Get the imageType that corresponds to the distribution selected by the blueprint
	imageType, err := api.getImageType(distroName, cr.ComposeType, archName)
	if err != nil {
		errors := responseError{
			ID:  "ComposeError",
			Msg: fmt.Sprintf("Failed to get compose type %q: %v", cr.ComposeType, err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	composeID := uuid.New()

	var targets []*target.Target
	// Always instruct the worker to upload the artifact back to the server
	workerServerTarget := target.NewWorkerServerTarget()
	workerServerTarget.ImageName = imageType.Filename()
	workerServerTarget.OsbuildArtifact.ExportFilename = imageType.Filename()
	workerServerTarget.OsbuildArtifact.ExportName = imageType.Exports()[0]
	targets = append(targets, workerServerTarget)
	if isRequestVersionAtLeast(params, 1) && cr.Upload != nil {
		t := uploadRequestToTarget(*cr.Upload, imageType)
		targets = append(targets, t)
	}

	// Check for test parameter
	q, err := url.ParseQuery(request.URL.RawQuery)
	if err != nil {
		errors := responseError{
			ID:  "InvalidChars",
			Msg: fmt.Sprintf("invalid query string: %v", err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	var size uint64

	// check if filesytem customizations have been set.
	// if compose size parameter is set, take the larger of
	// the two values
	if minSize := bp.Customizations.GetFilesystemsMinSize(); bp.Customizations != nil && minSize > 0 && minSize > cr.Size {
		size = imageType.Size(minSize)
	} else {
		size = imageType.Size(cr.Size)
	}

	bigSeed, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
	if err != nil {
		panic("cannot generate a manifest seed: " + err.Error())
	}
	seed := bigSeed.Int64()

	// Get the partitioning mode
	pm, err := bp.Customizations.GetPartitioningMode()
	if err != nil {
		errors := responseError{
			ID:  "BlueprintsError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	options := distro.ImageOptions{
		Size:             size,
		OSTree:           &cr.OSTree,
		PartitioningMode: pm,
	}
	options.Facts = &facts.ImageOptions{
		APIType: facts.WELDR_APITYPE,
	}

	imageRepos, err := api.allRepositoriesByImageType(distroName, imageType)
	if err != nil {
		errors := responseError{
			ID:  "InternalError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}

	ibp := blueprint.Convert(*bp)
	manifest, warnings, err := imageType.Manifest(&ibp, options, imageRepos, seed)
	if err != nil {
		errors := responseError{
			ID:  "ManifestCreationFailed",
			Msg: fmt.Sprintf("failed to initialize osbuild manifest: %v", err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	packageSets, err := api.depsolve(manifest.GetPackageSetChains(), distroName, imageType.Arch())
	if err != nil {
		errors := responseError{
			ID:  "DepsolveError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}

	containerSpecs, err := api.resolveContainers(manifest.GetContainerSourceSpecs(), archName)
	if err != nil {
		errors := responseError{
			ID:  "ContainerResolveError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}

	testMode := q.Get("test")

	ostreeCommitSpecs, err := api.resolveOSTreeCommits(manifest.GetOSTreeSourceSpecs(), testMode == "1" || testMode == "2")
	if err != nil {
		errors := responseError{
			ID:  "OSTreeOptionsError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	mf, err := manifest.Serialize(packageSets, containerSpecs, ostreeCommitSpecs)
	if err != nil {
		errors := responseError{
			ID:  "ManifestCreationFailed",
			Msg: fmt.Sprintf("failed to serialize osbuild manifest: %v", err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	var packages []rpmmd.PackageSpec
	// TODO: introduce a way to query these from the manifest / image type
	// BUG: installer/container image types will have empty package sets
	if packages = packageSets["packages"]; len(packages) == 0 {
		if packages = packageSets["os"]; len(packages) == 0 {
			packages = packageSets["ostree-tree"]
		}
	}

	workerAvailable, err := api.workers.WorkerAvailableForArch(archName)
	if err != nil {
		log.Println("error when pushing new compose: ", err.Error())
		errors := responseError{
			ID:  "ComposePushErrored",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}
	if !workerAvailable {
		errors := responseError{
			ID:  "ComposePushErrored",
			Msg: fmt.Sprintf("No worker for arch '%s'  available", archName),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	if testMode == "1" {
		// Create a failed compose
		err = api.store.PushTestCompose(composeID, mf, imageType, bp, size, targets, false, packages)
	} else if testMode == "2" {
		// Create a successful compose
		err = api.store.PushTestCompose(composeID, mf, imageType, bp, size, targets, true, packages)
	} else {
		var jobId uuid.UUID
		jobId, err = api.workers.EnqueueOSBuild(archName, &worker.OSBuildJob{
			Manifest: mf,
			Targets:  targets,
			PipelineNames: &worker.PipelineNames{
				Build:   imageType.BuildPipelines(),
				Payload: imageType.PayloadPipelines(),
			},
			ImageBootMode: imageType.BootMode().String(),
		}, "")
		if err == nil {
			err = api.store.PushCompose(composeID, mf, imageType, bp, size, targets, jobId, packages)
		}
	}

	// TODO: we should probably do some kind of blueprint validation in future
	// for now, let's just 500 and bail out
	if err != nil {
		log.Println("error when pushing new compose: ", err.Error())
		errors := responseError{
			ID:  "ComposePushErrored",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}

	err = json.NewEncoder(writer).Encode(ComposeReply{
		BuildID:  composeID,
		Status:   true,
		Warnings: warnings,
	})
	common.PanicOnError(err)
}

func (api *API) composeDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	type composeDeleteStatus struct {
		UUID   uuid.UUID `json:"uuid"`
		Status bool      `json:"status"`
	}

	type composeDeleteError struct {
		ID  string `json:"id"`
		Msg string `json:"msg"`
	}

	uuidsParam := params.ByName("uuids")

	results := []composeDeleteStatus{}
	errors := []composeDeleteError{}
	uuidStrings := strings.Split(uuidsParam, ",")
	for _, uuidString := range uuidStrings {
		id, err := uuid.Parse(uuidString)
		if err != nil {
			errors = append(errors, composeDeleteError{
				"UnknownUUID",
				fmt.Sprintf("%s is not a valid uuid", uuidString),
			})
			continue
		}

		compose, exists := api.store.GetCompose(id)
		if !exists {
			errors = append(errors, composeDeleteError{
				"UnknownUUID",
				fmt.Sprintf("compose %s doesn't exist", id),
			})
			continue
		}

		composeStatus, err := api.getComposeStatus(compose)
		if err != nil {
			errors = append(errors, composeDeleteError{
				"ComposeStatusError",
				fmt.Sprintf("Error getting status of compose %s: %s", id, err),
			})
			continue
		}
		if composeStatus.State != ComposeFinished && composeStatus.State != ComposeFailed {
			errors = append(errors, composeDeleteError{
				"BuildInWrongState",
				fmt.Sprintf("Compose %s is not in FINISHED or FAILED.", id),
			})
			continue
		}

		err = api.store.DeleteCompose(id)
		if err != nil {
			errors = append(errors, composeDeleteError{
				"ComposeError",
				fmt.Sprintf("%s: %s", id, err.Error()),
			})
			continue
		}

		// Delete artifacts from the worker server or — if that doesn't
		// have this job — the compat output dir. Ignore errors,
		// because there's no point of reporting them to the client
		// after the compose itself has already been deleted.
		err = api.workers.DeleteArtifacts(compose.ImageBuild.JobID)
		if err == jobqueue.ErrNotExist && api.compatOutputDir != "" {
			_ = os.RemoveAll(path.Join(api.compatOutputDir, id.String()))
		}

		results = append(results, composeDeleteStatus{id, true})
	}

	reply := struct {
		UUIDs  []composeDeleteStatus `json:"uuids"`
		Errors []composeDeleteError  `json:"errors"`
	}{results, errors}

	err := json.NewEncoder(writer).Encode(reply)
	common.PanicOnError(err)
}

func (api *API) composeCancelHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	uuidString := params.ByName("uuid")
	id, err := uuid.Parse(uuidString)
	if err != nil {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	compose, exists := api.store.GetCompose(id)
	if !exists {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	composeStatus, err := api.getComposeStatus(compose)
	if err != nil {
		errors := responseError{
			ID:  "ComposeStatusError",
			Msg: fmt.Sprintf("Error getting status of compose %s: %s", id, err),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}
	if composeStatus.State != ComposeWaiting && composeStatus.State != ComposeRunning {
		errors := responseError{
			ID:  "BuildInWrongState",
			Msg: fmt.Sprintf("Build %s is not in WAITING or RUNNING.", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	err = api.workers.Cancel(compose.ImageBuild.JobID)
	if err != nil {
		errors := responseError{
			ID:  "InternalServerError",
			Msg: fmt.Sprintf("Internal server error: %v", err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	reply := CancelComposeStatusV0{id, true}
	_ = json.NewEncoder(writer).Encode(reply)
}

func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	archName := api.hostArch
	if queryArch := request.URL.Query().Get("arch"); queryArch != "" {
		archName = queryArch
	}

	// Optional distro parameter
	// If it is empty it will return api.hostDistroName
	distroName, err := api.parseDistro(request.URL.Query(), archName)
	if err != nil {
		errors := responseError{
			ID:  "DistroError",
			Msg: err.Error(),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	d := api.getDistro(distroName, archName)
	if d == nil {
		errors := responseError{
			ID:  "DistroError",
			Msg: fmt.Sprintf("Unknown distribution: %s", distroName),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	// Get the distro specific arch so that we can return the correct list of image types
	arch, err := d.GetArch(archName)
	if err != nil {
		errors := responseError{
			ID:  "DistroError",
			Msg: fmt.Sprintf("Unknown arch: %s", arch),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	type composeType struct {
		Name    string `json:"name"`
		Enabled bool   `json:"enabled"`
	}

	var reply struct {
		Types []composeType `json:"types"`
	}

	for _, format := range arch.ListImageTypes() {
		imgAllowed, err := api.isImageTypeAllowed(distroName, format)
		if err != nil {
			errors := responseError{
				ID:  "InternalError",
				Msg: fmt.Sprintf("Error while checking if image type is allowed: %v", err),
			}
			statusResponseError(writer, http.StatusInternalServerError, errors)
			return
		}
		if !imgAllowed {
			continue
		}
		reply.Types = append(reply.Types, composeType{format, true})
	}

	err = json.NewEncoder(writer).Encode(reply)
	common.PanicOnError(err)
}

func (api *API) composeQueueHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	reply := struct {
		New []*ComposeEntry `json:"new"`
		Run []*ComposeEntry `json:"run"`
	}{[]*ComposeEntry{}, []*ComposeEntry{}}

	includeUploads := isRequestVersionAtLeast(params, 1)

	composes := api.store.GetAllComposes()
	for id, compose := range composes {
		composeStatus, err := api.getComposeStatus(compose)
		if err != nil {
			log.Printf("Error getting status of compose %s: %s", id, err)
			continue
		}
		switch composeStatus.State {
		case ComposeWaiting:
			reply.New = append(reply.New, composeToComposeEntry(id, compose, composeStatus, includeUploads))
		case ComposeRunning:
			reply.Run = append(reply.Run, composeToComposeEntry(id, compose, composeStatus, includeUploads))
		}
	}

	err := json.NewEncoder(writer).Encode(reply)
	common.PanicOnError(err)
}

func (api *API) composeStatusHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	// TODO: lorax has some params: /api/v0/compose/status/<uuids>[?blueprint=<blueprint_name>&status=<compose_status>&type=<compose_type>]
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	var reply struct {
		UUIDs []*ComposeEntry `json:"uuids"`
	}

	uuidsParam := params.ByName("uuids")

	composes := api.store.GetAllComposes()
	uuids := []uuid.UUID{}

	if uuidsParam != "*" {
		for _, uuidString := range strings.Split(uuidsParam, ",") {
			id, err := uuid.Parse(uuidString)
			if err != nil {
				errors := responseError{
					ID:  "UnknownUUID",
					Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
				}
				statusResponseError(writer, http.StatusBadRequest, errors)
				return
			}
			uuids = append(uuids, id)
		}
	} else {
		for id := range composes {
			uuids = append(uuids, id)
		}
	}

	q, err := url.ParseQuery(request.URL.RawQuery)
	if err != nil {
		errors := responseError{
			ID:  "InvalidChars",
			Msg: fmt.Sprintf("invalid query string: %v", err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	filterBlueprint := q.Get("blueprint")
	if len(filterBlueprint) > 0 && !verifyStringsWithRegex(writer, []string{filterBlueprint}, ValidBlueprintName) {
		return
	}

	filterStatus := q.Get("status")
	filterImageType := q.Get("type")

	filteredUUIDs := []uuid.UUID{}
	for _, id := range uuids {
		compose, exists := composes[id]
		if !exists {
			continue
		}
		composeStatus, err := api.getComposeStatus(compose)
		if err != nil {
			log.Printf("Error getting status of compose %s: %s", id, err)
			continue
		}

		if filterBlueprint != "" && compose.Blueprint.Name != filterBlueprint {
			continue
		} else if filterStatus != "" && composeStatus.State.ToString() != filterStatus {
			continue
		} else if filterImageType != "" && compose.ImageBuild.ImageType.Name() != filterImageType {
			continue
		}
		filteredUUIDs = append(filteredUUIDs, id)
	}

	reply.UUIDs = []*ComposeEntry{}
	includeUploads := isRequestVersionAtLeast(params, 1)
	for _, id := range filteredUUIDs {
		if compose, exists := composes[id]; exists {
			composeStatus, err := api.getComposeStatus(compose)
			if err != nil {
				log.Printf("Error getting status of compose %s: %s", id, err)
				continue
			}

			reply.UUIDs = append(reply.UUIDs, composeToComposeEntry(id, compose, composeStatus, includeUploads))
		}
	}
	sortComposeEntries(reply.UUIDs)

	err = json.NewEncoder(writer).Encode(reply)
	common.PanicOnError(err)
}

func (api *API) composeInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	uuidString := params.ByName("uuid")
	id, err := uuid.Parse(uuidString)
	if err != nil {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	compose, exists := api.store.GetCompose(id)

	if !exists {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	var reply struct {
		ID        uuid.UUID            `json:"id"`
		Config    string               `json:"config"`    // anaconda config, let's ignore this field
		Blueprint *blueprint.Blueprint `json:"blueprint"` // blueprint not frozen!
		Commit    string               `json:"commit"`    // empty for now
		Deps      struct {
			Packages []rpmmd.PackageSpec `json:"packages"`
		} `json:"deps"`
		ComposeType string           `json:"compose_type"`
		QueueStatus string           `json:"queue_status"`
		ImageSize   uint64           `json:"image_size"`
		Uploads     []uploadResponse `json:"uploads,omitempty"`
	}

	reply.ID = id
	reply.Blueprint = compose.Blueprint
	// Weldr API assumes only one image build per compose, that's why only the
	// 1st build is considered
	composeStatus, err := api.getComposeStatus(compose)
	if err != nil {
		errors := responseError{
			ID:  "ComposeStatusError",
			Msg: fmt.Sprintf("Error getting status of compose %s: %s", id, err),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}

	reply.ComposeType = compose.ImageBuild.ImageType.Name()
	reply.QueueStatus = composeStatus.State.ToString()
	reply.ImageSize = compose.ImageBuild.Size

	if isRequestVersionAtLeast(params, 1) {
		reply.Uploads = targetsToUploadResponses(compose.ImageBuild.Targets, composeStatus.State)
	}

	// Add package dependencies from the compose
	// This information may not be included for the compose
	dependencies := compose.Packages
	// Sort dependencies by Name (names should be unique so no need to sort by EVRA)
	sort.Slice(dependencies, func(i, j int) bool {
		return dependencies[i].Name < dependencies[j].Name
	})
	reply.Deps.Packages = dependencies
	err = json.NewEncoder(writer).Encode(reply)
	common.PanicOnError(err)
}

func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	uuidString := params.ByName("uuid")
	uuid, err := uuid.Parse(uuidString)
	if err != nil {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	compose, exists := api.store.GetCompose(uuid)
	if !exists {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	composeStatus, err := api.getComposeStatus(compose)
	if err != nil {
		errors := responseError{
			ID:  "ComposeStatusError",
			Msg: fmt.Sprintf("Error getting status of compose %s: %s", uuid, err),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}
	if composeStatus.State != ComposeFinished {
		errors := responseError{
			ID:  "BuildInWrongState",
			Msg: fmt.Sprintf("Build %s is in wrong state: %s", uuidString, composeStatus.State.ToString()),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	imageName := compose.ImageBuild.ImageType.Filename()
	imageMime := compose.ImageBuild.ImageType.MIMEType()

	reader, fileSize, err := api.openImageFile(uuid, compose)
	if err != nil {
		errors := responseError{
			ID:  "InternalServerError",
			Msg: fmt.Sprintf("Error accessing image file for compose %s: %v", uuid, err),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	writer.Header().Set("Content-Disposition", "attachment; filename="+uuid.String()+"-"+imageName)
	writer.Header().Set("Content-Type", imageMime)
	writer.Header().Set("Content-Length", fmt.Sprintf("%d", fileSize))

	_, err = io.Copy(writer, reader)
	common.PanicOnError(err)
}

// composeMetadataHandler returns a tar of the metadata used to compose the requested UUID
func (api *API) composeMetadataHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	uuidString := params.ByName("uuid")
	uuid, err := uuid.Parse(uuidString)
	if err != nil {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	compose, exists := api.store.GetCompose(uuid)
	if !exists {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	composeStatus, err := api.getComposeStatus(compose)
	if err != nil {
		errors := responseError{
			ID:  "ComposeStatusError",
			Msg: fmt.Sprintf("Error getting status of compose %s: %s", uuid, err),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}
	if composeStatus.State != ComposeFinished && composeStatus.State != ComposeFailed {
		errors := responseError{
			ID:  "BuildInWrongState",
			Msg: fmt.Sprintf("Build %s is in wrong state: %s", uuidString, composeStatus.State.ToString()),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	metadata, err := json.Marshal(&compose.ImageBuild.Manifest)
	common.PanicOnError(err)

	writer.Header().Set("Content-Disposition", "attachment; filename="+uuid.String()+"-metadata.tar")
	writer.Header().Set("Content-Type", "application/x-tar")
	// NOTE: Do not set Content-Length, it will use chunked transfer encoding automatically

	tw := tar.NewWriter(writer)
	hdr := &tar.Header{
		Name:    uuid.String() + ".json",
		Mode:    0600,
		Size:    int64(len(metadata)),
		ModTime: time.Now().Truncate(time.Second),
	}
	err = tw.WriteHeader(hdr)
	common.PanicOnError(err)

	_, err = tw.Write(metadata)
	common.PanicOnError(err)

	err = tw.Close()
	common.PanicOnError(err)
}

// composeResultsHandler returns a tar of the metadata, logs, and image from a compose
func (api *API) composeResultsHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	uuidString := params.ByName("uuid")
	uuid, err := uuid.Parse(uuidString)
	if err != nil {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	compose, exists := api.store.GetCompose(uuid)
	if !exists {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	composeStatus, err := api.getComposeStatus(compose)
	if err != nil {
		errors := responseError{
			ID:  "ComposeStatusError",
			Msg: fmt.Sprintf("Error getting status of compose %s: %s", uuid, err),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}
	if composeStatus.State != ComposeFinished && composeStatus.State != ComposeFailed {
		errors := responseError{
			ID:  "BuildInWrongState",
			Msg: fmt.Sprintf("Build %s is in wrong state: %s", uuidString, composeStatus.State.ToString()),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	metadata, err := json.Marshal(&compose.ImageBuild.Manifest)
	common.PanicOnError(err)

	writer.Header().Set("Content-Disposition", "attachment; filename="+uuid.String()+".tar")
	writer.Header().Set("Content-Type", "application/x-tar")
	// NOTE: Do not set Content-Length, it should use chunked transfer encoding automatically

	tw := tar.NewWriter(writer)
	hdr := &tar.Header{
		Name:    uuid.String() + ".json",
		Mode:    0644,
		Size:    int64(len(metadata)),
		ModTime: time.Now().Truncate(time.Second),
	}
	err = tw.WriteHeader(hdr)
	common.PanicOnError(err)
	_, err = tw.Write(metadata)
	common.PanicOnError(err)

	// Add the logs
	var fileContents bytes.Buffer
	if composeStatus.Result != nil {
		err = composeStatus.Result.Write(&fileContents)
		common.PanicOnError(err)

		hdr = &tar.Header{
			Name:    "logs/osbuild.log",
			Mode:    0644,
			Size:    int64(fileContents.Len()),
			ModTime: time.Now().Truncate(time.Second),
		}
		err = tw.WriteHeader(hdr)
		common.PanicOnError(err)
		_, err = tw.Write(fileContents.Bytes())
		common.PanicOnError(err)
	}

	reader, fileSize, err := api.openImageFile(uuid, compose)
	if err == nil {
		hdr = &tar.Header{
			Name:    uuid.String() + "-" + compose.ImageBuild.ImageType.Filename(),
			Mode:    0644,
			Size:    int64(fileSize),
			ModTime: time.Now().Truncate(time.Second),
		}
		err = tw.WriteHeader(hdr)
		common.PanicOnError(err)
		_, err = io.Copy(tw, reader)
		common.PanicOnError(err)
	}

	err = tw.Close()
	common.PanicOnError(err)
}

func (api *API) composeLogsHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	uuidString := params.ByName("uuid")
	id, err := uuid.Parse(uuidString)
	if err != nil {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	compose, exists := api.store.GetCompose(id)
	if !exists {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	composeStatus, err := api.getComposeStatus(compose)
	if err != nil {
		errors := responseError{
			ID:  "ComposeStatusError",
			Msg: fmt.Sprintf("Error getting status of compose %s: %s", id, err),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}
	if composeStatus.State != ComposeFinished && composeStatus.State != ComposeFailed {
		errors := responseError{
			ID:  "BuildInWrongState",
			Msg: fmt.Sprintf("Build %s not in FINISHED or FAILED state.", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	writer.Header().Set("Content-Disposition", "attachment; filename="+id.String()+"-logs.tar")
	writer.Header().Set("Content-Type", "application/x-tar")

	tw := tar.NewWriter(writer)

	// tar format needs to contain file size before the actual file content, therefore the intermediate buffer
	var fileContents bytes.Buffer
	err = composeStatus.Result.Write(&fileContents)
	common.PanicOnError(err)

	header := &tar.Header{
		Name:    "logs/osbuild.log",
		Mode:    0644,
		Size:    int64(fileContents.Len()),
		ModTime: time.Now().Truncate(time.Second),
	}

	err = tw.WriteHeader(header)
	common.PanicOnError(err)

	_, err = io.Copy(tw, &fileContents)
	common.PanicOnError(err)

	err = tw.Close()
	common.PanicOnError(err)
}

func (api *API) composeLogHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	// TODO: implement size param
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	uuidString := params.ByName("uuid")
	id, err := uuid.Parse(uuidString)
	if err != nil {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	compose, exists := api.store.GetCompose(id)
	if !exists {
		errors := responseError{
			ID:  "UnknownUUID",
			Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
		}
		statusResponseError(writer, http.StatusBadRequest, errors)
		return
	}

	composeStatus, err := api.getComposeStatus(compose)
	if err != nil {
		errors := responseError{
			ID:  "ComposeStatusError",
			Msg: fmt.Sprintf("Error getting status of compose %s: %s", id, err),
		}
		statusResponseError(writer, http.StatusInternalServerError, errors)
		return
	}
	if composeStatus.State == ComposeWaiting {
		errors := responseError{
			ID:  "BuildInWrongState",
			Msg: fmt.Sprintf("Build %s has not started yet. No logs to view.", uuidString),
		}
		statusResponseError(writer, http.StatusOK, errors) // weirdly, Lorax returns 200 in this case
		return
	}

	if composeStatus.State == ComposeRunning {
		fmt.Fprintf(writer, "Build %s is still running.\n", uuidString)
		return
	}

	err = composeStatus.Result.Write(writer)
	common.PanicOnError(err)
}

func (api *API) composeFinishedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	reply := struct {
		Finished []*ComposeEntry `json:"finished"`
	}{[]*ComposeEntry{}}

	includeUploads := isRequestVersionAtLeast(params, 1)
	for id, compose := range api.store.GetAllComposes() {
		composeStatus, err := api.getComposeStatus(compose)
		if err != nil {
			log.Printf("Error getting status of compose %s: %s", id, err)
			continue
		}
		if composeStatus.State != ComposeFinished {
			continue
		}
		reply.Finished = append(reply.Finished, composeToComposeEntry(id, compose, composeStatus, includeUploads))
	}
	sortComposeEntries(reply.Finished)

	err := json.NewEncoder(writer).Encode(reply)
	common.PanicOnError(err)
}

func (api *API) composeFailedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 0) {
		return
	}

	reply := struct {
		Failed []*ComposeEntry `json:"failed"`
	}{[]*ComposeEntry{}}

	includeUploads := isRequestVersionAtLeast(params, 1)
	for id, compose := range api.store.GetAllComposes() {
		composeStatus, err := api.getComposeStatus(compose)
		if err != nil {
			log.Printf("Error getting status of compose %s: %s", id, err)
			continue
		}
		if composeStatus.State != ComposeFailed {
			continue
		}
		reply.Failed = append(reply.Failed, composeToComposeEntry(id, compose, composeStatus, includeUploads))
	}
	sortComposeEntries(reply.Failed)

	err := json.NewEncoder(writer).Encode(reply)
	common.PanicOnError(err)
}

// fetchPackageList returns the package list or the selected distribution
func (api *API) fetchPackageList(distroName, arch string, names []string) (packages rpmmd.PackageList, err error) {
	d := api.getDistro(distroName, arch)
	if d == nil {
		return nil, fmt.Errorf("GetDistro - unknown distribution: %s", distroName)
	}
	repos, err := api.allRepositories(distroName, arch)
	if err != nil {
		return nil, err
	}

	solver := api.solver.NewWithConfig(d.ModulePlatformID(), d.Releasever(), arch, d.Name())
	if len(names) == 0 {
		packages, err = solver.FetchMetadata(repos)
	} else {
		packages, err = solver.SearchMetadata(repos, names)
	}
	if err != nil {
		return nil, err
	}
	if err := solver.CleanCache(); err != nil {
		// log and ignore
		log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
	}
	return packages, nil
}

// Returns only user-defined repositories, which should be used only for
// payload package sets.
func (api *API) payloadRepositories(distroName string) []rpmmd.RepoConfig {
	distroSourceConfigs := api.store.GetAllDistroSources(distroName)
	payloadRepos := make([]rpmmd.RepoConfig, 0, len(distroSourceConfigs))
	for id, source := range distroSourceConfigs {
		payloadRepos = append(payloadRepos, source.RepoConfig(id))
	}
	return payloadRepos
}

// Returns all configured repositories as rpmmd.RepoConfig.
// Payload repositories (defined by the user) are assigned the payload package
// set names from the image type.
//
// The difference from allRepositories() is that this method may return additional repositories
// which are needed to build the specific image type. The allRepositories() can't do this, because
// it is used in places where image types are not considered.
//
// Note that the distro name is not determined from the image type, but must be provided as an argument.
// This enables using distro name aliases to get repositories.
func (api *API) allRepositoriesByImageType(distroName string, imageType distro.ImageType) ([]rpmmd.RepoConfig, error) {
	repos, err := api.repoRegistry.ReposByImageTypeName(distroName, imageType.Arch().Name(), imageType.Name())
	if err != nil {
		return nil, err
	}

	payloadRepos := api.payloadRepositories(distroName)
	// tag payload repositories with the payload package set names and add them to list of repos
	for _, pr := range payloadRepos {
		pr.PackageSets = imageType.PayloadPackageSets()
		repos = append(repos, pr)
	}

	return repos, nil
}

// Returns all configured repositories (base + sources) as rpmmd.RepoConfig
func (api *API) allRepositories(distroName, arch string) ([]rpmmd.RepoConfig, error) {
	repos, err := api.repoRegistry.ReposByArchName(distroName, arch, false)
	if err != nil {
		return nil, err
	}

	payloadRepos := api.payloadRepositories(distroName)
	repos = append(repos, payloadRepos...)

	return repos, nil
}

func (api *API) depsolveBlueprint(bp blueprint.Blueprint) ([]rpmmd.PackageSpec, error) {
	// Depsolve using the host distro if none has been specified
	if bp.Distro == "" {
		bp.Distro = api.hostDistroName
	}

	arch := bp.Arch
	if arch == "" {
		arch = api.hostArch
	}

	d := api.getDistro(bp.Distro, arch)
	if d == nil {
		return nil, fmt.Errorf("GetDistro - unknown distribution: %s", bp.Distro)
	}
	repos, err := api.allRepositories(bp.Distro, arch)
	if err != nil {
		return nil, err
	}

	solver := api.solver.NewWithConfig(d.ModulePlatformID(), d.Releasever(), arch, d.Name())
	solved, err := solver.Depsolve([]rpmmd.PackageSet{{Include: bp.GetPackages(), Repositories: repos}})
	if err != nil {
		return nil, err
	}

	if err := solver.CleanCache(); err != nil {
		// log and ignore
		log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
	}
	return solved, nil
}

func (api *API) uploadsScheduleHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}

	// TODO: implement this route (it is v1 only)
	notImplementedHandler(writer, request, params)
}

func (api *API) uploadsDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}

	// TODO: implement this route (it is v1 only)
	notImplementedHandler(writer, request, params)
}

func (api *API) uploadsInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}

	// TODO: implement this route (it is v1 only)
	notImplementedHandler(writer, request, params)
}

func (api *API) uploadsLogHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}

	// TODO: implement this route (it is v1 only)
	notImplementedHandler(writer, request, params)
}

func (api *API) uploadsResetHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}

	// TODO: implement this route (it is v1 only)
	notImplementedHandler(writer, request, params)
}

func (api *API) uploadsCancelHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}

	// TODO: implement this route (it is v1 only)
	notImplementedHandler(writer, request, params)
}

func (api *API) providersHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}

	// TODO: implement this route (it is v1 only)
	notImplementedHandler(writer, request, params)
}

func (api *API) providersSaveHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}

	// TODO: implement this route (it is v1 only)
	notImplementedHandler(writer, request, params)
}

func (api *API) providersDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}

	// TODO: implement this route (it is v1 only)
	notImplementedHandler(writer, request, params)
}

func (api *API) distrosListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
	if !verifyRequestVersion(writer, params, 1) {
		return
	}

	archName := api.hostArch
	if arch := request.URL.Query().Get("arch"); arch != "" {
		archName = arch
	}

	var reply struct {
		Distros []string `json:"distros"`
	}
	reply.Distros = api.validDistros(archName)

	err := json.NewEncoder(writer).Encode(reply)
	common.PanicOnError(err)
}
