package containerd

import (
	"context"
	"io"

	snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1"
	"github.com/containerd/containerd/api/types"
	"github.com/containerd/containerd/errdefs"
	"github.com/containerd/containerd/mount"
	"github.com/containerd/containerd/snapshots"
	protobuftypes "github.com/gogo/protobuf/types"
)

// NewSnapshotterFromClient returns a new Snapshotter which communicates
// over a GRPC connection.
func NewSnapshotterFromClient(client snapshotsapi.SnapshotsClient, snapshotterName string) snapshots.Snapshotter {
	return &remoteSnapshotter{
		client:          client,
		snapshotterName: snapshotterName,
	}
}

type remoteSnapshotter struct {
	client          snapshotsapi.SnapshotsClient
	snapshotterName string
}

func (r *remoteSnapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) {
	resp, err := r.client.Stat(ctx,
		&snapshotsapi.StatSnapshotRequest{
			Snapshotter: r.snapshotterName,
			Key:         key,
		})
	if err != nil {
		return snapshots.Info{}, errdefs.FromGRPC(err)
	}
	return toInfo(resp.Info), nil
}

func (r *remoteSnapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
	resp, err := r.client.Update(ctx,
		&snapshotsapi.UpdateSnapshotRequest{
			Snapshotter: r.snapshotterName,
			Info:        fromInfo(info),
			UpdateMask: &protobuftypes.FieldMask{
				Paths: fieldpaths,
			},
		})
	if err != nil {
		return snapshots.Info{}, errdefs.FromGRPC(err)
	}
	return toInfo(resp.Info), nil
}

func (r *remoteSnapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
	resp, err := r.client.Usage(ctx, &snapshotsapi.UsageRequest{
		Snapshotter: r.snapshotterName,
		Key:         key,
	})
	if err != nil {
		return snapshots.Usage{}, errdefs.FromGRPC(err)
	}
	return toUsage(resp), nil
}

func (r *remoteSnapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) {
	resp, err := r.client.Mounts(ctx, &snapshotsapi.MountsRequest{
		Snapshotter: r.snapshotterName,
		Key:         key,
	})
	if err != nil {
		return nil, errdefs.FromGRPC(err)
	}
	return toMounts(resp.Mounts), nil
}

func (r *remoteSnapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
	var local snapshots.Info
	for _, opt := range opts {
		if err := opt(&local); err != nil {
			return nil, err
		}
	}
	resp, err := r.client.Prepare(ctx, &snapshotsapi.PrepareSnapshotRequest{
		Snapshotter: r.snapshotterName,
		Key:         key,
		Parent:      parent,
		Labels:      local.Labels,
	})
	if err != nil {
		return nil, errdefs.FromGRPC(err)
	}
	return toMounts(resp.Mounts), nil
}

func (r *remoteSnapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
	var local snapshots.Info
	for _, opt := range opts {
		if err := opt(&local); err != nil {
			return nil, err
		}
	}
	resp, err := r.client.View(ctx, &snapshotsapi.ViewSnapshotRequest{
		Snapshotter: r.snapshotterName,
		Key:         key,
		Parent:      parent,
		Labels:      local.Labels,
	})
	if err != nil {
		return nil, errdefs.FromGRPC(err)
	}
	return toMounts(resp.Mounts), nil
}

func (r *remoteSnapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
	var local snapshots.Info
	for _, opt := range opts {
		if err := opt(&local); err != nil {
			return err
		}
	}
	_, err := r.client.Commit(ctx, &snapshotsapi.CommitSnapshotRequest{
		Snapshotter: r.snapshotterName,
		Name:        name,
		Key:         key,
		Labels:      local.Labels,
	})
	return errdefs.FromGRPC(err)
}

func (r *remoteSnapshotter) Remove(ctx context.Context, key string) error {
	_, err := r.client.Remove(ctx, &snapshotsapi.RemoveSnapshotRequest{
		Snapshotter: r.snapshotterName,
		Key:         key,
	})
	return errdefs.FromGRPC(err)
}

func (r *remoteSnapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error {
	sc, err := r.client.List(ctx, &snapshotsapi.ListSnapshotsRequest{
		Snapshotter: r.snapshotterName,
	})
	if err != nil {
		return errdefs.FromGRPC(err)
	}
	for {
		resp, err := sc.Recv()
		if err != nil {
			if err == io.EOF {
				return nil
			}
			return errdefs.FromGRPC(err)
		}
		if resp == nil {
			return nil
		}
		for _, info := range resp.Info {
			if err := fn(ctx, toInfo(info)); err != nil {
				return err
			}
		}
	}
}

func (r *remoteSnapshotter) Close() error {
	return nil
}

func toKind(kind snapshotsapi.Kind) snapshots.Kind {
	if kind == snapshotsapi.KindActive {
		return snapshots.KindActive
	}
	if kind == snapshotsapi.KindView {
		return snapshots.KindView
	}
	return snapshots.KindCommitted
}

func toInfo(info snapshotsapi.Info) snapshots.Info {
	return snapshots.Info{
		Name:    info.Name,
		Parent:  info.Parent,
		Kind:    toKind(info.Kind),
		Created: info.CreatedAt,
		Updated: info.UpdatedAt,
		Labels:  info.Labels,
	}
}

func toUsage(resp *snapshotsapi.UsageResponse) snapshots.Usage {
	return snapshots.Usage{
		Inodes: resp.Inodes,
		Size:   resp.Size_,
	}
}

func toMounts(mm []*types.Mount) []mount.Mount {
	mounts := make([]mount.Mount, len(mm))
	for i, m := range mm {
		mounts[i] = mount.Mount{
			Type:    m.Type,
			Source:  m.Source,
			Options: m.Options,
		}
	}
	return mounts
}

func fromKind(kind snapshots.Kind) snapshotsapi.Kind {
	if kind == snapshots.KindActive {
		return snapshotsapi.KindActive
	}
	if kind == snapshots.KindView {
		return snapshotsapi.KindView
	}
	return snapshotsapi.KindCommitted
}

func fromInfo(info snapshots.Info) snapshotsapi.Info {
	return snapshotsapi.Info{
		Name:      info.Name,
		Parent:    info.Parent,
		Kind:      fromKind(info.Kind),
		CreatedAt: info.Created,
		UpdatedAt: info.Updated,
		Labels:    info.Labels,
	}
}
