package deb

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/ugorji/go/codec"

	"github.com/aptly-dev/aptly/aptly"
	"github.com/aptly-dev/aptly/database"
	"github.com/aptly-dev/aptly/pgp"
	"github.com/aptly-dev/aptly/utils"
)

type SourceEntry struct {
	Component, Name string
}

type PublishedRepoUpdateResult struct {
	AddedSources   map[string]string
	UpdatedSources map[string]string
	RemovedSources map[string]string
}

type repoSourceItem struct {
	// Pointer to snapshot if SourceKind == "snapshot"
	snapshot *Snapshot
	// Pointer to local repo if SourceKind == "local"
	localRepo *LocalRepo
	// Package references is SourceKind == "local"
	packageRefs *PackageRefList
}

// PublishedRepo is a published for http/ftp representation of snapshot as Debian repository
type PublishedRepo struct {
	// Internal unique ID
	UUID string
	// Storage & Prefix & distribution should be unique across all published repositories
	Storage              string
	Prefix               string
	Distribution         string
	Origin               string
	NotAutomatic         string
	ButAutomaticUpgrades string
	Label                string
	Suite                string
	Codename             string
	// Architectures is a list of all architectures published
	Architectures []string
	// SourceKind is "local"/"repo"
	SourceKind string

	// Map of sources by each component: component name -> source UUID
	Sources map[string]string

	// Legacy fields for compatibility with old published repositories (< 0.6)
	Component string
	// SourceUUID is UUID of either snapshot or local repo
	SourceUUID string `codec:"SnapshotUUID"`
	// Map of component to source items
	sourceItems map[string]repoSourceItem

	// Skip contents generation
	SkipContents bool

	// Skip bz2 compression for index files
	SkipBz2 bool

	// True if repo is being re-published
	rePublishing bool

	// Provide index files per hash also
	AcquireByHash bool

	// Support multiple distributions
	MultiDist bool

	// Revision
	Revision *PublishedRepoRevision
}

type PublishedRepoRevision struct {
	// Map of sources: component name -> snapshot name/local repo Name
	Sources map[string]string
}

func (result *PublishedRepoUpdateResult) AddedComponents() []string {
	components := make([]string, 0, len(result.AddedSources))
	for component := range result.AddedSources {
		components = append(components, component)
	}
	sort.Strings(components)

	return components
}

func (result *PublishedRepoUpdateResult) UpdatedComponents() []string {
	components := make([]string, 0, len(result.UpdatedSources))
	for component := range result.UpdatedSources {
		components = append(components, component)
	}
	sort.Strings(components)

	return components
}

func (result *PublishedRepoUpdateResult) RemovedComponents() []string {
	components := make([]string, 0, len(result.RemovedSources))
	for component := range result.RemovedSources {
		components = append(components, component)
	}
	sort.Strings(components)

	return components
}

func (revision *PublishedRepoRevision) Components() []string {
	components := make([]string, 0, len(revision.Sources))
	for component := range revision.Sources {
		components = append(components, component)
	}
	sort.Strings(components)

	return components
}

func (revision *PublishedRepoRevision) SourceList() []SourceEntry {
	sources := revision.Sources
	components := revision.Components()
	sourceList := make([]SourceEntry, 0, len(sources))
	for _, component := range components {
		name := sources[component]
		sourceList = append(sourceList, SourceEntry{
			Component: component,
			Name:      name,
		})
	}

	return sourceList
}

func (revision *PublishedRepoRevision) SourceNames() []string {
	sources := revision.Sources
	names := make([]string, 0, len(sources))
	for _, component := range revision.Components() {
		names = append(names, sources[component])
	}

	return names
}

func (p *PublishedRepo) DropRevision() *PublishedRepoRevision {
	revision := p.Revision
	p.Revision = nil

	return revision
}

func (p *PublishedRepo) ObtainRevision() *PublishedRepoRevision {
	revision := p.Revision
	if revision == nil {
		sources := make(map[string]string, len(p.Sources))
		for _, component := range p.Components() {
			item := p.sourceItems[component]
			if item.snapshot != nil {
				sources[component] = item.snapshot.Name
			} else if item.localRepo != nil {
				sources[component] = item.localRepo.Name
			} else {
				panic("no snapshot/localRepo")
			}
		}
		revision = &PublishedRepoRevision{
			Sources: sources,
		}
		p.Revision = revision
	}
	return revision
}

func (p *PublishedRepo) Update(collectionFactory *CollectionFactory, _ aptly.Progress) (*PublishedRepoUpdateResult, error) {
	result := &PublishedRepoUpdateResult{
		AddedSources:   map[string]string{},
		UpdatedSources: map[string]string{},
		RemovedSources: map[string]string{},
	}

	revision := p.DropRevision()
	if revision == nil {
		if p.SourceKind == SourceLocalRepo {
			// Re-fetch packages from local repository
			for component, item := range p.sourceItems {
				localRepo := item.localRepo
				if localRepo != nil {
					p.UpdateLocalRepo(component, localRepo)
					result.UpdatedSources[component] = localRepo.Name
				}
			}
		}
	} else {
		for _, component := range p.Components() {
			name, exists := revision.Sources[component]
			if !exists {
				p.RemoveComponent(component)
				result.RemovedSources[component] = name
			}
		}

		if p.SourceKind == SourceLocalRepo {
			localRepoCollection := collectionFactory.LocalRepoCollection()
			for component, name := range revision.Sources {
				localRepo, err := localRepoCollection.ByName(name)
				if err != nil {
					return result, fmt.Errorf("unable to update: %s", err)
				}

				err = localRepoCollection.LoadComplete(localRepo)
				if err != nil {
					return result, fmt.Errorf("unable to update: %s", err)
				}

				_, exists := p.Sources[component]
				if exists {
					// Even in the case, when the local repository has not been changed as package source,
					// it may contain a modified set of packages that requires (re-)publication.
					p.UpdateLocalRepo(component, localRepo)
					result.UpdatedSources[component] = name
				} else {
					p.UpdateLocalRepo(component, localRepo)
					result.AddedSources[component] = name
				}
			}
		} else if p.SourceKind == SourceSnapshot {
			snapshotCollection := collectionFactory.SnapshotCollection()
			for component, name := range revision.Sources {
				snapshot, err := snapshotCollection.ByName(name)
				if err != nil {
					return result, fmt.Errorf("unable to update: %s", err)
				}

				err = snapshotCollection.LoadComplete(snapshot)
				if err != nil {
					return result, fmt.Errorf("unable to update: %s", err)
				}

				sourceUUID, exists := p.Sources[component]
				if exists {
					if snapshot.UUID != sourceUUID {
						p.UpdateSnapshot(component, snapshot)
						result.UpdatedSources[component] = name
					}
				} else {
					p.UpdateSnapshot(component, snapshot)
					result.AddedSources[component] = name
				}
			}
		} else {
			return result, fmt.Errorf("unknown published repository type")
		}
	}

	return result, nil
}

// ParsePrefix splits [storage:]prefix into components
func ParsePrefix(param string) (storage, prefix string) {
	i := strings.LastIndex(param, ":")
	if i != -1 {
		storage = param[:i]
		prefix = param[i+1:]
		if prefix == "" {
			prefix = "."
		}
	} else {
		prefix = param
	}
	prefix = strings.TrimPrefix(strings.TrimSuffix(prefix, "/"), "/")
	return
}

// walkUpTree goes from source in the tree of source snapshots/mirrors/local repos
// gathering information about declared components and distributions
func walkUpTree(source interface{}, collectionFactory *CollectionFactory) (rootDistributions []string, rootComponents []string) {
	var (
		head    interface{}
		current = []interface{}{source}
	)

	rootComponents = []string{}
	rootDistributions = []string{}

	// walk up the tree from current source up to roots (local or remote repos)
	// and collect information about distribution and components
	for len(current) > 0 {
		head, current = current[0], current[1:]

		if snapshot, ok := head.(*Snapshot); ok {
			for _, uuid := range snapshot.SourceIDs {
				if snapshot.SourceKind == SourceRemoteRepo {
					remoteRepo, err := collectionFactory.RemoteRepoCollection().ByUUID(uuid)
					if err != nil {
						continue
					}
					current = append(current, remoteRepo)
				} else if snapshot.SourceKind == SourceLocalRepo {
					localRepo, err := collectionFactory.LocalRepoCollection().ByUUID(uuid)
					if err != nil {
						continue
					}
					current = append(current, localRepo)
				} else if snapshot.SourceKind == SourceSnapshot {
					snap, err := collectionFactory.SnapshotCollection().ByUUID(uuid)
					if err != nil {
						continue
					}
					current = append(current, snap)
				}
			}
		} else if localRepo, ok := head.(*LocalRepo); ok {
			if localRepo.DefaultDistribution != "" {
				rootDistributions = append(rootDistributions, localRepo.DefaultDistribution)
			}
			if localRepo.DefaultComponent != "" {
				rootComponents = append(rootComponents, localRepo.DefaultComponent)
			}
		} else if remoteRepo, ok := head.(*RemoteRepo); ok {
			if remoteRepo.Distribution != "" {
				rootDistributions = append(rootDistributions, remoteRepo.Distribution)
			}
			rootComponents = append(rootComponents, remoteRepo.Components...)
		} else {
			panic("unknown type")
		}
	}

	return
}

// NewPublishedRepo creates new published repository
//
// storage is PublishedStorage name
// prefix specifies publishing prefix
// distribution and architectures are user-defined properties
// components & sources are lists of component to source mapping (*Snapshot or *LocalRepo)
func NewPublishedRepo(storage, prefix, distribution string, architectures []string,
	components []string, sources []interface{}, collectionFactory *CollectionFactory, multiDist bool) (*PublishedRepo, error) {
	result := &PublishedRepo{
		UUID:          uuid.NewString(),
		Storage:       storage,
		Architectures: architectures,
		Sources:       make(map[string]string),
		sourceItems:   make(map[string]repoSourceItem),
		MultiDist:     multiDist,
	}

	if len(sources) == 0 {
		panic("publish with empty sources")
	}

	if len(sources) != len(components) {
		panic("sources and components should be equal in size")
	}

	var (
		discoveredDistributions = []string{}
		source                  interface{}
		component               string
		snapshot                *Snapshot
		localRepo               *LocalRepo
		fields                  = make(map[string][]string)
	)

	// get first source
	source = sources[0]

	// figure out source kind
	switch source.(type) {
	case *Snapshot:
		result.SourceKind = SourceSnapshot
	case *LocalRepo:
		result.SourceKind = SourceLocalRepo
	default:
		panic("unknown source kind")
	}

	for i := range sources {
		component, source = components[i], sources[i]
		if distribution == "" || component == "" {
			rootDistributions, rootComponents := walkUpTree(source, collectionFactory)
			if distribution == "" {
				discoveredDistributions = append(discoveredDistributions, rootDistributions...)
			}
			if component == "" {
				sort.Strings(rootComponents)
				if len(rootComponents) > 0 && rootComponents[0] == rootComponents[len(rootComponents)-1] {
					component = rootComponents[0]
				} else if len(sources) == 1 {
					// only if going from one source, assume default component "main"
					component = "main"
				} else {
					return nil, fmt.Errorf("unable to figure out component name for %s", source)
				}
			}
		}

		_, exists := result.Sources[component]
		if exists {
			return nil, fmt.Errorf("duplicate component name: %s", component)
		}

		if result.SourceKind == SourceSnapshot {
			snapshot = source.(*Snapshot)
			result.Sources[component] = snapshot.UUID
			result.sourceItems[component] = repoSourceItem{snapshot: snapshot}

			if !utils.StrSliceHasItem(fields["Origin"], snapshot.Origin) {
				fields["Origin"] = append(fields["Origin"], snapshot.Origin)
			}
			if !utils.StrSliceHasItem(fields["NotAutomatic"], snapshot.NotAutomatic) {
				fields["NotAutomatic"] = append(fields["NotAutomatic"], snapshot.NotAutomatic)
			}
			if !utils.StrSliceHasItem(fields["ButAutomaticUpgrades"], snapshot.ButAutomaticUpgrades) {
				fields["ButAutomaticUpgrades"] = append(fields["ButAutomaticUpgrades"], snapshot.ButAutomaticUpgrades)
			}
		} else if result.SourceKind == SourceLocalRepo {
			localRepo = source.(*LocalRepo)
			result.Sources[component] = localRepo.UUID
			result.sourceItems[component] = repoSourceItem{localRepo: localRepo, packageRefs: localRepo.RefList()}
		}
	}

	// clean & verify prefix
	prefix = filepath.Clean(prefix)
	prefix = strings.TrimPrefix(strings.TrimSuffix(prefix, "/"), "/")
	prefix = filepath.Clean(prefix)

	for _, part := range strings.Split(prefix, "/") {
		if part == ".." || part == "dists" || part == "pool" {
			return nil, fmt.Errorf("invalid prefix %s", prefix)
		}
	}

	result.Prefix = prefix

	// guessing distribution
	if distribution == "" {
		sort.Strings(discoveredDistributions)
		if len(discoveredDistributions) > 0 && discoveredDistributions[0] == discoveredDistributions[len(discoveredDistributions)-1] {
			distribution = discoveredDistributions[0]
		} else {
			return nil, fmt.Errorf("unable to guess distribution name, please specify explicitly")
		}
	}

	result.Distribution = distribution

	// only fields which are unique by all given snapshots are set on published
	if len(fields["Origin"]) == 1 {
		result.Origin = fields["Origin"][0]
	}
	if len(fields["NotAutomatic"]) == 1 {
		result.NotAutomatic = fields["NotAutomatic"][0]
	}
	if len(fields["ButAutomaticUpgrades"]) == 1 {
		result.ButAutomaticUpgrades = fields["ButAutomaticUpgrades"][0]
	}

	return result, nil
}

func (revision *PublishedRepoRevision) MarshalJSON() ([]byte, error) {
	sources := revision.Sources
	components := revision.Components()
	sourceList := make([]SourceEntry, 0, len(sources))
	for _, component := range components {
		name := sources[component]
		sourceList = append(sourceList, SourceEntry{
			Component: component,
			Name:      name,
		})
	}

	return json.Marshal(map[string]interface{}{
		"Sources": sourceList,
	})
}

// MarshalJSON requires object to filled by "LoadShallow" or "LoadComplete"
func (p *PublishedRepo) MarshalJSON() ([]byte, error) {
	sources := []SourceEntry{}
	for _, component := range p.Components() {
		item := p.sourceItems[component]
		name := ""
		if item.snapshot != nil {
			name = item.snapshot.Name
		} else if item.localRepo != nil {
			name = item.localRepo.Name
		} else {
			panic("no snapshot/local repo")
		}
		sources = append(sources, SourceEntry{
			Component: component,
			Name:      name,
		})
	}

	return json.Marshal(map[string]interface{}{
		"Architectures":        p.Architectures,
		"Distribution":         p.Distribution,
		"Label":                p.Label,
		"Origin":               p.Origin,
		"Suite":                p.Suite,
		"Codename":             p.Codename,
		"NotAutomatic":         p.NotAutomatic,
		"ButAutomaticUpgrades": p.ButAutomaticUpgrades,
		"Prefix":               p.Prefix,
		"Path":                 p.GetPath(),
		"SourceKind":           p.SourceKind,
		"Sources":              sources,
		"Storage":              p.Storage,
		"SkipContents":         p.SkipContents,
		"AcquireByHash":        p.AcquireByHash,
		"MultiDist":            p.MultiDist,
	})
}

// String returns human-readable representation of PublishedRepo
func (p *PublishedRepo) String() string {
	var sources = []string{}

	for _, component := range p.Components() {
		var source string

		item := p.sourceItems[component]
		if item.snapshot != nil {
			source = item.snapshot.String()
		} else if item.localRepo != nil {
			source = item.localRepo.String()
		} else {
			panic("no snapshot/localRepo")
		}

		sources = append(sources, fmt.Sprintf("{%s: %s}", component, source))
	}

	var extras []string
	var extra string

	if p.Origin != "" {
		extras = append(extras, fmt.Sprintf("origin: %s", p.Origin))
	}

	if p.NotAutomatic != "" {
		extras = append(extras, fmt.Sprintf("notautomatic: %s", p.NotAutomatic))
	}

	if p.ButAutomaticUpgrades != "" {
		extras = append(extras, fmt.Sprintf("butautomaticupgrades: %s", p.ButAutomaticUpgrades))
	}

	if p.Label != "" {
		extras = append(extras, fmt.Sprintf("label: %s", p.Label))
	}

	if p.Suite != "" {
		extras = append(extras, fmt.Sprintf("suite: %s", p.Suite))
	}

	if p.Codename != "" {
		extras = append(extras, fmt.Sprintf("codename: %s", p.Codename))
	}

	extra = strings.Join(extras, ", ")

	if extra != "" {
		extra = " (" + extra + ")"
	}

	return fmt.Sprintf("%s/%s%s [%s] publishes %s", p.StoragePrefix(), p.Distribution, extra, strings.Join(p.Architectures, ", "),
		strings.Join(sources, ", "))
}

// StoragePrefix returns combined storage & prefix for the repo
func (p *PublishedRepo) StoragePrefix() string {
	result := p.Prefix
	if p.Storage != "" {
		result = p.Storage + ":" + p.Prefix
	}
	return result
}

// Key returns unique key identifying PublishedRepo
func (p *PublishedRepo) Key() []byte {
	return []byte("U" + p.StoragePrefix() + ">>" + p.Distribution)
}

// RefKey is a unique id for package reference list
func (p *PublishedRepo) RefKey(component string) []byte {
	return []byte("E" + p.UUID + component)
}

// RefList returns list of package refs in local repo
func (p *PublishedRepo) RefList(component string) *PackageRefList {
	item := p.sourceItems[component]
	if p.SourceKind == SourceLocalRepo {
		return item.packageRefs
	}
	if p.SourceKind == SourceSnapshot {
		return item.snapshot.RefList()
	}
	panic("unknown source")
}

// Components returns sorted list of published repo components
func (p *PublishedRepo) Components() []string {
	result := make([]string, 0, len(p.Sources))
	for component := range p.Sources {
		result = append(result, component)
	}

	sort.Strings(result)
	return result
}

// SourceNames returns sorted list of published repo source names
func (p *PublishedRepo) SourceNames() []string {
	var sources = []string{}

	for _, component := range p.Components() {
		var source string

		item := p.sourceItems[component]
		if item.snapshot != nil {
			source = item.snapshot.Name
		} else if item.localRepo != nil {
			source = item.localRepo.Name
		} else {
			panic("no snapshot/localRepo")
		}

		sources = append(sources, fmt.Sprintf("%s:%s", source, component))
	}

	sort.Strings(sources)
	return sources
}

// UpdateLocalRepo inserts/updates local repository source for component
func (p *PublishedRepo) UpdateLocalRepo(component string, localRepo *LocalRepo) {
	if p.SourceKind != SourceLocalRepo {
		panic("not local repo publish")
	}

	item, exists := p.sourceItems[component]
	if !exists {
		item = repoSourceItem{}
	}
	item.localRepo = localRepo
	item.packageRefs = localRepo.RefList()
	p.sourceItems[component] = item

	p.Sources[component] = localRepo.UUID
	p.rePublishing = true
}

// UpdateSnapshot inserts/updates snapshot source for component
func (p *PublishedRepo) UpdateSnapshot(component string, snapshot *Snapshot) {
	if p.SourceKind != SourceSnapshot {
		panic("not snapshot publish")
	}

	item, exists := p.sourceItems[component]
	if !exists {
		item = repoSourceItem{}
	}
	item.snapshot = snapshot
	p.sourceItems[component] = item

	p.Sources[component] = snapshot.UUID
	p.rePublishing = true
}

// RemoveComponent removes component from published repository
func (p *PublishedRepo) RemoveComponent(component string) {
	delete(p.Sources, component)
	delete(p.sourceItems, component)

	p.rePublishing = true
}

// Encode does msgpack encoding of PublishedRepo
func (p *PublishedRepo) Encode() []byte {
	var buf bytes.Buffer

	encoder := codec.NewEncoder(&buf, &codec.MsgpackHandle{})
	_ = encoder.Encode(p)

	return buf.Bytes()
}

// Decode decodes msgpack representation into PublishedRepo
func (p *PublishedRepo) Decode(input []byte) error {
	decoder := codec.NewDecoderBytes(input, &codec.MsgpackHandle{})
	err := decoder.Decode(p)
	if err != nil {
		return err
	}

	// old PublishedRepo were publishing only snapshots
	if p.SourceKind == "" {
		p.SourceKind = SourceSnapshot
	}

	// <0.6 aptly used single SourceUUID + Component instead of Sources
	if p.Component != "" && p.SourceUUID != "" && len(p.Sources) == 0 {
		p.Sources = map[string]string{p.Component: p.SourceUUID}
		p.Component = ""
		p.SourceUUID = ""
	}

	return nil
}

// GetOrigin returns default or manual Origin:
func (p *PublishedRepo) GetOrigin() string {
	if p.Origin == "" {
		return p.Prefix + " " + p.Distribution
	}
	return p.Origin
}

// GetLabel returns default or manual Label:
func (p *PublishedRepo) GetLabel() string {
	if p.Label == "" {
		return p.Prefix + " " + p.Distribution
	}
	return p.Label
}

// GetPath returns the unique name of the repo
func (p *PublishedRepo) GetPath() string {
	prefix := p.StoragePrefix()

	if prefix == "" {
		return p.Distribution
	}

	return fmt.Sprintf("%s/%s", prefix, p.Distribution)
}

// GetSuite returns default or manual Suite:
func (p *PublishedRepo) GetSuite() string {
	if p.Suite == "" {
		return p.Distribution
	}
	return p.Suite
}

// GetCodename returns default or manual Codename:
func (p *PublishedRepo) GetCodename() string {
	if p.Codename == "" {
		return p.Distribution
	}
	return p.Codename
}

// GetSkelFiles returns a map of files to be added to a repo. Key being the relative
// path from component folder, and value being the full local FS path.
func (p *PublishedRepo) GetSkelFiles(skelDir string, component string) (map[string]string, error) {
	files := make(map[string]string)

	if skelDir == "" {
		return files, nil
	}

	fsPath := filepath.Join(skelDir, p.Prefix, "dists", p.Distribution, component)
	if err := filepath.Walk(fsPath, func(path string, _ os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		stat, err := os.Stat(path)
		if err != nil {
			return err
		}

		if !stat.Mode().IsRegular() {
			return nil
		}

		relativePath, err := filepath.Rel(fsPath, path)
		if err != nil {
			return err
		}

		files[relativePath] = path
		return nil
	}); err != nil && !os.IsNotExist(err) {
		return files, err
	}

	return files, nil
}

// Publish publishes snapshot (repository) contents, links package files, generates Packages & Release files, signs them
func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageProvider aptly.PublishedStorageProvider,
	collectionFactory *CollectionFactory, signer pgp.Signer, progress aptly.Progress, forceOverwrite bool, skelDir string) error {
	publishedStorage := publishedStorageProvider.GetPublishedStorage(p.Storage)

	err := publishedStorage.MkDir(filepath.Join(p.Prefix, "pool"))
	if err != nil {
		return err
	}
	basePath := filepath.Join(p.Prefix, "dists", p.Distribution)
	err = publishedStorage.MkDir(basePath)
	if err != nil {
		return err
	}

	tempDB, err := collectionFactory.TemporaryDB()
	if err != nil {
		return err
	}
	defer func() {
		e := tempDB.Close()
		if e != nil && progress != nil {
			progress.Printf("failed to close temp DB: %s", err)
		}
		e = tempDB.Drop()
		if e != nil && progress != nil {
			progress.Printf("failed to drop temp DB: %s", err)
		}
	}()

	if progress != nil {
		progress.Printf("Loading packages...\n")
	}

	lists := map[string]*PackageList{}

	for component := range p.sourceItems {
		// Load all packages
		lists[component], err = NewPackageListFromRefList(p.RefList(component), collectionFactory.PackageCollection(), progress)
		if err != nil {
			return fmt.Errorf("unable to load packages: %s", err)
		}
	}

	if !p.rePublishing {
		if len(p.Architectures) == 0 {
			for _, list := range lists {
				p.Architectures = append(p.Architectures, list.Architectures(true)...)
			}
		}

		if len(p.Architectures) == 0 {
			return fmt.Errorf("unable to figure out list of architectures, please supply explicit list")
		}

		sort.Strings(p.Architectures)
		p.Architectures = utils.StrSliceDeduplicate(p.Architectures)
	}

	var suffix string
	if p.rePublishing {
		suffix = ".tmp"
	}

	if progress != nil {
		progress.Printf("Generating metadata files and linking package files...\n")
	}

	var tempDir string
	tempDir, err = os.MkdirTemp(os.TempDir(), "aptly")
	if err != nil {
		return err
	}
	defer func() { _ = os.RemoveAll(tempDir) }()

	indexes := newIndexFiles(publishedStorage, basePath, tempDir, suffix, p.AcquireByHash, p.SkipBz2)

	legacyContentIndexes := map[string]*ContentsIndex{}
	var count int64
	for _, list := range lists {
		count = count + int64(list.Len())
	}

	if progress != nil {
		progress.InitBar(count, false, aptly.BarPublishGeneratePackageFiles)
	}

	for component, list := range lists {
		hadUdebs := false

		// For all architectures, pregenerate packages/sources files
		for _, arch := range p.Architectures {
			indexes.PackageIndex(component, arch, false, false, p.Distribution)
		}

		list.PrepareIndex()

		contentIndexes := map[string]*ContentsIndex{}

		err = list.ForEachIndexed(func(pkg *Package) error {
			if progress != nil {
				progress.AddBar(1)
			}

			for _, arch := range p.Architectures {
				if pkg.MatchesArchitecture(arch) {
					hadUdebs = hadUdebs || pkg.IsUdeb

					var relPath string
					if !pkg.IsInstaller {
						poolDir, err2 := pkg.PoolDirectory()
						if err2 != nil {
							return err2
						}
						if p.MultiDist {
							relPath = filepath.Join("pool", p.Distribution, component, poolDir)
						} else {
							relPath = filepath.Join("pool", component, poolDir)
						}

					} else {
						if p.Distribution == aptly.DistributionFocal {
							relPath = filepath.Join("dists", p.Distribution, component, fmt.Sprintf("%s-%s", pkg.Name, arch), "current", "legacy-images")
						} else {
							relPath = filepath.Join("dists", p.Distribution, component, fmt.Sprintf("%s-%s", pkg.Name, arch), "current", "images")
						}
					}

					err = pkg.LinkFromPool(publishedStorage, packagePool, p.Prefix, relPath, forceOverwrite)
					if err != nil {
						return err
					}
					break
				}
			}

			// Start a db batch. If we fill contents data we'll need
			// to push each path of the package into the database.
			// We'll want this batched so as to avoid an excessive
			// amount of write() calls.
			batch := tempDB.CreateBatch()

			for _, arch := range p.Architectures {
				if pkg.MatchesArchitecture(arch) {
					var bufWriter *bufio.Writer

					if !p.SkipContents && !pkg.IsInstaller {
						key := fmt.Sprintf("%s-%v", arch, pkg.IsUdeb)
						qualifiedName := []byte(pkg.QualifiedName())
						contents := pkg.Contents(packagePool, progress)

						for _, contentIndexesMap := range []map[string]*ContentsIndex{contentIndexes, legacyContentIndexes} {
							contentIndex := contentIndexesMap[key]

							if contentIndex == nil {
								contentIndex = NewContentsIndex(tempDB)
								contentIndexesMap[key] = contentIndex
							}

							_ = contentIndex.Push(qualifiedName, contents, batch)
						}
					}

					bufWriter, err = indexes.PackageIndex(component, arch, pkg.IsUdeb, pkg.IsInstaller, p.Distribution).BufWriter()
					if err != nil {
						return err
					}

					err = pkg.Stanza().WriteTo(bufWriter, pkg.IsSource, false, pkg.IsInstaller)
					if err != nil {
						return err
					}
					err = bufWriter.WriteByte('\n')
					if err != nil {
						return err
					}
				}
			}

			pkg.files = nil
			pkg.deps = nil
			pkg.extra = nil
			pkg.contents = nil

			return batch.Write()
		})

		if err != nil {
			return fmt.Errorf("unable to process packages: %s", err)
		}

		for _, arch := range p.Architectures {
			for _, udeb := range []bool{true, false} {
				index := contentIndexes[fmt.Sprintf("%s-%v", arch, udeb)]
				if index == nil || index.Empty() {
					continue
				}

				var bufWriter *bufio.Writer
				bufWriter, err = indexes.ContentsIndex(component, arch, udeb).BufWriter()
				if err != nil {
					return fmt.Errorf("unable to generate contents index: %v", err)
				}

				_, err = index.WriteTo(bufWriter)
				if err != nil {
					return fmt.Errorf("unable to generate contents index: %v", err)
				}
			}
		}

		for component := range p.sourceItems {
			skelFiles, err := p.GetSkelFiles(skelDir, component)
			if err != nil {
				return fmt.Errorf("unable to get skeleton files: %v", err)
			}

			for relPath, absPath := range skelFiles {
				bufWriter, err := indexes.SkelIndex(component, relPath).BufWriter()
				if err != nil {
					return fmt.Errorf("unable to generate skeleton index: %v", err)
				}

				file, err := os.Open(absPath)
				if err != nil {
					return fmt.Errorf("unable to read skeleton file: %v", err)
				}

				_, err = bufio.NewReader(file).WriteTo(bufWriter)
				if err != nil {
					return fmt.Errorf("unable to write skeleton file: %v", err)
				}
			}
		}

		udebs := []bool{false}
		if hadUdebs {
			udebs = append(udebs, true)

			// For all architectures, pregenerate .udeb indexes
			for _, arch := range p.Architectures {
				indexes.PackageIndex(component, arch, true, false, p.Distribution)
			}
		}

		// For all architectures, generate Release files
		for _, arch := range p.Architectures {
			for _, udeb := range udebs {
				release := make(Stanza)
				release["Archive"] = p.Distribution
				release["Architecture"] = arch
				release["Component"] = component
				release["Origin"] = p.GetOrigin()
				release["Label"] = p.GetLabel()
				release["Suite"] = p.GetSuite()
				release["Codename"] = p.GetCodename()
				if p.AcquireByHash {
					release["Acquire-By-Hash"] = "yes"
				}

				var bufWriter *bufio.Writer
				bufWriter, err = indexes.ReleaseIndex(component, arch, udeb).BufWriter()
				if err != nil {
					return fmt.Errorf("unable to get ReleaseIndex writer: %s", err)
				}

				err = release.WriteTo(bufWriter, false, true, false)
				if err != nil {
					return fmt.Errorf("unable to create Release file: %s", err)
				}
			}
		}
	}

	for _, arch := range p.Architectures {
		for _, udeb := range []bool{true, false} {
			index := legacyContentIndexes[fmt.Sprintf("%s-%v", arch, udeb)]
			if index == nil || index.Empty() {
				continue
			}

			var bufWriter *bufio.Writer
			bufWriter, err = indexes.LegacyContentsIndex(arch, udeb).BufWriter()
			if err != nil {
				return fmt.Errorf("unable to generate contents index: %v", err)
			}

			_, err = index.WriteTo(bufWriter)
			if err != nil {
				return fmt.Errorf("unable to generate contents index: %v", err)
			}
		}
	}

	if progress != nil {
		progress.ShutdownBar()
		progress.Printf("Finalizing metadata files...\n")
	}

	err = indexes.FinalizeAll(progress, signer)
	if err != nil {
		return err
	}

	release := make(Stanza)
	release["Origin"] = p.GetOrigin()
	if p.NotAutomatic != "" {
		release["NotAutomatic"] = p.NotAutomatic
	}
	if p.ButAutomaticUpgrades != "" {
		release["ButAutomaticUpgrades"] = p.ButAutomaticUpgrades
	}
	release["Label"] = p.GetLabel()
	release["Suite"] = p.GetSuite()
	release["Codename"] = p.GetCodename()
	release["Date"] = time.Now().UTC().Format("Mon, 2 Jan 2006 15:04:05 MST")
	release["Architectures"] = strings.Join(utils.StrSlicesSubstract(p.Architectures, []string{ArchitectureSource}), " ")
	if p.AcquireByHash {
		release["Acquire-By-Hash"] = "yes"
	}
	release["Description"] = " Generated by aptly\n"
	release["MD5Sum"] = ""
	release["SHA1"] = ""
	release["SHA256"] = ""
	release["SHA512"] = ""

	release["Components"] = strings.Join(p.Components(), " ")

	sortedPaths := make([]string, 0, len(indexes.generatedFiles))
	for path := range indexes.generatedFiles {
		sortedPaths = append(sortedPaths, path)
	}
	sort.Strings(sortedPaths)

	for _, path := range sortedPaths {
		info := indexes.generatedFiles[path]
		release["MD5Sum"] += fmt.Sprintf(" %s %8d %s\n", info.MD5, info.Size, path)
		release["SHA1"] += fmt.Sprintf(" %s %8d %s\n", info.SHA1, info.Size, path)
		release["SHA256"] += fmt.Sprintf(" %s %8d %s\n", info.SHA256, info.Size, path)
		release["SHA512"] += fmt.Sprintf(" %s %8d %s\n", info.SHA512, info.Size, path)
	}

	releaseFile := indexes.ReleaseFile()
	bufWriter, err := releaseFile.BufWriter()
	if err != nil {
		return err
	}

	err = release.WriteTo(bufWriter, false, true, false)
	if err != nil {
		return fmt.Errorf("unable to create Release file: %s", err)
	}

	// Signing files might output to console, so flush progress writer first
	if progress != nil {
		progress.Flush()
	}

	err = releaseFile.Finalize(signer)
	if err != nil {
		return err
	}

	return indexes.RenameFiles()
}

// RemoveFiles removes files that were created by Publish
//
// It can remove prefix fully, and part of pool (for specific component)
func (p *PublishedRepo) RemoveFiles(publishedStorageProvider aptly.PublishedStorageProvider, removePrefix bool,
	removePoolComponents []string, progress aptly.Progress) error {
	publishedStorage := publishedStorageProvider.GetPublishedStorage(p.Storage)

	// I. Easy: remove whole prefix (meta+packages)
	if removePrefix {
		err := publishedStorage.RemoveDirs(filepath.Join(p.Prefix, "dists"), progress)
		if err != nil {
			return err
		}

		return publishedStorage.RemoveDirs(filepath.Join(p.Prefix, "pool"), progress)
	}

	// II. Medium: remove metadata, it can't be shared as prefix/distribution as unique
	err := publishedStorage.RemoveDirs(filepath.Join(p.Prefix, "dists", p.Distribution), progress)
	if err != nil {
		return err
	}

	// III. Complex: there are no other publishes with the same prefix + component
	for _, component := range removePoolComponents {
		err = publishedStorage.RemoveDirs(filepath.Join(p.Prefix, "pool", component), progress)
		if err != nil {
			return err
		}
	}

	return nil
}

// PublishedRepoCollection does listing, updating/adding/deleting of PublishedRepos
type PublishedRepoCollection struct {
	db   database.Storage
	list []*PublishedRepo
}

// NewPublishedRepoCollection loads PublishedRepos from DB and makes up collection
func NewPublishedRepoCollection(db database.Storage) *PublishedRepoCollection {
	return &PublishedRepoCollection{
		db: db,
	}
}

func (collection *PublishedRepoCollection) loadList() {
	if collection.list != nil {
		return
	}

	blobs := collection.db.FetchByPrefix([]byte("U"))
	collection.list = make([]*PublishedRepo, 0, len(blobs))

	for _, blob := range blobs {
		r := &PublishedRepo{}
		if err := r.Decode(blob); err != nil {
			log.Printf("Error decoding published repo: %s\n", err)
		} else {
			collection.list = append(collection.list, r)
		}
	}
}

// Add appends new repo to collection and saves it
func (collection *PublishedRepoCollection) Add(repo *PublishedRepo) error {
	collection.loadList()

	if collection.CheckDuplicate(repo) != nil {
		return fmt.Errorf("published repo with storage/prefix/distribution %s/%s/%s already exists", repo.Storage, repo.Prefix, repo.Distribution)
	}

	err := collection.Update(repo)
	if err != nil {
		return err
	}

	collection.list = append(collection.list, repo)
	return nil
}

// CheckDuplicate verifies that there's no published repo with the same name
func (collection *PublishedRepoCollection) CheckDuplicate(repo *PublishedRepo) *PublishedRepo {
	collection.loadList()

	for _, r := range collection.list {
		if r.Prefix == repo.Prefix && r.Distribution == repo.Distribution && r.Storage == repo.Storage {
			return r
		}
	}

	return nil
}

// Update stores updated information about repo in DB
func (collection *PublishedRepoCollection) Update(repo *PublishedRepo) error {
	batch := collection.db.CreateBatch()
	_ = batch.Put(repo.Key(), repo.Encode())

	if repo.SourceKind == SourceLocalRepo {
		for component, item := range repo.sourceItems {
			_ = batch.Put(repo.RefKey(component), item.packageRefs.Encode())
		}
	}
	return batch.Write()
}

// LoadShallow loads basic information on the repo's sources
//
// This does not *fully* load in the sources themselves and their packages.
// It's useful if you just want to use JSON serialization without loading in unnecessary things.
func (collection *PublishedRepoCollection) LoadShallow(repo *PublishedRepo, collectionFactory *CollectionFactory) (err error) {
	repo.sourceItems = make(map[string]repoSourceItem)

	if repo.SourceKind == SourceSnapshot {
		for component, sourceUUID := range repo.Sources {
			item := repoSourceItem{}

			item.snapshot, err = collectionFactory.SnapshotCollection().ByUUID(sourceUUID)
			if err != nil {
				return
			}

			repo.sourceItems[component] = item
		}
	} else if repo.SourceKind == SourceLocalRepo {
		for component, sourceUUID := range repo.Sources {
			item := repoSourceItem{}

			item.localRepo, err = collectionFactory.LocalRepoCollection().ByUUID(sourceUUID)
			if err != nil {
				return
			}

			item.packageRefs = &PackageRefList{}
			repo.sourceItems[component] = item
		}
	} else {
		panic("unknown SourceKind")
	}

	return
}

// LoadComplete loads complete information on the sources of the repo *and* their packages
func (collection *PublishedRepoCollection) LoadComplete(repo *PublishedRepo, collectionFactory *CollectionFactory) (err error) {
	_ = collection.LoadShallow(repo, collectionFactory)

	if repo.SourceKind == SourceSnapshot {
		for _, item := range repo.sourceItems {
			err = collectionFactory.SnapshotCollection().LoadComplete(item.snapshot)
			if err != nil {
				return
			}
		}
	} else if repo.SourceKind == SourceLocalRepo {
		for component, item := range repo.sourceItems {
			err = collectionFactory.LocalRepoCollection().LoadComplete(item.localRepo)
			if err != nil {
				return
			}

			var encoded []byte
			encoded, err = collection.db.Get(repo.RefKey(component))
			if err != nil {
				// < 0.6 saving w/o component name
				if err == database.ErrNotFound && len(repo.Sources) == 1 {
					encoded, err = collection.db.Get(repo.RefKey(""))
				}

				if err != nil {
					return
				}
			}

			err = item.packageRefs.Decode(encoded)
			if err != nil {
				return
			}
		}
	} else {
		panic("unknown SourceKind")
	}

	return
}

// ByStoragePrefixDistribution looks up repository by storage, prefix & distribution
func (collection *PublishedRepoCollection) ByStoragePrefixDistribution(storage, prefix, distribution string) (*PublishedRepo, error) {
	collection.loadList()

	for _, r := range collection.list {
		if r.Prefix == prefix && r.Distribution == distribution && r.Storage == storage {
			return r, nil
		}
	}
	if storage != "" {
		storage += ":"
	}
	return nil, fmt.Errorf("published repo with storage:prefix/distribution %s%s/%s not found", storage, prefix, distribution)
}

// ByUUID looks up repository by uuid
func (collection *PublishedRepoCollection) ByUUID(uuid string) (*PublishedRepo, error) {
	collection.loadList()

	for _, r := range collection.list {
		if r.UUID == uuid {
			return r, nil
		}
	}
	return nil, fmt.Errorf("published repo with uuid %s not found", uuid)
}

// BySnapshot looks up repository by snapshot source
func (collection *PublishedRepoCollection) BySnapshot(snapshot *Snapshot) []*PublishedRepo {
	collection.loadList()

	var result []*PublishedRepo
	for _, r := range collection.list {
		if r.SourceKind == SourceSnapshot {
			if r.SourceUUID == snapshot.UUID {
				result = append(result, r)
			}

			for _, sourceUUID := range r.Sources {
				if sourceUUID == snapshot.UUID {
					result = append(result, r)
					break
				}
			}
		}
	}
	return result
}

// ByLocalRepo looks up repository by local repo source
func (collection *PublishedRepoCollection) ByLocalRepo(repo *LocalRepo) []*PublishedRepo {
	collection.loadList()

	var result []*PublishedRepo
	for _, r := range collection.list {
		if r.SourceKind == SourceLocalRepo {
			if r.SourceUUID == repo.UUID {
				result = append(result, r)
			}

			for _, sourceUUID := range r.Sources {
				if sourceUUID == repo.UUID {
					result = append(result, r)
					break
				}
			}
		}
	}
	return result
}

// ForEach runs method for each repository
func (collection *PublishedRepoCollection) ForEach(handler func(*PublishedRepo) error) error {
	return collection.db.ProcessByPrefix([]byte("U"), func(_, blob []byte) error {
		r := &PublishedRepo{}
		if err := r.Decode(blob); err != nil {
			log.Printf("Error decoding published repo: %s\n", err)
			return nil
		}

		return handler(r)
	})
}

// Len returns number of remote repos
func (collection *PublishedRepoCollection) Len() int {
	collection.loadList()

	return len(collection.list)
}

func (collection *PublishedRepoCollection) listReferencedFilesByComponent(prefix string, components []string,
	collectionFactory *CollectionFactory, progress aptly.Progress) (map[string][]string, error) {
	referencedFiles := map[string][]string{}
	processedComponentRefs := map[string]*PackageRefList{}

	for _, r := range collection.list {
		if r.Prefix == prefix && !r.MultiDist {
			matches := false

			repoComponents := r.Components()

			for _, component := range components {
				if utils.StrSliceHasItem(repoComponents, component) {
					matches = true
					break
				}
			}

			if !matches {
				continue
			}

			if err := collection.LoadComplete(r, collectionFactory); err != nil {
				return nil, err
			}

			for _, component := range components {
				if utils.StrSliceHasItem(repoComponents, component) {
					unseenRefs := r.RefList(component)
					processedRefs := processedComponentRefs[component]
					if processedRefs != nil {
						unseenRefs = unseenRefs.Subtract(processedRefs)
					} else {
						processedRefs = NewPackageRefList()
					}

					if unseenRefs.Len() == 0 {
						continue
					}
					processedComponentRefs[component] = processedRefs.Merge(unseenRefs, false, true)

					packageList, err := NewPackageListFromRefList(unseenRefs, collectionFactory.PackageCollection(), progress)
					if err != nil {
						return nil, err
					}

					_ = packageList.ForEach(func(p *Package) error {
						poolDir, err := p.PoolDirectory()
						if err != nil {
							return err
						}

						for _, f := range p.Files() {
							referencedFiles[component] = append(referencedFiles[component], filepath.Join(poolDir, f.Filename))
						}

						return nil
					})
				}
			}
		}
	}

	return referencedFiles, nil
}

// CleanupPrefixComponentFiles removes all unreferenced files in published storage under prefix/component pair
func (collection *PublishedRepoCollection) CleanupPrefixComponentFiles(publishedStorageProvider aptly.PublishedStorageProvider,
	published *PublishedRepo, cleanComponents []string, collectionFactory *CollectionFactory, progress aptly.Progress) error {

	var err error

	collection.loadList()

	storage := published.Storage
	prefix := published.Prefix
	distribution := published.Distribution

	rootPath := filepath.Join(prefix, "dists", distribution)
	publishedStorage := publishedStorageProvider.GetPublishedStorage(published.Storage)

	sort.Strings(cleanComponents)
	publishedComponents := published.Components()
	removedComponents := utils.StrSlicesSubstract(cleanComponents, publishedComponents)
	updatedComponents := utils.StrSlicesSubstract(cleanComponents, removedComponents)

	if progress != nil {
		progress.Printf("Cleaning up published repository %s/%s...\n", published.StoragePrefix(), distribution)
	}

	for _, component := range removedComponents {
		if progress != nil {
			progress.Printf("Removing component '%s'...\n", component)
		}

		err = publishedStorage.RemoveDirs(filepath.Join(rootPath, component), progress)
		if err != nil {
			return err
		}

		// Ensure that component does not exist in multi distribution pool
		err = publishedStorage.RemoveDirs(filepath.Join(prefix, "pool", distribution, component), nil)
		if err != nil {
			return err
		}
	}

	referencedFiles := map[string][]string{}

	if published.MultiDist {
		rootPath = filepath.Join(prefix, "pool", distribution)

		// Get all referenced files by component for determining orphaned pool files.
		for _, component := range publishedComponents {
			packageList, err := NewPackageListFromRefList(published.RefList(component), collectionFactory.PackageCollection(), progress)
			if err != nil {
				return err
			}

			_ = packageList.ForEach(func(p *Package) error {
				poolDir, err := p.PoolDirectory()
				if err != nil {
					return err
				}

				for _, file := range p.Files() {
					referencedFiles[component] = append(referencedFiles[component], filepath.Join(poolDir, file.Filename))
				}

				return nil
			})
		}
	} else {
		rootPath = filepath.Join(prefix, "pool")

		// In case of a shared component pool directory, we must check, if a component is no longer referenced by any other
		// published repository within the same prefix.
		referencedComponents := map[string]struct{}{}
		for _, p := range collection.list {
			if p.Prefix == prefix && p.Storage == storage && !p.MultiDist {
				for _, component := range p.Components() {
					referencedComponents[component] = struct{}{}
				}
			}
		}

		// Remove orphaned component pool directories in the prefix.
		for _, component := range removedComponents {
			_, exists := referencedComponents[component]
			if !exists {
				err := publishedStorage.RemoveDirs(filepath.Join(rootPath, component), progress)
				if err != nil {
					return err
				}
			}
		}

		// Get all referenced files by component for determining orphaned pool files.
		referencedFiles, err = collection.listReferencedFilesByComponent(prefix, publishedComponents, collectionFactory, progress)
		if err != nil {
			return err
		}
	}

	for _, component := range updatedComponents {
		if progress != nil {
			progress.Printf("Cleaning up component '%s'...\n", component)
		}
		sort.Strings(referencedFiles[component])

		path := filepath.Join(rootPath, component)
		existingFiles, err := publishedStorage.Filelist(path)
		if err != nil {
			return err
		}

		sort.Strings(existingFiles)

		orphanedFiles := utils.StrSlicesSubstract(existingFiles, referencedFiles[component])

		for _, file := range orphanedFiles {
			err = publishedStorage.Remove(filepath.Join(path, file))
			if err != nil {
				return err
			}
		}
	}

	return err
}

// Remove removes published repository, cleaning up directories, files
func (collection *PublishedRepoCollection) Remove(publishedStorageProvider aptly.PublishedStorageProvider,
	storage, prefix, distribution string, collectionFactory *CollectionFactory, progress aptly.Progress,
	force, skipCleanup bool) error {

	// TODO: load via transaction
	collection.loadList()

	repo, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution)
	if err != nil {
		return err
	}

	err = collection.LoadComplete(repo, collectionFactory)
	if err != nil {
		return err
	}

	removePrefix := true
	removePoolComponents := repo.Components()
	cleanComponents := []string{}
	repoPosition := -1

	for i, r := range collection.list {
		if r == repo {
			repoPosition = i
			continue
		}
		if r.Storage == repo.Storage && r.Prefix == repo.Prefix {
			removePrefix = false

			rComponents := r.Components()
			for _, component := range rComponents {
				if utils.StrSliceHasItem(removePoolComponents, component) {
					removePoolComponents = utils.StrSlicesSubstract(removePoolComponents, []string{component})
					cleanComponents = append(cleanComponents, component)
				}
			}
		}
	}

	err = repo.RemoveFiles(publishedStorageProvider, removePrefix, removePoolComponents, progress)
	if err != nil {
		if !force {
			return fmt.Errorf("published files removal failed, use -force-drop to override: %s", err)
		}
		// ignore error with -force-drop
	}

	collection.list[len(collection.list)-1], collection.list[repoPosition], collection.list =
		nil, collection.list[len(collection.list)-1], collection.list[:len(collection.list)-1]

	if !skipCleanup && len(cleanComponents) > 0 {
		err = collection.CleanupPrefixComponentFiles(publishedStorageProvider, repo, cleanComponents, collectionFactory, progress)
		if err != nil {
			if !force {
				return fmt.Errorf("cleanup failed, use -force-drop to override: %s", err)
			}
		}
	}

	batch := collection.db.CreateBatch()
	_ = batch.Delete(repo.Key())

	for _, component := range repo.Components() {
		_ = batch.Delete(repo.RefKey(component))
	}

	return batch.Write()
}
