// Package licensematcher implements a client for matching licenses using the deps.dev API.
package licensematcher

import (
	"context"

	depsdevpb "deps.dev/api/v3"
	"github.com/google/osv-scalibr/clients/datasource"
	"github.com/google/osv-scalibr/extractor"
	"github.com/google/osv-scanner/v2/internal/depsdev"
	"github.com/google/osv-scanner/v2/internal/imodels"
	"github.com/google/osv-scanner/v2/pkg/models"
	"golang.org/x/sync/errgroup"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const (
	maxConcurrentRequests = 1000
)

// DepsDevLicenseMatcher implements the LicenseMatcher interface with a deps.dev client.
// It sends out requests for every package version and does not perform caching.
type DepsDevLicenseMatcher struct {
	Client *datasource.CachedInsightsClient
}

func (matcher *DepsDevLicenseMatcher) MatchLicenses(ctx context.Context, packages []*extractor.Package) error {
	queries := make([]*depsdevpb.GetVersionRequest, len(packages))

	for i, pkg := range packages {
		system, ok := depsdev.System[imodels.Ecosystem(pkg).Ecosystem]
		if !ok || imodels.Name(pkg) == "" || imodels.Version(pkg) == "" {
			continue
		}
		queries[i] = versionQuery(system, imodels.Name(pkg), imodels.Version(pkg))
	}

	pkgLicenses, err := matcher.makeVersionRequest(ctx, queries)
	if err != nil {
		return err
	}

	for i, licenses := range pkgLicenses {
		packages[i].Licenses = make([]string, len(licenses))

		for j, license := range licenses {
			packages[i].Licenses[j] = string(license)
		}
	}

	return nil
}

// makeVersionRequest calls the deps.dev GetVersion gRPC API endpoint for each
// query. It makes these requests concurrently, sharing the single HTTP/2
// connection. The order in which the requests are specified should correspond
// to the order of licenses returned by this function.
func (matcher *DepsDevLicenseMatcher) makeVersionRequest(ctx context.Context, queries []*depsdevpb.GetVersionRequest) ([][]models.License, error) {
	licenses := make([][]models.License, len(queries))
	g, ctx := errgroup.WithContext(ctx)
	g.SetLimit(maxConcurrentRequests)

	for i := range queries {
		if queries[i] == nil {
			// This may be a private package.
			licenses[i] = []models.License{models.License("UNKNOWN")}
			continue
		}
		g.Go(func() error {
			resp, err := matcher.Client.GetVersion(ctx, queries[i])
			if err != nil {
				if status.Code(err) == codes.NotFound {
					licenses[i] = append(licenses[i], "UNKNOWN")
					return nil
				}

				return err
			}
			ls := make([]models.License, len(resp.GetLicenses()))
			for j, license := range resp.GetLicenses() {
				ls[j] = models.License(license)
			}
			if len(ls) == 0 {
				// The deps.dev API will return an
				// empty slice if the license is
				// unknown.
				ls = []models.License{models.License("UNKNOWN")}
			}
			licenses[i] = ls

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

	return licenses, nil
}

func versionQuery(system depsdevpb.System, name string, version string) *depsdevpb.GetVersionRequest {
	if system == depsdevpb.System_GO {
		// deps.dev uses native go versioning, which includes prepending v for package versions
		// and go for stdlib
		if name == "stdlib" {
			version = "go" + version
		} else {
			version = "v" + version
		}
	}

	return &depsdevpb.GetVersionRequest{
		VersionKey: &depsdevpb.VersionKey{
			System:  system,
			Name:    name,
			Version: version,
		},
	}
}
