/*
   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"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/containerd/containerd/v2/core/metadata/boltutil"
	api "github.com/containerd/containerd/v2/core/sandbox"
	"github.com/containerd/containerd/v2/pkg/filters"
	"github.com/containerd/containerd/v2/pkg/identifiers"
	"github.com/containerd/containerd/v2/pkg/namespaces"
	"github.com/containerd/containerd/v2/pkg/tracing"
	"github.com/containerd/errdefs"
	"github.com/containerd/typeurl/v2"
	"go.etcd.io/bbolt"
	errbolt "go.etcd.io/bbolt/errors"
)

const (
	spanSandboxPrefix = "metadata.sandbox"
)

type sandboxStore struct {
	db *DB
}

var _ api.Store = (*sandboxStore)(nil)

// NewSandboxStore creates a datababase client for sandboxes
func NewSandboxStore(db *DB) api.Store {
	return &sandboxStore{db: db}
}

// Create a sandbox record in the store
func (s *sandboxStore) Create(ctx context.Context, sandbox api.Sandbox) (api.Sandbox, error) {
	ctx, span := tracing.StartSpan(ctx,
		tracing.Name(spanSandboxPrefix, "Create"),
		tracing.WithAttribute("sandbox.id", sandbox.ID),
	)
	defer span.End()
	ns, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return api.Sandbox{}, err
	}

	sandbox.CreatedAt = time.Now().UTC()
	sandbox.UpdatedAt = sandbox.CreatedAt

	if err := s.validate(&sandbox); err != nil {
		return api.Sandbox{}, fmt.Errorf("failed to validate sandbox: %w", err)
	}

	if err := update(ctx, s.db, func(tx *bbolt.Tx) error {
		parent, err := createSandboxBucket(tx, ns)
		if err != nil {
			return fmt.Errorf("create error: %w", err)
		}

		if err := s.write(parent, &sandbox, false); err != nil {
			return fmt.Errorf("write error: %w", err)
		}

		span.SetAttributes(
			tracing.Attribute("sandbox.CreatedAt", sandbox.CreatedAt.Format(time.RFC3339)),
		)
		return nil
	}); err != nil {
		return api.Sandbox{}, err
	}

	return sandbox, nil
}

// Update the sandbox with the provided sandbox object and fields
func (s *sandboxStore) Update(ctx context.Context, sandbox api.Sandbox, fieldpaths ...string) (api.Sandbox, error) {
	ctx, span := tracing.StartSpan(ctx,
		tracing.Name(spanSandboxPrefix, "Update"),
		tracing.WithAttribute("sandbox.id", sandbox.ID),
	)
	defer span.End()
	ns, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return api.Sandbox{}, err
	}

	ret := api.Sandbox{}
	if err := update(ctx, s.db, func(tx *bbolt.Tx) error {
		parent := getSandboxBucket(tx, ns)
		if parent == nil {
			return fmt.Errorf("no sandbox buckets: %w", errdefs.ErrNotFound)
		}

		updated, err := s.read(parent, []byte(sandbox.ID))
		if err != nil {
			return err
		}

		if len(fieldpaths) == 0 {
			fieldpaths = []string{"labels", "extensions", "spec", "runtime"}

			if updated.Runtime.Name != sandbox.Runtime.Name {
				return fmt.Errorf("sandbox.Runtime.Name field is immutable: %w", errdefs.ErrInvalidArgument)
			}
		}

		for _, path := range fieldpaths {
			if strings.HasPrefix(path, "labels.") {
				if updated.Labels == nil {
					updated.Labels = map[string]string{}
				}

				key := strings.TrimPrefix(path, "labels.")
				updated.Labels[key] = sandbox.Labels[key]
				continue
			} else if strings.HasPrefix(path, "extensions.") {
				if updated.Extensions == nil {
					updated.Extensions = map[string]typeurl.Any{}
				}

				key := strings.TrimPrefix(path, "extensions.")
				updated.Extensions[key] = sandbox.Extensions[key]
				continue
			}

			switch path {
			case "labels":
				updated.Labels = sandbox.Labels
			case "extensions":
				updated.Extensions = sandbox.Extensions
			case "runtime":
				updated.Runtime = sandbox.Runtime
			case "spec":
				updated.Spec = sandbox.Spec
			default:
				return fmt.Errorf("cannot update %q field on sandbox %q: %w", path, sandbox.ID, errdefs.ErrInvalidArgument)
			}
		}

		updated.UpdatedAt = time.Now().UTC()

		if err := s.write(parent, &updated, true); err != nil {
			return err
		}

		span.SetAttributes(
			tracing.Attribute("sandbox.CreatedAt", updated.CreatedAt.Format(time.RFC3339)),
			tracing.Attribute("sandbox.UpdatedAt", updated.UpdatedAt.Format(time.RFC3339)),
		)
		ret = updated
		return nil
	}); err != nil {
		return api.Sandbox{}, err
	}

	return ret, nil
}

// Get sandbox metadata using the id
func (s *sandboxStore) Get(ctx context.Context, id string) (api.Sandbox, error) {
	ns, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return api.Sandbox{}, err
	}

	ret := api.Sandbox{}
	if err := view(ctx, s.db, func(tx *bbolt.Tx) error {
		bucket := getSandboxBucket(tx, ns)
		if bucket == nil {
			return fmt.Errorf("no sandbox buckets: %w", errdefs.ErrNotFound)
		}

		out, err := s.read(bucket, []byte(id))
		if err != nil {
			return err
		}

		ret = out
		return nil
	}); err != nil {
		return api.Sandbox{}, err
	}

	return ret, nil
}

// List returns sandboxes that match one or more of the provided filters
func (s *sandboxStore) List(ctx context.Context, fields ...string) ([]api.Sandbox, error) {
	ns, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return nil, err
	}

	filter, err := filters.ParseAll(fields...)
	if err != nil {
		return nil, fmt.Errorf("%s: %w", err.Error(), errdefs.ErrInvalidArgument)
	}

	var (
		list []api.Sandbox
	)

	if err := view(ctx, s.db, func(tx *bbolt.Tx) error {
		bucket := getSandboxBucket(tx, ns)
		if bucket == nil {
			// We haven't created any sandboxes yet, just return empty list
			return nil
		}

		if err := bucket.ForEach(func(k, v []byte) error {
			info, err := s.read(bucket, k)
			if err != nil {
				return fmt.Errorf("failed to read bucket %q: %w", string(k), err)
			}

			if filter.Match(adaptSandbox(&info)) {
				list = append(list, info)
			}

			return nil
		}); err != nil {
			return err
		}

		return nil
	}); err != nil {
		return nil, err
	}

	return list, nil
}

// Delete a sandbox from metadata store using the id
func (s *sandboxStore) Delete(ctx context.Context, id string) error {
	ctx, span := tracing.StartSpan(ctx,
		tracing.Name(spanSandboxPrefix, "Delete"),
		tracing.WithAttribute("sandbox.id", id),
	)
	defer span.End()
	ns, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return err
	}

	if err := update(ctx, s.db, func(tx *bbolt.Tx) error {
		buckets := getSandboxBucket(tx, ns)
		if buckets == nil {
			return fmt.Errorf("no sandbox buckets: %w", errdefs.ErrNotFound)
		}

		if err := buckets.DeleteBucket([]byte(id)); err != nil {
			if err == errbolt.ErrBucketNotFound {
				err = errdefs.ErrNotFound
			}
			return fmt.Errorf("failed to delete sandbox %q: %w", id, err)
		}

		return nil
	}); err != nil {
		return err
	}

	return nil
}

func (s *sandboxStore) write(parent *bbolt.Bucket, instance *api.Sandbox, overwrite bool) error {
	if err := s.validate(instance); err != nil {
		return err
	}

	var (
		bucket *bbolt.Bucket
		err    error
		id     = []byte(instance.ID)
	)

	if overwrite {
		bucket, err = parent.CreateBucketIfNotExists(id)
		if err != nil {
			return err
		}
	} else {
		bucket, err = parent.CreateBucket(id)
		if err != nil {
			if err == errbolt.ErrBucketExists {
				return fmt.Errorf("sandbox bucket %q already exists: %w", instance.ID, errdefs.ErrAlreadyExists)
			}
			return err
		}
	}

	if err := boltutil.WriteTimestamps(bucket, instance.CreatedAt, instance.UpdatedAt); err != nil {
		return err
	}

	if err := boltutil.WriteLabels(bucket, instance.Labels); err != nil {
		return err
	}

	if err := boltutil.WriteExtensions(bucket, instance.Extensions); err != nil {
		return err
	}

	if err := boltutil.WriteAny(bucket, bucketKeySpec, instance.Spec); err != nil {
		return err
	}

	if err := bucket.Put(bucketKeySandboxer, []byte(instance.Sandboxer)); err != nil {
		return err
	}

	runtimeBucket, err := bucket.CreateBucketIfNotExists(bucketKeyRuntime)
	if err != nil {
		return err
	}

	if err := runtimeBucket.Put(bucketKeyName, []byte(instance.Runtime.Name)); err != nil {
		return err
	}

	if err := boltutil.WriteAny(runtimeBucket, bucketKeyOptions, instance.Runtime.Options); err != nil {
		return err
	}

	return nil
}

func (s *sandboxStore) read(parent *bbolt.Bucket, id []byte) (api.Sandbox, error) {
	var (
		inst api.Sandbox
		err  error
	)

	bucket := parent.Bucket(id)
	if bucket == nil {
		return api.Sandbox{}, fmt.Errorf("bucket %q not found: %w", id, errdefs.ErrNotFound)
	}

	inst.ID = string(id)

	inst.Labels, err = boltutil.ReadLabels(bucket)
	if err != nil {
		return api.Sandbox{}, err
	}

	if err := boltutil.ReadTimestamps(bucket, &inst.CreatedAt, &inst.UpdatedAt); err != nil {
		return api.Sandbox{}, err
	}

	inst.Spec, err = boltutil.ReadAny(bucket, bucketKeySpec)
	if err != nil {
		return api.Sandbox{}, err
	}

	runtimeBucket := bucket.Bucket(bucketKeyRuntime)
	if runtimeBucket == nil {
		return api.Sandbox{}, errors.New("no runtime bucket")
	}

	inst.Runtime.Name = string(runtimeBucket.Get(bucketKeyName))
	inst.Runtime.Options, err = boltutil.ReadAny(runtimeBucket, bucketKeyOptions)
	if err != nil {
		return api.Sandbox{}, err
	}

	inst.Extensions, err = boltutil.ReadExtensions(bucket)
	if err != nil {
		return api.Sandbox{}, err
	}
	sandboxer := bucket.Get(bucketKeySandboxer)
	if sandboxer == nil {
		inst.Sandboxer = ""
	} else {
		inst.Sandboxer = string(sandboxer)
	}

	return inst, nil
}

func (s *sandboxStore) validate(new *api.Sandbox) error {
	if err := identifiers.Validate(new.ID); err != nil {
		return fmt.Errorf("invalid sandbox ID: %w", err)
	}

	if new.CreatedAt.IsZero() {
		return fmt.Errorf("creation date must not be zero: %w", errdefs.ErrInvalidArgument)
	}

	if new.UpdatedAt.IsZero() {
		return fmt.Errorf("updated date must not be zero: %w", errdefs.ErrInvalidArgument)
	}

	return nil
}
