package vm

import (
	"context"
	"io"

	lru "github.com/hashicorp/golang-lru/v2"
	ebsfile "github.com/masahiro331/go-ebs-file"
	"golang.org/x/xerrors"

	"github.com/aquasecurity/trivy/pkg/cache"
	config "github.com/aquasecurity/trivy/pkg/config/aws"
	"github.com/aquasecurity/trivy/pkg/fanal/artifact"
	"github.com/aquasecurity/trivy/pkg/fanal/types"
	"github.com/aquasecurity/trivy/pkg/log"
)

// default block size 512 KB
// Max cache memory size 64 MB
const storageEBSCacheSize = 128

const ebsArtifactVersion = 0

// EBS represents an artifact for AWS EBS snapshots
type EBS struct {
	Storage
	logger     *log.Logger
	snapshotID string
	ebs        ebsfile.EBSAPI
}

func newEBS(snapshotID string, vm Storage, region, endpoint string) (*EBS, error) {
	ebs, err := ebsfile.New(context.TODO(), config.MakeAWSOptions(region, endpoint)...)
	if err != nil {
		return nil, xerrors.Errorf("new ebsfile error: %w", err)
	}

	return &EBS{
		Storage:    vm,
		logger:     log.WithPrefix("ebs"),
		snapshotID: snapshotID,
		ebs:        ebs,
	}, nil
}

func (a *EBS) Inspect(ctx context.Context) (artifact.Reference, error) {
	sr, err := a.openEBS(ctx)
	if err != nil {
		return artifact.Reference{}, xerrors.Errorf("EBS open error: %w", err)
	}

	cacheKey, err := a.calcCacheKey(a.snapshotID)
	if err != nil {
		return artifact.Reference{}, xerrors.Errorf("cache key calculation error: %w", err)
	}

	if a.hasCache(ctx, cacheKey) {
		return artifact.Reference{
			Name:    a.snapshotID,
			Type:    types.TypeVM,
			ID:      cacheKey, // use a cache key as pseudo artifact ID
			BlobIDs: []string{cacheKey},
		}, nil
	}

	blobInfo, err := a.Analyze(ctx, sr)
	if err != nil {
		return artifact.Reference{}, xerrors.Errorf("inspection error: %w", err)
	}

	if err = a.cache.PutBlob(ctx, cacheKey, blobInfo); err != nil {
		return artifact.Reference{}, xerrors.Errorf("failed to store blob (%s) in cache: %w", cacheKey, err)
	}

	return artifact.Reference{
		Name:    a.snapshotID,
		Type:    types.TypeVM,
		ID:      cacheKey, // use a cache key as pseudo artifact ID
		BlobIDs: []string{cacheKey},
	}, nil
}

func (a *EBS) openEBS(ctx context.Context) (*io.SectionReader, error) {
	c, err := lru.New[string, []byte](storageEBSCacheSize)
	if err != nil {
		return nil, xerrors.Errorf("lru cache error: %w", err)
	}

	r, err := ebsfile.Open(a.snapshotID, ctx, c, a.ebs)
	if err != nil {
		return nil, xerrors.Errorf("EBS error: %w", err)
	}
	return r, nil
}

func (a *EBS) Clean(_ artifact.Reference) error {
	return nil
}

func (a *EBS) SetEBS(ebs ebsfile.EBSAPI) {
	a.ebs = ebs
}

func (a *EBS) calcCacheKey(key string) (string, error) {
	s, err := cache.CalcKey(key, ebsArtifactVersion, a.analyzer.AnalyzerVersions(), a.handlerManager.Versions(), a.artifactOption)
	if err != nil {
		return "", xerrors.Errorf("failed to calculate cache key: %w", err)
	}
	return s, nil
}

func (a *EBS) hasCache(ctx context.Context, cacheKey string) bool {
	_, missingCacheKeys, err := a.cache.MissingBlobs(ctx, cacheKey, []string{cacheKey})
	if err != nil {
		a.logger.Debug("Unable to query missing cache", log.Err(err))
		return false
	}

	// Cache exists
	if len(missingCacheKeys) == 0 {
		return true
	}

	a.logger.Debug("Missing virtual machine cache", log.String("key", cacheKey))
	return false
}
