package data

import (
	"encoding/json"
	"errors"
	"slices"
	"strings"
)

// Default implementation of IRepository using an IDatastore.
type DefaultRepository[T IIdentifiable] struct {
    ds      IDatastore
    prefix  string
}

// Creates a new DefaultRepository for a generic type T given a IDatastore and
// a prefix. The type T must implement the IIdentifiable interface. The prefix
// will be used for the key for every entry using the created repository. The
// prefix should be not yet be in use in the given datastore and not contain a
// ':' character.
func NewDefaultRepository[T IIdentifiable](ds IDatastore, prefix string) (*DefaultRepository[T], error) {
    if strings.Contains(prefix, ":") { return nil, errors.New("prefix should not contain ':'") }
    return &DefaultRepository[T]{ ds: ds, prefix: prefix }, nil
}

// Creates a new entry in the repository with the given object t. Throws an
// error if there already exists an entry with the same id, the json encoding
// fails or the connection to the IDatastore fails.
func (repo *DefaultRepository[T]) Create(t T) error {
    key := repo.prefix + ":" + t.Id()
    exists, err := repo.ds.KeyExists(key)
    if err != nil { return err }
    if exists { return errors.New("entry with given id already exists") }

    d, err := json.Marshal(t)
    if err != nil { return err }
    
    err = repo.ds.Set(key, string(d))
    if err != nil { return err }

    return nil
}

// Updates the entry with the same id as t in the repository with the values of
// t. Trows an error if the json encoding fails or the connection to the
// IDatastore fails.
func (repo *DefaultRepository[T]) Update(t T) error {
    key := repo.prefix + ":" + t.Id()
    exists, err := repo.ds.KeyExists(key)
    if err != nil { return err }
    if !exists { return errors.New("no entry with given id") }

    d, err := json.Marshal(t)
    if err != nil { return err }
    
    err = repo.ds.Set(key, string(d))
    if err != nil { return err }

    return nil
}

// Delete the entry with the same id as t in the repository. Trows an error if
// the connection to the IDatastore fails or the key of t does not exist.
func (repo *DefaultRepository[T]) Delete(t T) error {
    key := repo.prefix + ":" + t.Id()

    exists, err := repo.ds.KeyExists(key)
    if err != nil { return err }
    if !exists { return errors.New("no entry with given id") }

    err = repo.ds.Delete(key)
    if err != nil { return err }

    return nil
}

// Get all the objects of type T from the repository as a list. Trows an error
// if the connection to the IDatastore fails.
func (repo *DefaultRepository[T]) GetAll() ([]T, error) {
    out := make([]T, 0)
    
    allkeys, err := repo.ds.GetAllKeys()
    if err != nil { return nil, err }

    for key, _ := range allkeys {
        splitkey := strings.Split(key, ":")
        if splitkey[0] == repo.prefix {

            // retrieve the object
            obj, err := repo.GetById(splitkey[1])
            if err != nil { return nil, err }
            out = append(out, obj)

        }
    }

    return out, nil
}

// Get the objects of type T from the repository that has the given id. Trows an error
// if the connection to the IDatastore or the decoding process fails.
func (repo *DefaultRepository[T]) GetById(id string) (T, error) {
    var obj T

    key := repo.prefix + ":" + id
    value, err := repo.ds.Get(key)
    if err != nil { return obj, err }
    
    err = json.Unmarshal([]byte(value), &obj)
    if err != nil { return obj, err }

    return obj, nil
}

// Returns a slice of all elememts in the repo that have a
// ISearchCriteria.Weight greater than 0 sort by that weight. Throws an error
// when the elememts cannot be retrieved from the repo.
func (repo *DefaultRepository[T]) GetByCriteria(c ISortCriteria[T]) ([]T, error) {
    all, err := repo.GetAll()
    if err != nil { return nil, err }

    filtered := make([]T, 0)
    for _, elem := range all {
        if c.Weight(elem) > 0 { filtered = append(filtered, elem) }
    }

    slices.SortFunc(filtered, func(a, b T) int {
        wa, wb := c.Weight(a), c.Weight(b)
        if wa > wb { return -1 }
        return 1
    })

    return filtered, nil
}
