package migration

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"sync"
	"time"

	"github.com/containerd/containerd/v2/core/content"
	"github.com/containerd/containerd/v2/core/images"
	"github.com/containerd/containerd/v2/core/leases"
	"github.com/containerd/containerd/v2/core/mount"
	"github.com/containerd/containerd/v2/core/snapshots"
	"github.com/containerd/containerd/v2/pkg/archive/compression"
	"github.com/containerd/continuity/fs"
	cerrdefs "github.com/containerd/errdefs"
	"github.com/containerd/log"
	"github.com/moby/moby/v2/daemon/internal/image"
	"github.com/moby/moby/v2/daemon/internal/layer"
	refstore "github.com/moby/moby/v2/daemon/internal/refstore"
	"github.com/opencontainers/go-digest"
	"github.com/opencontainers/image-spec/identity"
	"github.com/opencontainers/image-spec/specs-go"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"golang.org/x/sync/errgroup"
)

type LayerMigrator struct {
	layers  layer.Store
	refs    refstore.Store
	dis     image.Store
	leases  leases.Manager
	content content.Store
	cis     images.Store
}

type Config struct {
	ImageCount       int
	LayerStore       layer.Store
	ReferenceStore   refstore.Store
	DockerImageStore image.Store
	Leases           leases.Manager
	Content          content.Store
	ImageStore       images.Store
}

func NewLayerMigrator(config Config) *LayerMigrator {
	return &LayerMigrator{
		layers:  config.LayerStore,
		refs:    config.ReferenceStore,
		dis:     config.DockerImageStore,
		leases:  config.Leases,
		content: config.Content,
		cis:     config.ImageStore,
	}
}

// MigrateTocontainerd migrates containers from overlay2 to overlayfs or vfs to native
func (lm *LayerMigrator) MigrateTocontainerd(ctx context.Context, snKey string, sn snapshots.Snapshotter) error {
	if sn == nil {
		return fmt.Errorf("no snapshotter to migrate to: %w", cerrdefs.ErrNotImplemented)
	}

	switch driver := lm.layers.DriverName(); driver {
	case "overlay2":
	case "vfs":
	default:
		return fmt.Errorf("%q not supported for migration: %w", driver, cerrdefs.ErrNotImplemented)
	}

	var (
		// Zstd makes migration 10x faster
		// TODO: make configurable
		layerMediaType   = ocispec.MediaTypeImageLayerZstd
		layerCompression = compression.Zstd
	)

	l, err := lm.leases.Create(ctx, leases.WithRandomID(), leases.WithExpiration(24*time.Hour))
	if err != nil {
		return err
	}
	defer func() {
		lm.leases.Delete(ctx, l)
	}()
	ctx = leases.WithLease(ctx, l.ID)

	for imgID, img := range lm.dis.Heads() {
		diffids := img.RootFS.DiffIDs
		if len(diffids) == 0 {
			continue
		}
		var (
			parent   string
			manifest = ocispec.Manifest{
				MediaType: ocispec.MediaTypeImageManifest,
				Versioned: specs.Versioned{
					SchemaVersion: 2,
				},
				Layers: make([]ocispec.Descriptor, len(diffids)),
			}
			ml        sync.Mutex
			eg, egctx = errgroup.WithContext(ctx)
		)
		for i := range diffids {
			chainID := identity.ChainID(diffids[:i+1])
			l, err := lm.layers.Get(chainID)
			if err != nil {
				return fmt.Errorf("failed to get layer [%d] %q: %w", i, chainID, err)
			}
			layerIndex := i
			eg.Go(func() error {
				ctx := egctx
				t1 := time.Now()
				ts, err := l.TarStream()
				if err != nil {
					return err
				}

				desc := ocispec.Descriptor{
					MediaType: layerMediaType,
				}

				cw, err := lm.content.Writer(ctx,
					content.WithRef(fmt.Sprintf("ingest-%s", chainID)),
					content.WithDescriptor(desc))
				if err != nil {
					return fmt.Errorf("failed to get content writer: %w", err)
				}

				dgstr := digest.Canonical.Digester()
				cs, _ := compression.CompressStream(io.MultiWriter(cw, dgstr.Hash()), layerCompression)
				_, err = io.Copy(cs, ts)
				if err != nil {
					return fmt.Errorf("failed to copy to compressed stream: %w", err)
				}
				cs.Close()

				status, err := cw.Status()
				if err != nil {
					return err
				}

				desc.Size = status.Offset
				desc.Digest = dgstr.Digest()

				if err := cw.Commit(ctx, desc.Size, desc.Digest); err != nil && !cerrdefs.IsAlreadyExists(err) {
					return err
				}

				log.G(ctx).WithFields(log.Fields{
					"t":      time.Since(t1),
					"size":   desc.Size,
					"digest": desc.Digest,
				}).Debug("Converted layer to content tar")

				ml.Lock()
				manifest.Layers[layerIndex] = desc
				ml.Unlock()
				return nil
			})

			metadata, err := l.Metadata()
			if err != nil {
				return err
			}
			src, ok := metadata["UpperDir"]
			if !ok {
				src, ok = metadata["SourceDir"]
				if !ok {
					log.G(ctx).WithField("metadata", metadata).WithField("driver", lm.layers.DriverName()).Debug("no source directory metadata")
					return fmt.Errorf("graphdriver not supported: %w", cerrdefs.ErrNotImplemented)
				}
			}
			log.G(ctx).WithField("metadata", metadata).Debugf("migrating %s from %s", chainID, src)

			active := fmt.Sprintf("migration-%s", chainID)

			key := chainID.String()

			snapshotLabels := map[string]string{
				"containerd.io/snapshot.ref": key,
			}
			mounts, err := sn.Prepare(ctx, active, parent, snapshots.WithLabels(snapshotLabels))
			parent = key
			if err != nil {
				if cerrdefs.IsAlreadyExists(err) {
					continue
				}
				return err
			}

			dst, err := extractSource(mounts)
			if err != nil {
				return err
			}

			t1 := time.Now()
			if err := fs.CopyDir(dst, src); err != nil {
				return err
			}
			log.G(ctx).WithFields(log.Fields{
				"t":   time.Since(t1),
				"key": key,
			}).Debug("Copied layer to snapshot")

			if err := sn.Commit(ctx, key, active); err != nil && !cerrdefs.IsAlreadyExists(err) {
				return err
			}
		}

		configBytes := img.RawJSON()
		digest.FromBytes(configBytes)
		manifest.Config = ocispec.Descriptor{
			MediaType: ocispec.MediaTypeImageConfig,
			Digest:    digest.FromBytes(configBytes),
			Size:      int64(len(configBytes)),
		}

		configLabels := map[string]string{
			"containerd.io/gc.ref.snapshot." + snKey: parent,
		}
		if err = content.WriteBlob(ctx, lm.content, "config"+manifest.Config.Digest.String(), bytes.NewReader(configBytes), manifest.Config, content.WithLabels(configLabels)); err != nil && !cerrdefs.IsAlreadyExists(err) {
			return err
		}

		if err := eg.Wait(); err != nil {
			return err
		}

		manifestBytes, err := json.MarshalIndent(manifest, "", "   ")
		if err != nil {
			return err
		}

		manifestDesc := ocispec.Descriptor{
			MediaType: manifest.MediaType,
			Digest:    digest.FromBytes(manifestBytes),
			Size:      int64(len(manifestBytes)),
		}

		manifestLabels := map[string]string{
			"containerd.io/gc.ref.content.config": manifest.Config.Digest.String(),
		}
		for i := range manifest.Layers {
			manifestLabels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = manifest.Layers[i].Digest.String()
		}

		if err = content.WriteBlob(ctx, lm.content, "manifest"+manifestDesc.Digest.String(), bytes.NewReader(manifestBytes), manifestDesc, content.WithLabels(manifestLabels)); err != nil && !cerrdefs.IsAlreadyExists(err) {
			return err
		}

		childrenHandler := images.ChildrenHandler(lm.content)
		childrenHandler = images.SetChildrenMappedLabels(lm.content, childrenHandler, nil)
		if err = images.Walk(ctx, childrenHandler, manifestDesc); err != nil {
			return err
		}

		var added bool
		for _, named := range lm.refs.References(digest.Digest(imgID)) {
			img := images.Image{
				Name:   named.String(),
				Target: manifestDesc,
				// TODO: Any labels?
			}
			img, err = lm.cis.Create(ctx, img)
			if err != nil && !cerrdefs.IsAlreadyExists(err) {
				return err
			} else if err != nil {
				log.G(ctx).Infof("Tag already exists: %s", named)
				continue
			}

			log.G(ctx).Infof("Migrated image %s to %s", img.Name, img.Target.Digest)
			added = true
		}

		if !added {
			img := images.Image{
				Name:   "moby-dangling@" + manifestDesc.Digest.String(),
				Target: manifestDesc,
				// TODO: Any labels?
			}
			img, err = lm.cis.Create(ctx, img)
			if err != nil && !cerrdefs.IsAlreadyExists(err) {
				return err
			} else if err == nil {
				log.G(ctx).Infof("Migrated image %s to %s", img.Name, img.Target.Digest)
			}
		}
	}

	return nil
}

func extractSource(mounts []mount.Mount) (string, error) {
	if len(mounts) != 1 {
		return "", fmt.Errorf("cannot support snapshotters with multiple mount sources: %w", cerrdefs.ErrNotImplemented)
	}
	switch mounts[0].Type {
	case "bind":
		return mounts[0].Source, nil
	case "overlay":
		for _, option := range mounts[0].Options {
			if strings.HasPrefix(option, "upperdir=") {
				return option[9:], nil
			}
		}
	default:
		return "", fmt.Errorf("mount type %q not supported: %w", mounts[0].Type, cerrdefs.ErrNotImplemented)
	}

	return "", fmt.Errorf("mount is missing upper option: %w", cerrdefs.ErrNotImplemented)
}
