package limited

import (
	"context"
	"io"
	"runtime"
	"strings"
	"sync"

	"github.com/containerd/containerd/v2/core/content"
	"github.com/containerd/containerd/v2/core/images"
	"github.com/containerd/containerd/v2/core/remotes"
	"github.com/distribution/reference"
	"github.com/moby/buildkit/util/bklog"
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
	"golang.org/x/sync/semaphore"
)

type contextKeyT string

var contextKey = contextKeyT("buildkit/util/resolver/limited")

// DefaultMaxConcurrency is the default number of concurrent connections per registry.
var DefaultMaxConcurrency int64 = 4

var Default = New(int(DefaultMaxConcurrency))

type Group struct {
	mu   sync.Mutex
	size int
	sem  map[string][2]*semaphore.Weighted
}

type req struct {
	g   *Group
	ref string
}

func (r *req) acquire(ctx context.Context, desc ocispecs.Descriptor) (context.Context, func(), error) {
	if v := ctx.Value(contextKey); v != nil {
		return ctx, func() {}, nil
	}

	ctx = context.WithValue(ctx, contextKey, struct{}{})

	// json request get one additional connection
	highPriority := strings.HasSuffix(desc.MediaType, "+json")

	s := r.g.getOrInit(r.ref)
	if !highPriority {
		if err := s[0].Acquire(ctx, 1); err != nil {
			return ctx, nil, err
		}
	}
	if err := s[1].Acquire(ctx, 1); err != nil {
		if !highPriority {
			s[0].Release(1)
		}
		return ctx, nil, err
	}
	return ctx, func() {
		s[1].Release(1)
		if !highPriority {
			s[0].Release(1)
		}
	}, nil
}

func New(size int) *Group {
	return &Group{
		size: size,
		sem:  make(map[string][2]*semaphore.Weighted),
	}
}

func (g *Group) req(ref string) *req {
	return &req{g: g, ref: domain(ref)}
}

func (g *Group) getOrInit(domain string) [2]*semaphore.Weighted {
	g.mu.Lock()
	defer g.mu.Unlock()

	s, ok := g.sem[domain]
	if !ok {
		s = [2]*semaphore.Weighted{
			semaphore.NewWeighted(int64(g.size)),
			semaphore.NewWeighted(int64(g.size + 1)),
		}
		g.sem[domain] = s
	}
	return s
}

// SetMaxConcurrency sets the default maximum concurrency for the default group.
func SetMaxConcurrency(size int) {
	Default = New(size)
}

func (g *Group) WrapFetcher(f remotes.Fetcher, ref string) remotes.Fetcher {
	return &fetcher{Fetcher: f, req: g.req(ref)}
}

func (g *Group) PushHandler(pusher remotes.Pusher, provider content.Provider, ref string) images.HandlerFunc {
	ph := remotes.PushHandler(pusher, provider)
	req := g.req(ref)
	return func(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
		ctx, release, err := req.acquire(ctx, desc)
		if err != nil {
			return nil, err
		}
		defer release()
		return ph(ctx, desc)
	}
}

type fetcher struct {
	remotes.Fetcher
	req *req
}

func (f *fetcher) Fetch(ctx context.Context, desc ocispecs.Descriptor) (io.ReadCloser, error) {
	ctx, release, err := f.req.acquire(ctx, desc)
	if err != nil {
		return nil, err
	}
	rc, err := f.Fetcher.Fetch(ctx, desc)
	if err != nil {
		release()
		return nil, err
	}

	rcw := &readCloser{ReadCloser: rc}
	closer := func() {
		if !rcw.closed {
			bklog.G(ctx).Warnf("fetcher not closed cleanly: %s", desc.Digest)
		}
		release()
	}
	rcw.release = closer
	runtime.SetFinalizer(rcw, func(rc *readCloser) {
		rc.close()
	})

	if s, ok := rc.(io.Seeker); ok {
		return &readCloserSeeker{rcw, s}, nil
	}

	return rcw, nil
}

type readCloserSeeker struct {
	*readCloser
	io.Seeker
}

type readCloser struct {
	io.ReadCloser
	once    sync.Once
	closed  bool
	release func()
}

func (r *readCloser) Close() error {
	r.closed = true
	r.close()
	return r.ReadCloser.Close()
}

func (r *readCloser) close() {
	r.once.Do(r.release)
}

func FetchHandler(ingester content.Ingester, fetcher remotes.Fetcher, ref string) images.HandlerFunc {
	return remotes.FetchHandler(ingester, Default.WrapFetcher(fetcher, ref))
}

func PushHandler(pusher remotes.Pusher, provider content.Provider, ref string) images.HandlerFunc {
	return Default.PushHandler(pusher, provider, ref)
}

func domain(ref string) string {
	if ref != "" {
		if named, err := reference.ParseNormalizedNamed(ref); err == nil {
			return reference.Domain(named)
		}
	}
	return ref
}
