package regclient

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/regclient/regclient/scheme"
	"github.com/regclient/regclient/types/descriptor"
	"github.com/regclient/regclient/types/errs"
	"github.com/regclient/regclient/types/manifest"
	"github.com/regclient/regclient/types/platform"
	"github.com/regclient/regclient/types/ref"
	"github.com/regclient/regclient/types/warning"
)

type manifestOpt struct {
	d             descriptor.Descriptor
	platform      *platform.Platform
	schemeOpts    []scheme.ManifestOpts
	requireDigest bool
}

// ManifestOpts define options for the Manifest* commands.
type ManifestOpts func(*manifestOpt)

// WithManifest passes a manifest to ManifestDelete.
func WithManifest(m manifest.Manifest) ManifestOpts {
	return func(opts *manifestOpt) {
		opts.schemeOpts = append(opts.schemeOpts, scheme.WithManifest(m))
	}
}

// WithManifestCheckReferrers checks for referrers field on ManifestDelete.
// This will update the client managed referrer listing.
func WithManifestCheckReferrers() ManifestOpts {
	return func(opts *manifestOpt) {
		opts.schemeOpts = append(opts.schemeOpts, scheme.WithManifestCheckReferrers())
	}
}

// WithManifestChild for ManifestPut indicates the manifest is not the top level manifest being copied.
// This is used by the ocidir scheme to determine what entries to include in the index.json.
func WithManifestChild() ManifestOpts {
	return func(opts *manifestOpt) {
		opts.schemeOpts = append(opts.schemeOpts, scheme.WithManifestChild())
	}
}

// WithManifestDesc includes the descriptor for ManifestGet.
// This is used to automatically extract a Data field if available.
func WithManifestDesc(d descriptor.Descriptor) ManifestOpts {
	return func(opts *manifestOpt) {
		opts.d = d
	}
}

// WithManifestPlatform resolves the platform specific manifest on Get and Head requests.
// This causes an additional GET query to a registry when an Index or Manifest List is encountered.
// This option is ignored if the retrieved manifest is not an Index or Manifest List.
func WithManifestPlatform(p platform.Platform) ManifestOpts {
	return func(opts *manifestOpt) {
		opts.platform = &p
	}
}

// WithManifestRequireDigest falls back from a HEAD to a GET request when digest headers aren't received.
func WithManifestRequireDigest() ManifestOpts {
	return func(opts *manifestOpt) {
		opts.requireDigest = true
	}
}

// ManifestDelete removes a manifest, including all tags pointing to that registry.
// The reference must include the digest to delete (see TagDelete for deleting a tag).
// All tags pointing to the manifest will be deleted.
func (rc *RegClient) ManifestDelete(ctx context.Context, r ref.Ref, opts ...ManifestOpts) error {
	if !r.IsSet() {
		return fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
	}
	opt := manifestOpt{schemeOpts: []scheme.ManifestOpts{}}
	for _, fn := range opts {
		fn(&opt)
	}
	schemeAPI, err := rc.schemeGet(r.Scheme)
	if err != nil {
		return err
	}
	return schemeAPI.ManifestDelete(ctx, r, opt.schemeOpts...)
}

// ManifestGet retrieves a manifest.
func (rc *RegClient) ManifestGet(ctx context.Context, r ref.Ref, opts ...ManifestOpts) (manifest.Manifest, error) {
	if !r.IsSet() {
		return nil, fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
	}
	opt := manifestOpt{schemeOpts: []scheme.ManifestOpts{}}
	for _, fn := range opts {
		fn(&opt)
	}
	if opt.d.Digest != "" {
		r.Digest = opt.d.Digest.String()
		data, err := opt.d.GetData()
		if err == nil {
			return manifest.New(
				manifest.WithDesc(opt.d),
				manifest.WithRaw(data),
				manifest.WithRef(r),
			)
		}
	}
	// dedup warnings
	if w := warning.FromContext(ctx); w == nil {
		ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
	}
	schemeAPI, err := rc.schemeGet(r.Scheme)
	if err != nil {
		return nil, err
	}
	m, err := schemeAPI.ManifestGet(ctx, r)
	if err != nil {
		return m, err
	}
	if opt.platform != nil && !m.IsList() {
		rc.slog.Debug("ignoring platform option, image is not an index",
			slog.String("platform", opt.platform.String()),
			slog.String("ref", r.CommonName()))
	}
	// this will loop to handle a nested index
	for opt.platform != nil && m.IsList() {
		d, err := manifest.GetPlatformDesc(m, opt.platform)
		if err != nil {
			return m, err
		}
		r = r.SetDigest(d.Digest.String())
		m, err = schemeAPI.ManifestGet(ctx, r)
		if err != nil {
			return m, err
		}
	}
	return m, err
}

// ManifestHead queries for the existence of a manifest and returns metadata (digest, media-type, size).
func (rc *RegClient) ManifestHead(ctx context.Context, r ref.Ref, opts ...ManifestOpts) (manifest.Manifest, error) {
	if !r.IsSet() {
		return nil, fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
	}
	opt := manifestOpt{schemeOpts: []scheme.ManifestOpts{}}
	for _, fn := range opts {
		fn(&opt)
	}
	// dedup warnings
	if w := warning.FromContext(ctx); w == nil {
		ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
	}
	schemeAPI, err := rc.schemeGet(r.Scheme)
	if err != nil {
		return nil, err
	}
	m, err := schemeAPI.ManifestHead(ctx, r)
	if err != nil {
		return m, err
	}
	if opt.platform != nil && !m.IsList() {
		rc.slog.Debug("ignoring platform option, image is not an index",
			slog.String("platform", opt.platform.String()),
			slog.String("ref", r.CommonName()))
	}
	// this will loop to handle a nested index
	for opt.platform != nil && m.IsList() {
		if !m.IsSet() {
			m, err = schemeAPI.ManifestGet(ctx, r)
		}
		d, err := manifest.GetPlatformDesc(m, opt.platform)
		if err != nil {
			return m, err
		}
		r = r.SetDigest(d.Digest.String())
		m, err = schemeAPI.ManifestHead(ctx, r)
		if err != nil {
			return m, err
		}
	}
	if opt.requireDigest && m.GetDescriptor().Digest.String() == "" {
		m, err = schemeAPI.ManifestGet(ctx, r)
	}
	return m, err
}

// ManifestPut pushes a manifest.
// Any descriptors referenced by the manifest typically need to be pushed first.
func (rc *RegClient) ManifestPut(ctx context.Context, r ref.Ref, m manifest.Manifest, opts ...ManifestOpts) error {
	if !r.IsSetRepo() {
		return fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
	}
	opt := manifestOpt{schemeOpts: []scheme.ManifestOpts{}}
	for _, fn := range opts {
		fn(&opt)
	}
	schemeAPI, err := rc.schemeGet(r.Scheme)
	if err != nil {
		return err
	}
	return schemeAPI.ManifestPut(ctx, r, m, opt.schemeOpts...)
}
