package containerized

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/containerd/containerd"
	"github.com/containerd/containerd/containers"
	"github.com/containerd/containerd/diff/apply"
	"github.com/containerd/containerd/mount"
	"github.com/containerd/containerd/rootfs"
	"github.com/containerd/containerd/snapshots"
	"github.com/opencontainers/image-spec/identity"
)

const (
	gcRoot           = "containerd.io/gc.root"
	timestampFormat  = "01-02-2006-15:04:05"
	previousRevision = "docker.com/revision.previous"
	imageLabel       = "docker.com/revision.image"
)

// ErrNoPreviousRevision returned if the container has to previous revision
var ErrNoPreviousRevision = errors.New("no previous revision")

// WithNewSnapshot creates a new snapshot managed by containerized
func WithNewSnapshot(i containerd.Image) containerd.NewContainerOpts {
	return func(ctx context.Context, client *containerd.Client, c *containers.Container) error {
		if c.Snapshotter == "" {
			c.Snapshotter = containerd.DefaultSnapshotter
		}
		r, err := create(ctx, client, i, c.ID, "")
		if err != nil {
			return err
		}
		c.SnapshotKey = r.Key
		c.Image = i.Name()
		return nil
	}
}

// WithUpgrade upgrades an existing container's image to a new one
func WithUpgrade(i containerd.Image) containerd.UpdateContainerOpts {
	return func(ctx context.Context, client *containerd.Client, c *containers.Container) error {
		revision, err := save(ctx, client, i, c)
		if err != nil {
			return err
		}
		c.Image = i.Name()
		err = updateConfig(c.ID, c.Image)
		if err != nil {
			return err
		}
		c.SnapshotKey = revision.Key
		return nil
	}
}

// WithRollback rolls back to the previous container's revision
func WithRollback(ctx context.Context, client *containerd.Client, c *containers.Container) error {
	prev, err := previous(ctx, client, c)
	if err != nil {
		return err
	}
	ss := client.SnapshotService(c.Snapshotter)
	sInfo, err := ss.Stat(ctx, prev.Key)
	if err != nil {
		return err
	}
	snapshotImage, ok := sInfo.Labels[imageLabel]
	if !ok {
		return fmt.Errorf("snapshot %s does not have a service image label", prev.Key)
	}
	if snapshotImage == "" {
		return fmt.Errorf("snapshot %s has an empty service image label", prev.Key)
	}
	c.Image = snapshotImage
	err = updateConfig(c.ID, c.Image)
	if err != nil {
		return err
	}
	c.SnapshotKey = prev.Key
	return nil
}

func newRevision(id string) *revision {
	now := time.Now()
	return &revision{
		Timestamp: now,
		Key:       fmt.Sprintf("boss.io.%s.%s", id, now.Format(timestampFormat)),
	}
}

type revision struct {
	Timestamp time.Time
	Key       string
	mounts    []mount.Mount
}

// nolint: interfacer
func create(ctx context.Context, client *containerd.Client, i containerd.Image, id string, previous string) (*revision, error) {
	diffIDs, err := i.RootFS(ctx)
	if err != nil {
		return nil, err
	}
	var (
		parent = identity.ChainID(diffIDs).String()
		r      = newRevision(id)
	)
	labels := map[string]string{
		gcRoot:     r.Timestamp.Format(time.RFC3339),
		imageLabel: i.Name(),
	}
	if previous != "" {
		labels[previousRevision] = previous
	}
	mounts, err := client.SnapshotService(containerd.DefaultSnapshotter).Prepare(ctx, r.Key, parent, snapshots.WithLabels(labels))
	if err != nil {
		return nil, err
	}
	r.mounts = mounts
	return r, nil
}

func save(ctx context.Context, client *containerd.Client, updatedImage containerd.Image, c *containers.Container) (*revision, error) {
	snapshot, err := create(ctx, client, updatedImage, c.ID, c.SnapshotKey)
	if err != nil {
		return nil, err
	}
	service := client.SnapshotService(c.Snapshotter)
	// create a diff from the existing snapshot
	diff, err := rootfs.CreateDiff(ctx, c.SnapshotKey, service, client.DiffService())
	if err != nil {
		return nil, err
	}
	applier := apply.NewFileSystemApplier(client.ContentStore())
	if _, err := applier.Apply(ctx, diff, snapshot.mounts); err != nil {
		return nil, err
	}
	return snapshot, nil
}

// nolint: interfacer
func previous(ctx context.Context, client *containerd.Client, c *containers.Container) (*revision, error) {
	service := client.SnapshotService(c.Snapshotter)
	sInfo, err := service.Stat(ctx, c.SnapshotKey)
	if err != nil {
		return nil, err
	}
	key := sInfo.Labels[previousRevision]
	if key == "" {
		return nil, ErrNoPreviousRevision
	}
	parts := strings.Split(key, ".")
	timestamp, err := time.Parse(timestampFormat, parts[len(parts)-1])
	if err != nil {
		return nil, err
	}
	return &revision{
		Timestamp: timestamp,
		Key:       key,
	}, nil
}
