package deb

import (
	"bytes"
	"errors"
	"fmt"
	"log"

	"github.com/aptly-dev/aptly/database"
	"github.com/google/uuid"
	"github.com/ugorji/go/codec"
)

// LocalRepo is a collection of packages created locally
type LocalRepo struct {
	// Permanent internal ID
	UUID string `codec:"UUID" json:"-"`
	// User-assigned name
	Name string
	// Comment
	Comment string
	// DefaultDistribution
	DefaultDistribution string `codec:",omitempty"`
	// DefaultComponent
	DefaultComponent string `codec:",omitempty"`
	// Uploaders configuration
	Uploaders *Uploaders `codec:"Uploaders,omitempty" json:"-"`
	// "Snapshot" of current list of packages
	packageRefs *PackageRefList
}

// NewLocalRepo creates new instance of Debian local repository
func NewLocalRepo(name string, comment string) *LocalRepo {
	return &LocalRepo{
		UUID:    uuid.NewString(),
		Name:    name,
		Comment: comment,
	}
}

// String interface
func (repo *LocalRepo) String() string {
	if repo.Comment != "" {
		return fmt.Sprintf("[%s]: %s", repo.Name, repo.Comment)
	}
	return fmt.Sprintf("[%s]", repo.Name)
}

// NumPackages return number of packages in local repo
func (repo *LocalRepo) NumPackages() int {
	if repo.packageRefs == nil {
		return 0
	}
	return repo.packageRefs.Len()
}

// RefList returns package list for repo
func (repo *LocalRepo) RefList() *PackageRefList {
	return repo.packageRefs
}

// UpdateRefList changes package list for local repo
func (repo *LocalRepo) UpdateRefList(reflist *PackageRefList) {
	repo.packageRefs = reflist
}

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

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

	return buf.Bytes()
}

// Decode decodes msgpack representation into LocalRepo
func (repo *LocalRepo) Decode(input []byte) error {
	decoder := codec.NewDecoderBytes(input, &codec.MsgpackHandle{})
	return decoder.Decode(repo)
}

// Key is a unique id in DB
func (repo *LocalRepo) Key() []byte {
	return []byte("L" + repo.UUID)
}

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

// LocalRepoCollection does listing, updating/adding/deleting of LocalRepos
type LocalRepoCollection struct {
	db    database.Storage
	cache map[string]*LocalRepo
}

// NewLocalRepoCollection loads LocalRepos from DB and makes up collection
func NewLocalRepoCollection(db database.Storage) *LocalRepoCollection {
	return &LocalRepoCollection{
		db:    db,
		cache: make(map[string]*LocalRepo),
	}
}

func (collection *LocalRepoCollection) search(filter func(*LocalRepo) bool, unique bool) []*LocalRepo {
	result := []*LocalRepo(nil)
	for _, r := range collection.cache {
		if filter(r) {
			result = append(result, r)
		}
	}

	if unique && len(result) > 0 {
		return result
	}

	_ = collection.db.ProcessByPrefix([]byte("L"), func(_, blob []byte) error {
		r := &LocalRepo{}
		if err := r.Decode(blob); err != nil {
			log.Printf("Error decoding local repo: %s\n", err)
			return nil
		}

		if filter(r) {
			if _, exists := collection.cache[r.UUID]; !exists {
				collection.cache[r.UUID] = r
				result = append(result, r)
				if unique {
					return errors.New("abort")
				}
			}
		}

		return nil
	})

	return result
}

// Add appends new repo to collection and saves it
func (collection *LocalRepoCollection) Add(repo *LocalRepo) error {
	_, err := collection.ByName(repo.Name)

	if err == nil {
		return fmt.Errorf("local repo with name %s already exists", repo.Name)
	}

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

	collection.cache[repo.UUID] = repo
	return nil
}

// Update stores updated information about repo in DB
func (collection *LocalRepoCollection) Update(repo *LocalRepo) error {
	batch := collection.db.CreateBatch()
	_ = batch.Put(repo.Key(), repo.Encode())
	if repo.packageRefs != nil {
		_ = batch.Put(repo.RefKey(), repo.packageRefs.Encode())
	}
	return batch.Write()
}

// LoadComplete loads additional information for local repo
func (collection *LocalRepoCollection) LoadComplete(repo *LocalRepo) error {
	encoded, err := collection.db.Get(repo.RefKey())
	if err == database.ErrNotFound {
		return nil
	}
	if err != nil {
		return err
	}

	repo.packageRefs = &PackageRefList{}
	return repo.packageRefs.Decode(encoded)
}

// ByName looks up repository by name
func (collection *LocalRepoCollection) ByName(name string) (*LocalRepo, error) {
	result := collection.search(func(r *LocalRepo) bool { return r.Name == name }, true)
	if len(result) == 0 {
		return nil, fmt.Errorf("local repo with name %s not found", name)
	}

	return result[0], nil
}

// ByUUID looks up repository by uuid
func (collection *LocalRepoCollection) ByUUID(uuid string) (*LocalRepo, error) {
	if r, ok := collection.cache[uuid]; ok {
		return r, nil
	}

	key := (&LocalRepo{UUID: uuid}).Key()

	value, err := collection.db.Get(key)
	if err == database.ErrNotFound {
		return nil, fmt.Errorf("local repo with uuid %s not found", uuid)
	}

	if err != nil {
		return nil, err
	}

	r := &LocalRepo{}
	err = r.Decode(value)

	if err == nil {
		collection.cache[r.UUID] = r
	}

	return r, err
}

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

		return handler(r)
	})
}

// Len returns number of remote repos
func (collection *LocalRepoCollection) Len() int {
	return len(collection.db.KeysByPrefix([]byte("L")))
}

// Drop removes remote repo from collection
func (collection *LocalRepoCollection) Drop(repo *LocalRepo) error {
	if _, err := collection.db.Get(repo.Key()); err != nil {
		if err == database.ErrNotFound {
			return errors.New("local repo not found")
		}

		return err
	}
	delete(collection.cache, repo.UUID)

	batch := collection.db.CreateBatch()
	_ = batch.Delete(repo.Key())
	_ = batch.Delete(repo.RefKey())
	return batch.Write()
}
