package convert

import (
	"context"
	"sort"
	"strconv"
	"time"

	"github.com/99designs/gqlgen/graphql"
	ispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/vektah/gqlparser/v2/gqlerror"

	zerr "zotregistry.dev/zot/errors"
	zcommon "zotregistry.dev/zot/pkg/common"
	cveinfo "zotregistry.dev/zot/pkg/extensions/search/cve"
	"zotregistry.dev/zot/pkg/extensions/search/gql_generated"
	"zotregistry.dev/zot/pkg/extensions/search/pagination"
	"zotregistry.dev/zot/pkg/log"
	mTypes "zotregistry.dev/zot/pkg/meta/types"
	reqCtx "zotregistry.dev/zot/pkg/requestcontext"
)

type SkipQGLField struct {
	Vulnerabilities bool
}

func UpdateLastUpdatedTimestamp(repoLastUpdatedTimestamp *time.Time,
	lastUpdatedImageSummary *gql_generated.ImageSummary, imageSummary *gql_generated.ImageSummary,
) *gql_generated.ImageSummary {
	newLastUpdatedImageSummary := lastUpdatedImageSummary

	if repoLastUpdatedTimestamp.Equal(time.Time{}) {
		// initialize with first time value
		*repoLastUpdatedTimestamp = *imageSummary.LastUpdated
		newLastUpdatedImageSummary = imageSummary
	} else if repoLastUpdatedTimestamp.Before(*imageSummary.LastUpdated) {
		*repoLastUpdatedTimestamp = *imageSummary.LastUpdated
		newLastUpdatedImageSummary = imageSummary
	}

	return newLastUpdatedImageSummary
}

func getReferrers(referrersInfo []mTypes.ReferrerInfo) []*gql_generated.Referrer {
	referrers := make([]*gql_generated.Referrer, 0, len(referrersInfo))

	for _, referrerInfo := range referrersInfo {
		referrerInfo := referrerInfo

		referrers = append(referrers, &gql_generated.Referrer{
			MediaType:    &referrerInfo.MediaType,
			ArtifactType: &referrerInfo.ArtifactType,
			Size:         &referrerInfo.Size,
			Digest:       &referrerInfo.Digest,
			Annotations:  getAnnotationsFromMap(referrerInfo.Annotations),
		})
	}

	return referrers
}

func getAnnotationsFromMap(annotationsMap map[string]string) []*gql_generated.Annotation {
	annotations := make([]*gql_generated.Annotation, 0, len(annotationsMap))

	for key, value := range annotationsMap {
		key := key
		value := value

		annotations = append(annotations, &gql_generated.Annotation{
			Key:   &key,
			Value: &value,
		})
	}

	return annotations
}

func getImageBlobsInfo(manifestDigest string, manifestSize int64, configDigest string, configSize int64,
	layers []ispec.Descriptor,
) (int64, map[string]int64) {
	imageBlobsMap := map[string]int64{}
	imageSize := int64(0)

	// add config size
	imageSize += configSize
	imageBlobsMap[configDigest] = configSize

	// add manifest size
	imageSize += manifestSize
	imageBlobsMap[manifestDigest] = manifestSize

	// add layers size
	for _, layer := range layers {
		imageBlobsMap[layer.Digest.String()] = layer.Size
		imageSize += layer.Size
	}

	return imageSize, imageBlobsMap
}

func RepoMeta2ImageSummaries(ctx context.Context, repoMeta mTypes.RepoMeta,
	imageMeta map[string]mTypes.ImageMeta, skip SkipQGLField, cveInfo cveinfo.CveInfo,
) []*gql_generated.ImageSummary {
	imageSummaries := make([]*gql_generated.ImageSummary, 0, len(repoMeta.Tags))

	// Make sure the tags are sorted
	// We need to implement a proper fix for this taking into account
	// the sorting criteria used in the requested page
	tags := make([]string, 0, len(repoMeta.Tags))
	for tag := range repoMeta.Tags {
		tags = append(tags, tag)
	}

	// Sorting ascending by tag name should do for now
	sort.Strings(tags)

	for _, tag := range tags {
		descriptor := repoMeta.Tags[tag]

		imageSummary, _, err := FullImageMeta2ImageSummary(ctx, GetFullImageMeta(tag, repoMeta, imageMeta[descriptor.Digest]))
		if err != nil {
			continue
		}

		// CVE scanning is expensive, only scan for final slice of results
		updateImageSummaryVulnerabilities(ctx, imageSummary, skip, cveInfo)

		imageSummaries = append(imageSummaries, imageSummary)
	}

	return imageSummaries
}

func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMeta,
	imageMetaMap map[string]mTypes.ImageMeta, skip SkipQGLField, cveInfo cveinfo.CveInfo, log log.Logger,
) (*gql_generated.RepoSummary, []*gql_generated.ImageSummary) {
	repoName := repoMeta.Name
	imageSummaries := make([]*gql_generated.ImageSummary, 0, len(repoMeta.Tags))

	userCanDeleteTag, _ := reqCtx.CanDelete(ctx, repoName)

	for tag, descriptor := range repoMeta.Tags {
		imageMeta := imageMetaMap[descriptor.Digest]

		imageSummary, _, err := FullImageMeta2ImageSummary(ctx, GetFullImageMeta(tag, repoMeta, imageMeta))
		if err != nil {
			log.Error().Str("repository", repoName).Str("reference", tag).Str("component", "metadb").
				Msg("error while converting descriptor for image")

			continue
		}

		imageSummary.IsDeletable = &userCanDeleteTag

		updateImageSummaryVulnerabilities(ctx, imageSummary, skip, cveInfo)

		imageSummaries = append(imageSummaries, imageSummary)
	}

	repoSummary := RepoMeta2RepoSummary(ctx, repoMeta, imageMetaMap)

	updateRepoSummaryVulnerabilities(ctx, repoSummary, skip, cveInfo)

	return repoSummary, imageSummaries
}

func GetFullImageMeta(tag string, repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta,
) mTypes.FullImageMeta {
	return mTypes.FullImageMeta{
		Repo:       repoMeta.Name,
		Tag:        tag,
		MediaType:  imageMeta.MediaType,
		Digest:     imageMeta.Digest,
		Size:       imageMeta.Size,
		Index:      imageMeta.Index,
		Manifests:  GetFullManifestMeta(repoMeta, imageMeta.Manifests),
		Referrers:  repoMeta.Referrers[imageMeta.Digest.String()],
		Statistics: repoMeta.Statistics[imageMeta.Digest.String()],
		Signatures: repoMeta.Signatures[imageMeta.Digest.String()],
	}
}

func GetFullManifestMeta(repoMeta mTypes.RepoMeta, manifests []mTypes.ManifestMeta) []mTypes.FullManifestMeta {
	results := make([]mTypes.FullManifestMeta, 0, len(manifests))

	for i := range manifests {
		results = append(results, mTypes.FullManifestMeta{
			ManifestMeta: manifests[i],
			Referrers:    repoMeta.Referrers[manifests[i].Digest.String()],
			Statistics:   repoMeta.Statistics[manifests[i].Digest.String()],
			Signatures:   repoMeta.Signatures[manifests[i].Digest.String()],
		})
	}

	return results
}

func StringMap2Annotations(strMap map[string]string) []*gql_generated.Annotation {
	annotations := make([]*gql_generated.Annotation, 0, len(strMap))

	for key, value := range strMap {
		key := key
		value := value

		annotations = append(annotations, &gql_generated.Annotation{
			Key:   &key,
			Value: &value,
		})
	}

	return annotations
}

func GetPreloads(ctx context.Context) map[string]bool {
	if !graphql.HasOperationContext(ctx) {
		return map[string]bool{}
	}

	nestedPreloads := GetNestedPreloads(
		graphql.GetOperationContext(ctx),
		graphql.CollectFieldsCtx(ctx, nil),
		"",
	)

	preloads := map[string]bool{}

	for _, str := range nestedPreloads {
		preloads[str] = true
	}

	return preloads
}

func GetNestedPreloads(ctx *graphql.OperationContext, fields []graphql.CollectedField, prefix string,
) []string {
	preloads := []string{}

	for _, column := range fields {
		prefixColumn := GetPreloadString(prefix, column.Name)
		preloads = append(preloads, prefixColumn)
		preloads = append(preloads,
			GetNestedPreloads(ctx, graphql.CollectFields(ctx, column.Selections, nil), prefixColumn)...,
		)
	}

	return preloads
}

func GetPreloadString(prefix, name string) string {
	if len(prefix) > 0 {
		return prefix + "." + name
	}

	return name
}

func GetSignaturesInfo(isSigned bool, signatures mTypes.ManifestSignatures) []*gql_generated.SignatureSummary {
	signaturesInfo := []*gql_generated.SignatureSummary{}

	if !isSigned {
		return signaturesInfo
	}

	for sigType, signatures := range signatures {
		for _, sig := range signatures {
			for _, layer := range sig.LayersInfo {
				var (
					isTrusted bool
					author    string
					tool      string
				)

				if layer.Signer != "" {
					author = layer.Signer

					if !layer.Date.IsZero() && time.Now().After(layer.Date) {
						isTrusted = false
					} else {
						isTrusted = true
					}
				} else {
					isTrusted = false
					author = ""
				}

				tool = sigType

				signaturesInfo = append(signaturesInfo,
					&gql_generated.SignatureSummary{Tool: &tool, IsTrusted: &isTrusted, Author: &author})
			}
		}
	}

	return signaturesInfo
}

func PaginatedRepoMeta2RepoSummaries(ctx context.Context, repoMetaList []mTypes.RepoMeta,
	imageMetaMap map[string]mTypes.ImageMeta, filter mTypes.Filter, pageInput pagination.PageInput,
	cveInfo cveinfo.CveInfo, skip SkipQGLField,
) ([]*gql_generated.RepoSummary, zcommon.PageInfo, error) {
	reposPageFinder, err := pagination.NewRepoSumPageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy)
	if err != nil {
		return []*gql_generated.RepoSummary{}, zcommon.PageInfo{}, err
	}

	for _, repoMeta := range repoMetaList {
		repoSummary := RepoMeta2RepoSummary(ctx, repoMeta, imageMetaMap)

		if RepoSumAcceptedByFilter(repoSummary, filter) {
			reposPageFinder.Add(repoSummary)
		}
	}

	page, pageInfo := reposPageFinder.Page()

	// CVE scanning is expensive, only scan for the current page
	for _, repoSummary := range page {
		updateRepoSummaryVulnerabilities(ctx, repoSummary, skip, cveInfo)
	}

	return page, pageInfo, nil
}

func RepoMeta2RepoSummary(ctx context.Context, repoMeta mTypes.RepoMeta,
	imageMetaMap map[string]mTypes.ImageMeta,
) *gql_generated.RepoSummary {
	var (
		repoName                 = repoMeta.Name
		lastUpdatedImage         = deref(repoMeta.LastUpdatedImage, mTypes.LastUpdatedImage{})
		lastUpdatedImageMeta     = imageMetaMap[lastUpdatedImage.Digest]
		lastUpdatedTag           = lastUpdatedImage.Tag
		repoLastUpdatedTimestamp = lastUpdatedImage.LastUpdated
		repoPlatforms            = repoMeta.Platforms
		repoVendors              = repoMeta.Vendors
		repoDownloadCount        = repoMeta.DownloadCount
		repoStarCount            = repoMeta.StarCount
		repoIsUserStarred        = repoMeta.IsStarred    // value specific to the current user
		repoIsUserBookMarked     = repoMeta.IsBookmarked // value specific to the current user
		repoSize                 = repoMeta.Size
	)

	if repoLastUpdatedTimestamp == nil {
		repoLastUpdatedTimestamp = &time.Time{}
	}

	imageSummary, _, err := FullImageMeta2ImageSummary(ctx, GetFullImageMeta(lastUpdatedTag, repoMeta,
		lastUpdatedImageMeta))
	_ = err

	return &gql_generated.RepoSummary{
		Name:          &repoName,
		LastUpdated:   repoLastUpdatedTimestamp,
		Size:          ref(strconv.FormatInt(repoSize, 10)),
		Platforms:     getGqlPlatforms(repoPlatforms),
		Vendors:       getGqlVendors(repoVendors),
		NewestImage:   imageSummary,
		DownloadCount: &repoDownloadCount,
		StarCount:     &repoStarCount,
		IsBookmarked:  &repoIsUserBookMarked,
		IsStarred:     &repoIsUserStarred,
		Rank:          ref(repoMeta.Rank),
	}
}

func getGqlVendors(repoVendors []string) []*string {
	result := make([]*string, 0, len(repoVendors))

	for i := range repoVendors {
		result = append(result, &repoVendors[i])
	}

	return result
}

func getGqlPlatforms(repoPlatforms []ispec.Platform) []*gql_generated.Platform {
	result := make([]*gql_generated.Platform, 0, len(repoPlatforms))

	for i := range repoPlatforms {
		result = append(result, &gql_generated.Platform{
			Os:   ref(repoPlatforms[i].OS),
			Arch: ref(getArch(repoPlatforms[i].Architecture, repoPlatforms[i].Variant)),
		})
	}

	return result
}

type (
	ManifestDigest = string
	BlobDigest     = string
)

func FullImageMeta2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullImageMeta,
) (*gql_generated.ImageSummary, map[BlobDigest]int64, error) {
	switch fullImageMeta.MediaType {
	case ispec.MediaTypeImageManifest:
		return ImageManifest2ImageSummary(ctx, fullImageMeta)
	case ispec.MediaTypeImageIndex:
		return ImageIndex2ImageSummary(ctx, fullImageMeta)
	default:
		return nil, nil, zerr.ErrMediaTypeNotSupported
	}
}

func ImageIndex2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullImageMeta,
) (*gql_generated.ImageSummary, map[BlobDigest]int64, error) {
	var (
		repo                = fullImageMeta.Repo
		tag                 = fullImageMeta.Tag
		indexLastUpdated    time.Time
		isSigned            = isImageSigned(fullImageMeta.Signatures)
		indexSize           = int64(0)
		manifestAnnotations *ImageAnnotations
		manifestSummaries   = make([]*gql_generated.ManifestSummary, 0, len(fullImageMeta.Manifests))
		indexBlobs          = map[string]int64{}

		indexDigestStr = fullImageMeta.Digest.String()
		indexMediaType = ispec.MediaTypeImageIndex
	)

	for _, imageManifest := range fullImageMeta.Manifests {
		imageManifestSummary, manifestBlobs, err := ImageManifest2ImageSummary(ctx, mTypes.FullImageMeta{
			Repo:       fullImageMeta.Repo,
			Tag:        fullImageMeta.Tag,
			MediaType:  ispec.MediaTypeImageManifest,
			Digest:     imageManifest.Digest,
			Size:       imageManifest.Size,
			Manifests:  []mTypes.FullManifestMeta{imageManifest},
			Referrers:  imageManifest.Referrers,
			Statistics: imageManifest.Statistics,
			Signatures: imageManifest.Signatures,
		})
		if err != nil {
			return &gql_generated.ImageSummary{}, map[string]int64{}, err
		}

		manifestSize := int64(0)

		for digest, size := range manifestBlobs {
			indexBlobs[digest] = size
			manifestSize += size
		}

		if indexLastUpdated.Before(*imageManifestSummary.LastUpdated) {
			indexLastUpdated = *imageManifestSummary.LastUpdated
		}

		annotations := GetAnnotations(imageManifest.Manifest.Annotations, imageManifest.Config.Config.Labels)
		if manifestAnnotations == nil {
			manifestAnnotations = &annotations
		}

		indexSize += manifestSize

		manifestSummaries = append(manifestSummaries, imageManifestSummary.Manifests[0])
	}

	signaturesInfo := GetSignaturesInfo(isSigned, fullImageMeta.Signatures)

	if manifestAnnotations == nil {
		manifestAnnotations = &ImageAnnotations{}
	}

	annotations := GetIndexAnnotations(fullImageMeta.Index.Annotations, manifestAnnotations)

	imageLastUpdated := annotations.Created
	if imageLastUpdated == nil {
		imageLastUpdated = &indexLastUpdated
	}

	indexSummary := gql_generated.ImageSummary{
		RepoName:      &repo,
		Tag:           &tag,
		Digest:        &indexDigestStr,
		MediaType:     &indexMediaType,
		Manifests:     manifestSummaries,
		LastUpdated:   imageLastUpdated,
		IsSigned:      &isSigned,
		SignatureInfo: signaturesInfo,
		Size:          ref(strconv.FormatInt(indexSize, 10)),
		DownloadCount: ref(fullImageMeta.Statistics.DownloadCount),
		Description:   &annotations.Description,
		Title:         &annotations.Title,
		Documentation: &annotations.Documentation,
		Licenses:      &annotations.Licenses,
		Labels:        &annotations.Labels,
		Source:        &annotations.Source,
		Vendor:        &annotations.Vendor,
		Authors:       &annotations.Authors,
		Referrers:     getReferrers(fullImageMeta.Referrers),
	}

	return &indexSummary, indexBlobs, nil
}

func ImageManifest2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullImageMeta,
) (*gql_generated.ImageSummary, map[BlobDigest]int64, error) {
	manifest := fullImageMeta.Manifests[0]

	var (
		repoName          = fullImageMeta.Repo
		tag               = fullImageMeta.Tag
		configDigest      = manifest.Manifest.Config.Digest.String()
		configSize        = manifest.Manifest.Config.Size
		manifestDigest    = manifest.Digest.String()
		manifestSize      = manifest.Size
		mediaType         = manifest.Manifest.MediaType
		artifactType      = zcommon.GetManifestArtifactType(fullImageMeta.Manifests[0].Manifest)
		platform          = getPlatform(manifest.Config.Platform)
		downloadCount     = fullImageMeta.Statistics.DownloadCount
		isSigned          = isImageSigned(fullImageMeta.Signatures)
		lastPullTimestamp = fullImageMeta.Statistics.LastPullTimestamp
		pushTimestamp     = fullImageMeta.Statistics.PushTimestamp
	)

	imageSize, imageBlobsMap := getImageBlobsInfo(manifestDigest, manifestSize, configDigest, configSize,
		manifest.Manifest.Layers)
	imageSizeStr := strconv.FormatInt(imageSize, 10)
	annotations := GetAnnotations(manifest.Manifest.Annotations, manifest.Config.Config.Labels)

	authors := annotations.Authors
	if authors == "" {
		authors = manifest.Config.Author
	}

	imageLastUpdated := annotations.Created
	if imageLastUpdated == nil {
		configCreated := zcommon.GetImageLastUpdated(manifest.Config)
		imageLastUpdated = &configCreated
	}

	historyEntries, err := getAllHistory(manifest.Manifest, manifest.Config)
	if err != nil {
		graphql.AddError(ctx, gqlerror.Errorf("error generating history on tag %s in repo %s: "+
			"manifest digest: %s, error: %s", tag, repoName, manifest.Digest, err.Error()))
	}

	signaturesInfo := GetSignaturesInfo(isSigned, fullImageMeta.Signatures)

	manifestSummary := gql_generated.ManifestSummary{
		Digest:        &manifestDigest,
		ConfigDigest:  &configDigest,
		LastUpdated:   imageLastUpdated,
		Size:          &imageSizeStr,
		IsSigned:      &isSigned,
		SignatureInfo: signaturesInfo,
		Platform:      &platform,
		DownloadCount: &downloadCount,
		Layers:        getLayersSummaries(manifest.Manifest),
		History:       historyEntries,
		Referrers:     getReferrers(fullImageMeta.Referrers),
		ArtifactType:  &artifactType,
	}

	imageSummary := gql_generated.ImageSummary{
		RepoName:          &repoName,
		Tag:               &tag,
		Digest:            &manifestDigest,
		MediaType:         &mediaType,
		Manifests:         []*gql_generated.ManifestSummary{&manifestSummary},
		LastUpdated:       imageLastUpdated,
		IsSigned:          &isSigned,
		SignatureInfo:     signaturesInfo,
		Size:              &imageSizeStr,
		DownloadCount:     &downloadCount,
		LastPullTimestamp: &lastPullTimestamp,
		PushTimestamp:     &pushTimestamp,
		Description:       &annotations.Description,
		Title:             &annotations.Title,
		Documentation:     &annotations.Documentation,
		Licenses:          &annotations.Licenses,
		Labels:            &annotations.Labels,
		Source:            &annotations.Source,
		Vendor:            &annotations.Vendor,
		Authors:           &authors,
		Referrers:         manifestSummary.Referrers,
	}

	return &imageSummary, imageBlobsMap, nil
}

func isImageSigned(manifestSignatures mTypes.ManifestSignatures) bool {
	for _, signatures := range manifestSignatures {
		if len(signatures) > 0 {
			return true
		}
	}

	return false
}

func getPlatform(platform ispec.Platform) gql_generated.Platform {
	return gql_generated.Platform{
		Os:   ref(platform.OS),
		Arch: ref(getArch(platform.Architecture, platform.Variant)),
	}
}

func getArch(arch string, variant string) string {
	if variant != "" {
		arch = arch + "/" + variant
	}

	return arch
}

func ref[T any](val T) *T {
	ref := val

	return &ref
}

func deref[T any](pointer *T, defaultVal T) T {
	if pointer != nil {
		return *pointer
	}

	return defaultVal
}

func PaginatedFullImageMeta2ImageSummaries(ctx context.Context, imageMetaList []mTypes.FullImageMeta, skip SkipQGLField,
	cveInfo cveinfo.CveInfo, filter mTypes.Filter, pageInput pagination.PageInput,
) ([]*gql_generated.ImageSummary, zcommon.PageInfo, error) {
	imagePageFinder, err := pagination.NewImgSumPageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy)
	if err != nil {
		return []*gql_generated.ImageSummary{}, zcommon.PageInfo{}, err
	}

	for _, imageMeta := range imageMetaList {
		imageSummary, _, err := FullImageMeta2ImageSummary(ctx, imageMeta)
		if err != nil {
			continue
		}

		if ImgSumAcceptedByFilter(imageSummary, filter) {
			imagePageFinder.Add(imageSummary)
		}
	}

	page, pageInfo := imagePageFinder.Page()

	for _, imageSummary := range page {
		// CVE scanning is expensive, only scan for this page
		updateImageSummaryVulnerabilities(ctx, imageSummary, skip, cveInfo)
	}

	return page, pageInfo, nil
}
