/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package metadata

import (
	"context"
	"fmt"
	"strings"

	"github.com/containerd/containerd/v2/pkg/identifiers"
	l "github.com/containerd/containerd/v2/pkg/labels"
	"github.com/containerd/containerd/v2/pkg/namespaces"
	"github.com/containerd/errdefs"
	bolt "go.etcd.io/bbolt"
	errbolt "go.etcd.io/bbolt/errors"
)

type namespaceStore struct {
	tx *bolt.Tx
}

// NewNamespaceStore returns a store backed by a bolt DB
func NewNamespaceStore(tx *bolt.Tx) namespaces.Store {
	return &namespaceStore{tx: tx}
}

func (s *namespaceStore) Create(ctx context.Context, namespace string, labels map[string]string) error {
	topbkt, err := createBucketIfNotExists(s.tx, bucketKeyVersion)
	if err != nil {
		return err
	}

	if err := identifiers.Validate(namespace); err != nil {
		return err
	}

	for k, v := range labels {
		if err := l.Validate(k, v); err != nil {
			return fmt.Errorf("namespace.Labels: %w", err)
		}
	}

	// provides the already exists error.
	bkt, err := topbkt.CreateBucket([]byte(namespace))
	if err != nil {
		if err == errbolt.ErrBucketExists {
			return fmt.Errorf("namespace %q: %w", namespace, errdefs.ErrAlreadyExists)
		}

		return err
	}

	lbkt, err := bkt.CreateBucketIfNotExists(bucketKeyObjectLabels)
	if err != nil {
		return err
	}

	for k, v := range labels {
		if err := lbkt.Put([]byte(k), []byte(v)); err != nil {
			return err
		}
	}

	return nil
}

func (s *namespaceStore) Labels(ctx context.Context, namespace string) (map[string]string, error) {
	labels := map[string]string{}

	bkt := getNamespaceLabelsBucket(s.tx, namespace)
	if bkt == nil {
		return labels, nil
	}

	if err := bkt.ForEach(func(k, v []byte) error {
		labels[string(k)] = string(v)
		return nil
	}); err != nil {
		return nil, err
	}

	return labels, nil
}

func (s *namespaceStore) SetLabel(ctx context.Context, namespace, key, value string) error {
	if err := l.Validate(key, value); err != nil {
		return fmt.Errorf("namespace.Labels: %w", err)
	}

	return withNamespacesLabelsBucket(s.tx, namespace, func(bkt *bolt.Bucket) error {
		if value == "" {
			return bkt.Delete([]byte(key))
		}

		return bkt.Put([]byte(key), []byte(value))
	})

}

func (s *namespaceStore) List(ctx context.Context) ([]string, error) {
	bkt := getBucket(s.tx, bucketKeyVersion)
	if bkt == nil {
		return nil, nil // no namespaces!
	}

	var namespaces []string
	if err := bkt.ForEach(func(k, v []byte) error {
		if v != nil {
			return nil // not a bucket
		}

		namespaces = append(namespaces, string(k))
		return nil
	}); err != nil {
		return nil, err
	}

	return namespaces, nil
}

func (s *namespaceStore) Delete(ctx context.Context, namespace string, opts ...namespaces.DeleteOpts) error {
	i := &namespaces.DeleteInfo{
		Name: namespace,
	}
	for _, o := range opts {
		if err := o(ctx, i); err != nil {
			return err
		}
	}
	bkt := getBucket(s.tx, bucketKeyVersion)
	types, err := s.listNs(namespace)
	if err != nil {
		return err
	}

	if len(types) > 0 {
		return fmt.Errorf(
			"namespace %q must be empty, but it still has %s: %w",
			namespace, strings.Join(types, ", "),
			errdefs.ErrFailedPrecondition,
		)
	}

	if err := bkt.DeleteBucket([]byte(namespace)); err != nil {
		if err == errbolt.ErrBucketNotFound {
			return fmt.Errorf("namespace %q: %w", namespace, errdefs.ErrNotFound)
		}

		return err
	}

	return nil
}

// listNs returns the types of the remaining objects inside the given namespace.
// It doesn't return exact objects due to performance concerns.
func (s *namespaceStore) listNs(namespace string) ([]string, error) {
	var out []string

	if !isBucketEmpty(getImagesBucket(s.tx, namespace)) {
		out = append(out, "images")
	}
	if !isBucketEmpty(getBlobsBucket(s.tx, namespace)) {
		out = append(out, "blobs")
	}
	if !isBucketEmpty(getContainersBucket(s.tx, namespace)) {
		out = append(out, "containers")
	}

	if snbkt := getSnapshottersBucket(s.tx, namespace); snbkt != nil {
		if err := snbkt.ForEach(func(k, v []byte) error {
			if v == nil {
				if !isBucketEmpty(snbkt.Bucket(k)) {
					out = append(out, fmt.Sprintf("snapshots on %q snapshotter", k))
				}
			}
			return nil
		}); err != nil {
			return nil, err
		}
	}

	return out, nil
}

func isBucketEmpty(bkt *bolt.Bucket) bool {
	if bkt == nil {
		return true
	}

	k, _ := bkt.Cursor().First()
	return k == nil
}
