package registry

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	artifactregistry "cloud.google.com/go/artifactregistry/apiv1beta2"
	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/ecr"
	"github.com/porter-dev/porter/internal/models"
	"github.com/porter-dev/porter/internal/oauth"
	"github.com/porter-dev/porter/internal/repository"
	"golang.org/x/oauth2"
	v1artifactregistry "google.golang.org/api/artifactregistry/v1"
	"google.golang.org/api/iterator"
	"google.golang.org/api/option"
	artifactregistrypb "google.golang.org/genproto/googleapis/devtools/artifactregistry/v1beta2"

	ints "github.com/porter-dev/porter/internal/models/integrations"

	ptypes "github.com/porter-dev/porter/api/types"

	"github.com/digitalocean/godo"
	"github.com/docker/cli/cli/config/configfile"
	"github.com/docker/cli/cli/config/types"
	"github.com/docker/distribution/reference"

	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
)

// Registry wraps the gorm Registry model
type Registry models.Registry

func GetECRRegistryURL(awsIntRepo repository.AWSIntegrationRepository, projectID, awsIntID uint) (string, error) {
	awsInt, err := awsIntRepo.ReadAWSIntegration(projectID, awsIntID)

	if err != nil {
		return "", err
	}

	sess, err := awsInt.GetSession()

	if err != nil {
		return "", err
	}

	ecrSvc := ecr.New(sess)

	output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})

	if err != nil {
		return "", err
	}

	return *output.AuthorizationData[0].ProxyEndpoint, nil
}

// ListRepositories lists the repositories for a registry
func (r *Registry) ListRepositories(
	repo repository.Repository,
	doAuth *oauth2.Config, // only required if using DOCR
) ([]*ptypes.RegistryRepository, error) {
	// switch on the auth mechanism to get a token
	if r.AWSIntegrationID != 0 {
		return r.listECRRepositories(repo)
	}

	if r.GCPIntegrationID != 0 {
		if strings.Contains(r.URL, "pkg.dev") {
			return r.listGARRepositories(repo)
		}

		return r.listGCRRepositories(repo)
	}

	if r.DOIntegrationID != 0 {
		return r.listDOCRRepositories(repo, doAuth)
	}

	if r.AzureIntegrationID != 0 {
		return r.listACRRepositories(repo)
	}

	if r.BasicIntegrationID != 0 {
		return r.listPrivateRegistryRepositories(repo)
	}

	return nil, fmt.Errorf("error listing repositories")
}

type gcrJWT struct {
	AccessToken  string `json:"token"`
	ExpiresInSec int    `json:"expires_in"`
}

type gcrErr struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

type gcrRepositoryResp struct {
	Repositories []string `json:"repositories"`
	Errors       []gcrErr `json:"errors"`
}

func (r *Registry) GetGCRToken(repo repository.Repository) (*oauth2.Token, error) {
	getTokenCache := r.getTokenCacheFunc(repo)

	gcp, err := repo.GCPIntegration().ReadGCPIntegration(
		r.ProjectID,
		r.GCPIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	// get oauth2 access token
	return gcp.GetBearerToken(
		getTokenCache,
		r.setTokenCacheFunc(repo),
		"https://www.googleapis.com/auth/devstorage.read_write",
	)
}

func (r *Registry) listGCRRepositories(
	repo repository.Repository,
) ([]*ptypes.RegistryRepository, error) {
	gcp, err := repo.GCPIntegration().ReadGCPIntegration(
		r.ProjectID,
		r.GCPIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	// Just use service account key to authenticate, since scopes may not be in place
	// for oauth. This also prevents us from making more requests.
	client := &http.Client{}

	regURL := r.URL

	if !strings.HasPrefix(regURL, "http") {
		regURL = fmt.Sprintf("https://%s", regURL)
	}

	regURLParsed, err := url.Parse(regURL)
	regHostname := "gcr.io"

	if err == nil {
		regHostname = regURLParsed.Host
	}

	req, err := http.NewRequest(
		"GET",
		fmt.Sprintf("https://%s/v2/_catalog", regHostname),
		nil,
	)

	if err != nil {
		return nil, err
	}

	req.SetBasicAuth("_json_key", string(gcp.GCPKeyData))

	resp, err := client.Do(req)

	if err != nil {
		return nil, err
	}

	gcrResp := gcrRepositoryResp{}

	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
		return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
	}

	if len(gcrResp.Errors) > 0 {
		errMsg := ""
		for _, gcrErr := range gcrResp.Errors {
			errMsg += fmt.Sprintf(": Code %s, message %s", gcrErr.Code, gcrErr.Message)
		}

		return nil, fmt.Errorf(errMsg)
	}

	res := make([]*ptypes.RegistryRepository, 0)

	parsedURL, err := url.Parse("https://" + r.URL)

	if err != nil {
		return nil, err
	}

	for _, repo := range gcrResp.Repositories {
		res = append(res, &ptypes.RegistryRepository{
			Name: repo,
			URI:  parsedURL.Host + "/" + repo,
		})
	}

	return res, nil
}

func (r *Registry) GetGARToken(repo repository.Repository) (*oauth2.Token, error) {
	getTokenCache := r.getTokenCacheFunc(repo)

	gcp, err := repo.GCPIntegration().ReadGCPIntegration(
		r.ProjectID,
		r.GCPIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	// get oauth2 access token
	return gcp.GetBearerToken(
		getTokenCache,
		r.setTokenCacheFunc(repo),
		"https://www.googleapis.com/auth/cloud-platform",
	)
}

type garTokenSource struct {
	reg  *Registry
	repo repository.Repository
}

func (source *garTokenSource) Token() (*oauth2.Token, error) {
	return source.reg.GetGARToken(source.repo)
}

// GAR has the concept of a "repository" which is a collection of images, unlike ECR or others
// where a repository is a single image. This function returns the list of fully qualified names
// of GAR images including their repository names.
func (r *Registry) listGARRepositories(
	repo repository.Repository,
) ([]*ptypes.RegistryRepository, error) {
	gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
		r.ProjectID,
		r.GCPIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	client, err := artifactregistry.NewClient(context.Background(), option.WithTokenSource(&garTokenSource{
		reg:  r,
		repo: repo,
	}), option.WithScopes("roles/artifactregistry.reader"))

	if err != nil {
		return nil, err
	}

	var repoNames []string
	nextToken := ""

	parsedURL, err := url.Parse("https://" + r.URL)

	if err != nil {
		return nil, err
	}

	location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")

	for {
		it := client.ListRepositories(context.Background(), &artifactregistrypb.ListRepositoriesRequest{
			Parent:    fmt.Sprintf("projects/%s/locations/%s", gcpInt.GCPProjectID, location),
			PageSize:  1000,
			PageToken: nextToken,
		})

		for {
			resp, err := it.Next()

			if err == iterator.Done {
				break
			} else if err != nil {
				return nil, err
			}

			if resp.GetFormat() == artifactregistrypb.Repository_DOCKER { // we only care about
				repoSlice := strings.Split(resp.GetName(), "/")
				repoName := repoSlice[len(repoSlice)-1]

				repoNames = append(repoNames, repoName)
			}
		}

		if it.PageInfo().Token == "" {
			break
		}

		nextToken = it.PageInfo().Token
	}

	svc, err := v1artifactregistry.NewService(context.Background(), option.WithTokenSource(&garTokenSource{
		reg:  r,
		repo: repo,
	}), option.WithScopes("roles/artifactregistry.reader"))

	if err != nil {
		return nil, err
	}

	nextToken = ""

	dockerSvc := v1artifactregistry.NewProjectsLocationsRepositoriesDockerImagesService(svc)

	var (
		wg     sync.WaitGroup
		resMap sync.Map
	)

	for _, repoName := range repoNames {
		wg.Add(1)

		go func(repoName string) {
			defer wg.Done()

			for {
				resp, err := dockerSvc.List(fmt.Sprintf("projects/%s/locations/%s/repositories/%s",
					gcpInt.GCPProjectID, location, repoName)).PageSize(1000).PageToken(nextToken).Do()

				if err != nil {
					// FIXME: we should report this error using a channel
					return
				}

				for _, image := range resp.DockerImages {
					named, err := reference.ParseNamed(image.Uri)

					if err != nil {
						// let us skip this image becaue it has a malformed URI coming from the GCP API
						continue
					}

					uploadTime, _ := time.Parse(time.RFC3339, image.UploadTime)

					resMap.Store(named.Name(), &ptypes.RegistryRepository{
						Name:      repoName,
						URI:       named.Name(),
						CreatedAt: uploadTime,
					})
				}

				if resp.NextPageToken == "" {
					break
				}

				nextToken = resp.NextPageToken
			}
		}(repoName)
	}

	wg.Wait()

	var res []*ptypes.RegistryRepository

	resMap.Range(func(_, value any) bool {
		res = append(res, value.(*ptypes.RegistryRepository))
		return true
	})

	return res, nil
}

func (r *Registry) listECRRepositories(repo repository.Repository) ([]*ptypes.RegistryRepository, error) {
	aws, err := repo.AWSIntegration().ReadAWSIntegration(
		r.ProjectID,
		r.AWSIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	sess, err := aws.GetSession()

	if err != nil {
		return nil, err
	}

	svc := ecr.New(sess)

	resp, err := svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{})

	if err != nil {
		return nil, err
	}

	res := make([]*ptypes.RegistryRepository, 0)

	for _, repo := range resp.Repositories {
		res = append(res, &ptypes.RegistryRepository{
			Name:      *repo.RepositoryName,
			CreatedAt: *repo.CreatedAt,
			URI:       *repo.RepositoryUri,
		})
	}

	return res, nil
}

func (r *Registry) listACRRepositories(repo repository.Repository) ([]*ptypes.RegistryRepository, error) {
	az, err := repo.AzureIntegration().ReadAzureIntegration(
		r.ProjectID,
		r.AzureIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	client := &http.Client{}

	req, err := http.NewRequest(
		"GET",
		fmt.Sprintf("%s/v2/_catalog", r.URL),
		nil,
	)

	if err != nil {
		return nil, err
	}

	req.SetBasicAuth(az.AzureClientID, string(az.ServicePrincipalSecret))

	resp, err := client.Do(req)

	if err != nil {
		return nil, err
	}

	gcrResp := gcrRepositoryResp{}

	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
		return nil, fmt.Errorf("Could not read Azure registry repositories: %v", err)
	}

	res := make([]*ptypes.RegistryRepository, 0)

	if err != nil {
		return nil, err
	}

	for _, repo := range gcrResp.Repositories {
		res = append(res, &ptypes.RegistryRepository{
			Name: repo,
			URI:  strings.TrimPrefix(r.URL, "https://") + "/" + repo,
		})
	}

	return res, nil
}

// Returns the username/password pair for the registry
func (r *Registry) GetACRCredentials(repo repository.Repository) (string, string, error) {
	az, err := repo.AzureIntegration().ReadAzureIntegration(
		r.ProjectID,
		r.AzureIntegrationID,
	)

	if err != nil {
		return "", "", err
	}

	// if the passwords and name aren't set, generate them
	if az.ACRTokenName == "" || len(az.ACRPassword1) == 0 {
		az.ACRTokenName = "porter-acr-token"

		// create an acr repo token
		cred, err := azidentity.NewClientSecretCredential(az.AzureTenantID, az.AzureClientID, string(az.ServicePrincipalSecret), nil)

		if err != nil {
			return "", "", err
		}

		scopeMapsClient, err := armcontainerregistry.NewScopeMapsClient(az.AzureSubscriptionID, cred, nil)

		if err != nil {
			return "", "", err
		}

		smRes, err := scopeMapsClient.Get(
			context.Background(),
			az.ACRResourceGroupName,
			az.ACRName,
			"_repositories_admin",
			nil,
		)

		if err != nil {
			return "", "", err
		}

		tokensClient, err := armcontainerregistry.NewTokensClient(az.AzureSubscriptionID, cred, nil)

		if err != nil {
			return "", "", err
		}

		pollerResp, err := tokensClient.BeginCreate(
			context.Background(),
			az.ACRResourceGroupName,
			az.ACRName,
			"porter-acr-token",
			armcontainerregistry.Token{
				Properties: &armcontainerregistry.TokenProperties{
					ScopeMapID: smRes.ID,
					Status:     to.Ptr(armcontainerregistry.TokenStatusEnabled),
				},
			},
			nil,
		)

		if err != nil {
			return "", "", err
		}

		tokResp, err := pollerResp.PollUntilDone(context.Background(), 2*time.Second)

		if err != nil {
			return "", "", err
		}

		registriesClient, err := armcontainerregistry.NewRegistriesClient(az.AzureSubscriptionID, cred, nil)

		if err != nil {
			return "", "", err
		}

		poller, err := registriesClient.BeginGenerateCredentials(
			context.Background(),
			az.ACRResourceGroupName,
			az.ACRName,
			armcontainerregistry.GenerateCredentialsParameters{
				TokenID: tokResp.ID,
			},
			&armcontainerregistry.RegistriesClientBeginGenerateCredentialsOptions{ResumeToken: ""})

		if err != nil {
			return "", "", err
		}

		genCredentialsResp, err := poller.PollUntilDone(context.Background(), 2*time.Second)

		if err != nil {
			return "", "", err
		}

		for i, tokPassword := range genCredentialsResp.Passwords {
			if i == 0 {
				az.ACRPassword1 = []byte(*tokPassword.Value)
			} else if i == 1 {
				az.ACRPassword2 = []byte(*tokPassword.Value)
			}
		}

		// update the az integration
		az, err = repo.AzureIntegration().OverwriteAzureIntegration(
			az,
		)

		if err != nil {
			return "", "", err
		}
	}

	return az.ACRTokenName, string(az.ACRPassword1), nil
}

func (r *Registry) listDOCRRepositories(
	repo repository.Repository,
	doAuth *oauth2.Config,
) ([]*ptypes.RegistryRepository, error) {
	oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(
		r.ProjectID,
		r.DOIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, repo))

	if err != nil {
		return nil, err
	}

	client := godo.NewFromToken(tok)

	urlArr := strings.Split(r.URL, "/")

	if len(urlArr) != 2 {
		return nil, fmt.Errorf("invalid digital ocean registry url")
	}

	name := urlArr[1]

	repos, _, err := client.Registry.ListRepositories(context.TODO(), name, &godo.ListOptions{})

	if err != nil {
		return nil, err
	}

	res := make([]*ptypes.RegistryRepository, 0)

	for _, repo := range repos {
		res = append(res, &ptypes.RegistryRepository{
			Name: repo.Name,
			URI:  r.URL + "/" + repo.Name,
		})
	}

	return res, nil
}

func (r *Registry) listPrivateRegistryRepositories(
	repo repository.Repository,
) ([]*ptypes.RegistryRepository, error) {
	// handle dockerhub different, as it doesn't implement the docker registry http api
	if strings.Contains(r.URL, "docker.io") {
		// in this case, we just return the single dockerhub repository that's linked
		res := make([]*ptypes.RegistryRepository, 0)

		res = append(res, &ptypes.RegistryRepository{
			Name: strings.Split(r.URL, "docker.io/")[1],
			URI:  r.URL,
		})

		return res, nil
	}

	basic, err := repo.BasicIntegration().ReadBasicIntegration(
		r.ProjectID,
		r.BasicIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	// Just use service account key to authenticate, since scopes may not be in place
	// for oauth. This also prevents us from making more requests.
	client := &http.Client{}

	// get the host and scheme to make the request
	parsedURL, err := url.Parse(r.URL)

	req, err := http.NewRequest(
		"GET",
		fmt.Sprintf("%s://%s/v2/_catalog", parsedURL.Scheme, parsedURL.Host),
		nil,
	)

	if err != nil {
		return nil, err
	}

	req.SetBasicAuth(string(basic.Username), string(basic.Password))

	resp, err := client.Do(req)

	if err != nil {
		return nil, err
	}

	// if the status code is 404, fallback to the Docker Hub implementation
	if resp.StatusCode == 404 {
		req, err := http.NewRequest(
			"GET",
			fmt.Sprintf("%s/", r.URL),
			nil,
		)

		if err != nil {
			return nil, err
		}

		req.SetBasicAuth(string(basic.Username), string(basic.Password))

		resp, err = client.Do(req)

		if err != nil {
			return nil, err
		}
	}

	gcrResp := gcrRepositoryResp{}

	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
		return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
	}

	res := make([]*ptypes.RegistryRepository, 0)

	if err != nil {
		return nil, err
	}

	for _, repo := range gcrResp.Repositories {
		res = append(res, &ptypes.RegistryRepository{
			Name: repo,
			URI:  parsedURL.Host + "/" + repo,
		})
	}

	return res, nil
}

func (r *Registry) getTokenCacheFunc(
	repo repository.Repository,
) ints.GetTokenCacheFunc {
	return func() (tok *ints.TokenCache, err error) {
		reg, err := repo.Registry().ReadRegistry(r.ProjectID, r.ID)

		if err != nil {
			return nil, err
		}

		return &reg.TokenCache.TokenCache, nil
	}
}

func (r *Registry) setTokenCacheFunc(
	repo repository.Repository,
) ints.SetTokenCacheFunc {
	return func(token string, expiry time.Time) error {
		_, err := repo.Registry().UpdateRegistryTokenCache(
			&ints.RegTokenCache{
				TokenCache: ints.TokenCache{
					Token:  []byte(token),
					Expiry: expiry,
				},
				RegistryID: r.ID,
			},
		)

		return err
	}
}

// CreateRepository creates a repository for a registry, if needed
// (currently only required for ECR)
func (r *Registry) CreateRepository(
	repo repository.Repository,
	name string,
) error {
	// if aws, create repository
	if r.AWSIntegrationID != 0 {
		return r.createECRRepository(repo, name)
	} else if r.GCPIntegrationID != 0 && strings.Contains(r.URL, "pkg.dev") {
		return r.createGARRepository(repo, name)
	}

	// otherwise, no-op
	return nil
}

func (r *Registry) createECRRepository(
	repo repository.Repository,
	name string,
) error {
	aws, err := repo.AWSIntegration().ReadAWSIntegration(
		r.ProjectID,
		r.AWSIntegrationID,
	)

	if err != nil {
		return err
	}

	sess, err := aws.GetSession()

	if err != nil {
		return err
	}

	svc := ecr.New(sess)

	// determine if repository already exists
	_, err = svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{
		RepositoryNames: []*string{&name},
	})

	// if the repository was not found, create it
	if aerr, ok := err.(awserr.Error); ok && aerr.Code() == ecr.ErrCodeRepositoryNotFoundException {
		_, err = svc.CreateRepository(&ecr.CreateRepositoryInput{
			RepositoryName: &name,
		})

		return err
	} else if err != nil {
		return err
	}

	return nil
}

func (r *Registry) createGARRepository(
	repo repository.Repository,
	name string,
) error {
	gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
		r.ProjectID,
		r.GCPIntegrationID,
	)

	if err != nil {
		return err
	}

	client, err := artifactregistry.NewClient(context.Background(), option.WithTokenSource(&garTokenSource{
		reg:  r,
		repo: repo,
	}), option.WithScopes("roles/artifactregistry.admin"))

	if err != nil {
		return err
	}

	defer client.Close()

	parsedURL, err := url.Parse("https://" + r.URL)

	if err != nil {
		return err
	}

	location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")

	_, err = client.GetRepository(context.Background(), &artifactregistrypb.GetRepositoryRequest{
		Name: fmt.Sprintf("projects/%s/locations/%s/repositories/%s", gcpInt.GCPProjectID, location, name),
	})

	if err != nil && strings.Contains(err.Error(), "not found") {
		// create a new repository
		_, err := client.CreateRepository(context.Background(), &artifactregistrypb.CreateRepositoryRequest{
			Parent:       fmt.Sprintf("projects/%s/locations/%s", gcpInt.GCPProjectID, location),
			RepositoryId: name,
			Repository: &artifactregistrypb.Repository{
				Format: artifactregistrypb.Repository_DOCKER,
			},
		})

		if err != nil {
			return err
		}
	} else if err != nil {
		return err
	}

	return nil
}

// ListImages lists the images for an image repository
func (r *Registry) ListImages(
	repoName string,
	repo repository.Repository,
	doAuth *oauth2.Config, // only required if using DOCR
) ([]*ptypes.Image, error) {
	// switch on the auth mechanism to get a token
	if r.AWSIntegrationID != 0 {
		return r.listECRImages(repoName, repo)
	}

	if r.AzureIntegrationID != 0 {
		return r.listACRImages(repoName, repo)
	}

	if r.GCPIntegrationID != 0 {
		if strings.Contains(r.URL, "pkg.dev") {
			return r.listGARImages(repoName, repo)
		}

		return r.listGCRImages(repoName, repo)
	}

	if r.DOIntegrationID != 0 {
		return r.listDOCRImages(repoName, repo, doAuth)
	}

	if r.BasicIntegrationID != 0 {
		return r.listPrivateRegistryImages(repoName, repo)
	}

	return nil, fmt.Errorf("error listing images")
}

func (r *Registry) GetECRPaginatedImages(
	repoName string,
	repo repository.Repository,
	maxResults int64,
	nextToken *string,
) ([]*ptypes.Image, *string, error) {
	aws, err := repo.AWSIntegration().ReadAWSIntegration(
		r.ProjectID,
		r.AWSIntegrationID,
	)

	if err != nil {
		return nil, nil, err
	}

	sess, err := aws.GetSession()

	if err != nil {
		return nil, nil, err
	}

	svc := ecr.New(sess)

	resp, err := svc.ListImages(&ecr.ListImagesInput{
		RepositoryName: &repoName,
		MaxResults:     &maxResults,
		NextToken:      nextToken,
	})

	if err != nil {
		return nil, nil, err
	}

	if len(resp.ImageIds) == 0 {
		return []*ptypes.Image{}, nil, nil
	}

	imageIDLen := len(resp.ImageIds)
	imageDetails := make([]*ecr.ImageDetail, 0)
	imageIDMap := make(map[string]bool)

	for _, id := range resp.ImageIds {
		if id != nil && id.ImageTag != nil {
			imageIDMap[*id.ImageTag] = true
		}
	}

	var wg sync.WaitGroup
	var mu sync.Mutex

	// AWS API expects the length of imageIDs to be at max 100 at a time
	for start := 0; start < imageIDLen; start += 100 {
		end := start + 100
		if end > imageIDLen {
			end = imageIDLen
		}

		wg.Add(1)

		go func(start, end int) {
			defer wg.Done()

			describeResp, err := svc.DescribeImages(&ecr.DescribeImagesInput{
				RepositoryName: &repoName,
				ImageIds:       resp.ImageIds[start:end],
			})

			if err != nil {
				return
			}

			mu.Lock()
			imageDetails = append(imageDetails, describeResp.ImageDetails...)
			mu.Unlock()
		}(start, end)
	}

	wg.Wait()

	res := make([]*ptypes.Image, 0)
	imageInfoMap := make(map[string]*ptypes.Image)

	for _, img := range imageDetails {
		for _, tag := range img.ImageTags {
			newImage := &ptypes.Image{
				Digest:         *img.ImageDigest,
				Tag:            *tag,
				RepositoryName: repoName,
				PushedAt:       img.ImagePushedAt,
			}

			if _, ok := imageIDMap[*tag]; ok {
				if _, ok := imageInfoMap[*tag]; !ok {
					imageInfoMap[*tag] = newImage
				}
			}

			if len(imageInfoMap) == int(maxResults) {
				break
			}
		}

		if len(imageInfoMap) == int(maxResults) {
			break
		}
	}

	for _, v := range imageInfoMap {
		res = append(res, v)
	}

	return res, resp.NextToken, nil
}

func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
	aws, err := repo.AWSIntegration().ReadAWSIntegration(
		r.ProjectID,
		r.AWSIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	sess, err := aws.GetSession()

	if err != nil {
		return nil, err
	}

	svc := ecr.New(sess)

	maxResults := int64(1000)

	var imageIDs []*ecr.ImageIdentifier

	resp, err := svc.ListImages(&ecr.ListImagesInput{
		RepositoryName: &repoName,
		MaxResults:     &maxResults,
	})

	if err != nil {
		return nil, err
	}

	if len(resp.ImageIds) == 0 {
		return []*ptypes.Image{}, nil
	}

	imageIDs = append(imageIDs, resp.ImageIds...)

	nextToken := resp.NextToken

	for nextToken != nil {
		resp, err := svc.ListImages(&ecr.ListImagesInput{
			RepositoryName: &repoName,
			MaxResults:     &maxResults,
			NextToken:      nextToken,
		})

		if err != nil {
			return nil, err
		}

		imageIDs = append(imageIDs, resp.ImageIds...)
		nextToken = resp.NextToken
	}

	imageIDLen := len(imageIDs)
	imageDetails := make([]*ecr.ImageDetail, 0)

	var wg sync.WaitGroup
	var mu sync.Mutex

	// AWS API expects the length of imageIDs to be at max 100 at a time
	for start := 0; start < imageIDLen; start += 100 {
		end := start + 100
		if end > imageIDLen {
			end = imageIDLen
		}

		wg.Add(1)

		go func(start, end int) {
			defer wg.Done()

			describeResp, err := svc.DescribeImages(&ecr.DescribeImagesInput{
				RepositoryName: &repoName,
				ImageIds:       imageIDs[start:end],
			})

			if err != nil {
				return
			}

			mu.Lock()
			imageDetails = append(imageDetails, describeResp.ImageDetails...)
			mu.Unlock()
		}(start, end)
	}

	wg.Wait()

	res := make([]*ptypes.Image, 0)
	imageInfoMap := make(map[string]*ptypes.Image)

	for _, img := range imageDetails {
		for _, tag := range img.ImageTags {
			newImage := &ptypes.Image{
				Digest:         *img.ImageDigest,
				Tag:            *tag,
				RepositoryName: repoName,
				PushedAt:       img.ImagePushedAt,
			}

			if _, ok := imageInfoMap[*tag]; !ok {
				imageInfoMap[*tag] = newImage
			}
		}
	}

	for _, v := range imageInfoMap {
		res = append(res, v)
	}

	return res, nil
}

func (r *Registry) listACRImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
	az, err := repo.AzureIntegration().ReadAzureIntegration(
		r.ProjectID,
		r.AzureIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	// use JWT token to request catalog
	client := &http.Client{}

	req, err := http.NewRequest(
		"GET",
		fmt.Sprintf("%s/v2/%s/tags/list", r.URL, repoName),
		nil,
	)

	if err != nil {
		return nil, err
	}

	req.SetBasicAuth(az.AzureClientID, string(az.ServicePrincipalSecret))

	resp, err := client.Do(req)

	if err != nil {
		return nil, err
	}

	gcrResp := gcrImageResp{}

	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
		return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
	}

	res := make([]*ptypes.Image, 0)

	for _, tag := range gcrResp.Tags {
		res = append(res, &ptypes.Image{
			RepositoryName: strings.TrimPrefix(repoName, "https://"),
			Tag:            tag,
		})
	}

	return res, nil
}

type gcrImageResp struct {
	Tags []string `json:"tags"`
}

func (r *Registry) listGCRImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
	gcp, err := repo.GCPIntegration().ReadGCPIntegration(
		r.ProjectID,
		r.GCPIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	// use JWT token to request catalog
	client := &http.Client{}

	parsedURL, err := url.Parse("https://" + r.URL)

	if err != nil {
		return nil, err
	}

	trimmedPath := strings.Trim(parsedURL.Path, "/")

	req, err := http.NewRequest(
		"GET",
		fmt.Sprintf("https://%s/v2/%s/%s/tags/list", parsedURL.Host, trimmedPath, repoName),
		nil,
	)

	if err != nil {
		return nil, err
	}

	req.SetBasicAuth("_json_key", string(gcp.GCPKeyData))

	resp, err := client.Do(req)

	if err != nil {
		return nil, err
	}

	gcrResp := gcrImageResp{}

	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
		return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
	}

	res := make([]*ptypes.Image, 0)

	for _, tag := range gcrResp.Tags {
		res = append(res, &ptypes.Image{
			RepositoryName: repoName,
			Tag:            tag,
		})
	}

	return res, nil
}

func (r *Registry) listGARImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
	repoImageSlice := strings.Split(repoName, "/")

	if len(repoImageSlice) != 2 {
		return nil, fmt.Errorf("invalid GAR repo name: %s. Expected to be in the form of REPOSITORY/IMAGE", repoName)
	}

	gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
		r.ProjectID,
		r.GCPIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	svc, err := v1artifactregistry.NewService(context.Background(), option.WithTokenSource(&garTokenSource{
		reg:  r,
		repo: repo,
	}), option.WithScopes("roles/artifactregistry.reader"))

	if err != nil {
		return nil, err
	}

	var res []*ptypes.Image

	parsedURL, err := url.Parse("https://" + r.URL)

	if err != nil {
		return nil, err
	}

	location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")
	dockerSvc := v1artifactregistry.NewProjectsLocationsRepositoriesDockerImagesService(svc)
	nextToken := ""

	for {
		resp, err := dockerSvc.List(fmt.Sprintf("projects/%s/locations/%s/repositories/%s",
			gcpInt.GCPProjectID, location, repoImageSlice[0])).PageSize(1000).PageToken(nextToken).Do()

		if err != nil {
			return nil, err
		}

		for _, image := range resp.DockerImages {
			named, err := reference.ParseNamed(image.Uri)

			if err != nil {
				continue
			}

			paths := strings.Split(reference.Path(named), "/")

			imageName := paths[len(paths)-1]

			if imageName == repoImageSlice[1] {
				uploadTime, _ := time.Parse(time.RFC3339, image.UploadTime)

				for _, tag := range image.Tags {
					res = append(res, &ptypes.Image{
						RepositoryName: repoName,
						Tag:            tag,
						PushedAt:       &uploadTime,
						Digest:         strings.Split(image.Uri, "@")[1],
					})
				}
			}
		}

		if resp.NextPageToken == "" {
			break
		}

		nextToken = resp.NextPageToken
	}

	return res, nil
}

func (r *Registry) listDOCRImages(
	repoName string,
	repo repository.Repository,
	doAuth *oauth2.Config,
) ([]*ptypes.Image, error) {
	oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(
		r.ProjectID,
		r.DOIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, repo))

	if err != nil {
		return nil, err
	}

	client := godo.NewFromToken(tok)

	urlArr := strings.Split(r.URL, "/")

	if len(urlArr) != 2 {
		return nil, fmt.Errorf("invalid digital ocean registry url")
	}

	name := urlArr[1]

	var tags []*godo.RepositoryTag
	opt := &godo.ListOptions{
		PerPage: 200,
	}

	for {
		nextTags, resp, err := client.Registry.ListRepositoryTags(context.TODO(), name, repoName, opt)

		if err != nil {
			return nil, err
		}

		tags = append(tags, nextTags...)

		if resp.Links == nil || resp.Links.IsLastPage() {
			break
		}

		page, err := resp.Links.CurrentPage()

		if err != nil {
			return nil, err
		}

		opt.Page = page + 1
	}

	res := make([]*ptypes.Image, 0)

	for _, tag := range tags {
		res = append(res, &ptypes.Image{
			RepositoryName: repoName,
			Tag:            tag.Tag,
		})
	}

	return res, nil
}

func (r *Registry) listPrivateRegistryImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
	// handle dockerhub different, as it doesn't implement the docker registry http api
	if strings.Contains(r.URL, "docker.io") {
		return r.listDockerHubImages(repoName, repo)
	}

	basic, err := repo.BasicIntegration().ReadBasicIntegration(
		r.ProjectID,
		r.BasicIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	// Just use service account key to authenticate, since scopes may not be in place
	// for oauth. This also prevents us from making more requests.
	client := &http.Client{}

	// get the host and scheme to make the request
	parsedURL, err := url.Parse(r.URL)

	req, err := http.NewRequest(
		"GET",
		fmt.Sprintf("%s://%s/v2/%s/tags/list", parsedURL.Scheme, parsedURL.Host, repoName),
		nil,
	)

	if err != nil {
		return nil, err
	}

	req.SetBasicAuth(string(basic.Username), string(basic.Password))

	resp, err := client.Do(req)

	if err != nil {
		return nil, err
	}

	gcrResp := gcrImageResp{}

	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
		return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
	}

	res := make([]*ptypes.Image, 0)

	for _, tag := range gcrResp.Tags {
		res = append(res, &ptypes.Image{
			RepositoryName: repoName,
			Tag:            tag,
		})
	}

	return res, nil
}

type dockerHubImageResult struct {
	Name string `json:"name"`
}

type dockerHubImageResp struct {
	Results []dockerHubImageResult `json:"results"`
}

type dockerHubLoginReq struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type dockerHubLoginResp struct {
	Token string `json:"token"`
}

func (r *Registry) listDockerHubImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
	basic, err := repo.BasicIntegration().ReadBasicIntegration(
		r.ProjectID,
		r.BasicIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	client := &http.Client{}

	// first, make a request for the access token

	data, err := json.Marshal(&dockerHubLoginReq{
		Username: string(basic.Username),
		Password: string(basic.Password),
	})

	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest(
		"POST",
		"https://hub.docker.com/v2/users/login",
		strings.NewReader(string(data)),
	)

	if err != nil {
		return nil, err
	}

	req.Header.Add("Content-Type", "application/json")

	resp, err := client.Do(req)

	if err != nil {
		return nil, err
	}

	tokenObj := dockerHubLoginResp{}

	if err := json.NewDecoder(resp.Body).Decode(&tokenObj); err != nil {
		return nil, fmt.Errorf("Could not decode Dockerhub token from response: %v", err)
	}

	req, err = http.NewRequest(
		"GET",
		fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/tags", strings.Split(r.URL, "docker.io/")[1]),
		nil,
	)

	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tokenObj.Token))

	resp, err = client.Do(req)

	if err != nil {
		return nil, err
	}

	imageResp := dockerHubImageResp{}

	if err := json.NewDecoder(resp.Body).Decode(&imageResp); err != nil {
		return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
	}

	res := make([]*ptypes.Image, 0)

	for _, result := range imageResp.Results {
		res = append(res, &ptypes.Image{
			RepositoryName: repoName,
			Tag:            result.Name,
		})
	}

	return res, nil
}

// GetDockerConfigJSON returns a dockerconfigjson file contents with "auths"
// populated.
func (r *Registry) GetDockerConfigJSON(
	repo repository.Repository,
	doAuth *oauth2.Config, // only required if using DOCR
) ([]byte, error) {
	var conf *configfile.ConfigFile
	var err error

	// switch on the auth mechanism to get a token
	if r.AWSIntegrationID != 0 {
		conf, err = r.getECRDockerConfigFile(repo)
	}

	if r.GCPIntegrationID != 0 {
		conf, err = r.getGCRDockerConfigFile(repo)
	}

	if r.DOIntegrationID != 0 {
		conf, err = r.getDOCRDockerConfigFile(repo, doAuth)
	}

	if r.BasicIntegrationID != 0 {
		conf, err = r.getPrivateRegistryDockerConfigFile(repo)
	}

	if r.AzureIntegrationID != 0 {
		conf, err = r.getACRDockerConfigFile(repo)
	}

	if err != nil {
		return nil, err
	}

	return json.Marshal(conf)
}

func (r *Registry) getECRDockerConfigFile(
	repo repository.Repository,
) (*configfile.ConfigFile, error) {
	aws, err := repo.AWSIntegration().ReadAWSIntegration(
		r.ProjectID,
		r.AWSIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	sess, err := aws.GetSession()

	if err != nil {
		return nil, err
	}

	ecrSvc := ecr.New(sess)

	output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})

	if err != nil {
		return nil, err
	}

	token := *output.AuthorizationData[0].AuthorizationToken

	decodedToken, err := base64.StdEncoding.DecodeString(token)

	if err != nil {
		return nil, err
	}

	parts := strings.SplitN(string(decodedToken), ":", 2)

	if len(parts) < 2 {
		return nil, err
	}

	key := r.URL

	if !strings.Contains(key, "http") {
		key = "https://" + key
	}

	return &configfile.ConfigFile{
		AuthConfigs: map[string]types.AuthConfig{
			key: {
				Username: parts[0],
				Password: parts[1],
				Auth:     token,
			},
		},
	}, nil
}

func (r *Registry) getGCRDockerConfigFile(
	repo repository.Repository,
) (*configfile.ConfigFile, error) {
	gcp, err := repo.GCPIntegration().ReadGCPIntegration(
		r.ProjectID,
		r.GCPIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	key := r.URL

	if !strings.Contains(key, "http") {
		key = "https://" + key
	}

	parsedURL, _ := url.Parse(key)

	return &configfile.ConfigFile{
		AuthConfigs: map[string]types.AuthConfig{
			parsedURL.Host: {
				Username: "_json_key",
				Password: string(gcp.GCPKeyData),
				Auth:     generateAuthToken("_json_key", string(gcp.GCPKeyData)),
			},
		},
	}, nil
}

func (r *Registry) getDOCRDockerConfigFile(
	repo repository.Repository,
	doAuth *oauth2.Config,
) (*configfile.ConfigFile, error) {
	oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(
		r.ProjectID,
		r.DOIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, repo))

	if err != nil {
		return nil, err
	}

	key := r.URL

	if !strings.Contains(key, "http") {
		key = "https://" + key
	}

	parsedURL, _ := url.Parse(key)

	return &configfile.ConfigFile{
		AuthConfigs: map[string]types.AuthConfig{
			parsedURL.Host: {
				Username: tok,
				Password: tok,
				Auth:     generateAuthToken(tok, tok),
			},
		},
	}, nil
}

func (r *Registry) getPrivateRegistryDockerConfigFile(
	repo repository.Repository,
) (*configfile.ConfigFile, error) {
	basic, err := repo.BasicIntegration().ReadBasicIntegration(
		r.ProjectID,
		r.BasicIntegrationID,
	)

	if err != nil {
		return nil, err
	}

	key := r.URL

	if !strings.Contains(key, "http") {
		key = "https://" + key
	}

	parsedURL, _ := url.Parse(key)

	authConfigKey := parsedURL.Host

	if strings.Contains(r.URL, "index.docker.io") {
		authConfigKey = "https://index.docker.io/v1/"
	}

	return &configfile.ConfigFile{
		AuthConfigs: map[string]types.AuthConfig{
			authConfigKey: {
				Username: string(basic.Username),
				Password: string(basic.Password),
				Auth:     generateAuthToken(string(basic.Username), string(basic.Password)),
			},
		},
	}, nil
}

func (r *Registry) getACRDockerConfigFile(
	repo repository.Repository,
) (*configfile.ConfigFile, error) {
	username, pw, err := r.GetACRCredentials(repo)

	if err != nil {
		return nil, err
	}

	key := r.URL

	if !strings.Contains(key, "http") {
		key = "https://" + key
	}

	parsedURL, _ := url.Parse(key)

	return &configfile.ConfigFile{
		AuthConfigs: map[string]types.AuthConfig{
			parsedURL.Host: {
				Username: string(username),
				Password: string(pw),
				Auth:     generateAuthToken(string(username), string(pw)),
			},
		},
	}, nil
}

func generateAuthToken(username, password string) string {
	return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
}
