package cache

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"time"

	"github.com/hashicorp/go-multierror"
	bolt "go.etcd.io/bbolt"
	bberrors "go.etcd.io/bbolt/errors"
	"golang.org/x/xerrors"

	"github.com/aquasecurity/trivy/pkg/fanal/types"
)

const defaultFSCacheTimeout = 5 * time.Second

var _ Cache = &FSCache{}

type FSCache struct {
	db        *bolt.DB
	directory string
}

func NewFSCache(cacheDir string) (FSCache, error) {
	dir := filepath.Join(cacheDir, scanCacheDirName)
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return FSCache{}, xerrors.Errorf("failed to create cache dir: %w", err)
	}

	db, err := bolt.Open(filepath.Join(dir, "fanal.db"), 0o600, &bolt.Options{
		Timeout: defaultFSCacheTimeout,
	})
	if err != nil {
		// Check if the error is due to timeout (database locked by another process)
		if errors.Is(err, bberrors.ErrTimeout) {
			return FSCache{}, xerrors.Errorf("cache may be in use by another process: %w", err)
		}
		return FSCache{}, xerrors.Errorf("unable to open cache DB: %w", err)
	}

	err = db.Update(func(tx *bolt.Tx) error {
		for _, bucket := range []string{
			artifactBucket,
			blobBucket,
		} {
			if _, err := tx.CreateBucketIfNotExists([]byte(bucket)); err != nil {
				return xerrors.Errorf("unable to create %s bucket: %w", bucket, err)
			}
		}
		return nil
	})
	if err != nil {
		return FSCache{}, xerrors.Errorf("DB error: %w", err)
	}

	return FSCache{
		db:        db,
		directory: dir,
	}, nil
}

// GetBlob gets blob information such as layer data from local cache
func (fs FSCache) GetBlob(_ context.Context, blobID string) (types.BlobInfo, error) {
	var blobInfo types.BlobInfo
	err := fs.db.View(func(tx *bolt.Tx) error {
		var err error
		blobBucket := tx.Bucket([]byte(blobBucket))
		blobInfo, err = fs.getBlob(blobBucket, blobID)
		if err != nil {
			return xerrors.Errorf("failed to get blob from the cache: %w", err)
		}
		return nil
	})
	if err != nil {
		return types.BlobInfo{}, xerrors.Errorf("DB error: %w", err)
	}
	return blobInfo, nil
}

func (fs FSCache) getBlob(blobBucket *bolt.Bucket, diffID string) (types.BlobInfo, error) {
	b := blobBucket.Get([]byte(diffID))

	var l types.BlobInfo
	if err := json.Unmarshal(b, &l); err != nil {
		return types.BlobInfo{}, xerrors.Errorf("JSON unmarshal error: %w", err)
	}
	return l, nil
}

// PutBlob stores blob information such as layer information in local cache
func (fs FSCache) PutBlob(_ context.Context, blobID string, blobInfo types.BlobInfo) error {
	b, err := json.Marshal(blobInfo)
	if err != nil {
		return xerrors.Errorf("unable to marshal blob JSON (%s): %w", blobID, err)
	}
	err = fs.db.Update(func(tx *bolt.Tx) error {
		blobBucket := tx.Bucket([]byte(blobBucket))
		err = blobBucket.Put([]byte(blobID), b)
		if err != nil {
			return xerrors.Errorf("unable to store blob information in cache (%s): %w", blobID, err)
		}
		return nil
	})
	if err != nil {
		return xerrors.Errorf("DB update error: %w", err)
	}
	return nil
}

// GetArtifact gets artifact information such as image metadata from local cache
func (fs FSCache) GetArtifact(_ context.Context, artifactID string) (types.ArtifactInfo, error) {
	var blob []byte
	err := fs.db.View(func(tx *bolt.Tx) error {
		artifactBucket := tx.Bucket([]byte(artifactBucket))
		blob = artifactBucket.Get([]byte(artifactID))
		return nil
	})
	if err != nil {
		return types.ArtifactInfo{}, xerrors.Errorf("DB error: %w", err)
	}

	var info types.ArtifactInfo
	if err := json.Unmarshal(blob, &info); err != nil {
		return types.ArtifactInfo{}, xerrors.Errorf("JSON unmarshal error: %w", err)
	}
	return info, nil
}

// DeleteBlobs removes blobs by IDs
func (fs FSCache) DeleteBlobs(_ context.Context, blobIDs []string) error {
	var errs error
	err := fs.db.Update(func(tx *bolt.Tx) error {
		bucket := tx.Bucket([]byte(blobBucket))
		for _, blobID := range blobIDs {
			if err := bucket.Delete([]byte(blobID)); err != nil {
				errs = multierror.Append(errs, err)
			}
		}
		return nil
	})
	if err != nil {
		return xerrors.Errorf("DB delete error: %w", err)
	}
	return errs
}

// PutArtifact stores artifact information such as image metadata in local cache
func (fs FSCache) PutArtifact(_ context.Context, artifactID string, artifactInfo types.ArtifactInfo) (err error) {
	b, err := json.Marshal(artifactInfo)
	if err != nil {
		return xerrors.Errorf("unable to marshal artifact JSON (%s): %w", artifactID, err)
	}

	err = fs.db.Update(func(tx *bolt.Tx) error {
		artifactBucket := tx.Bucket([]byte(artifactBucket))
		err = artifactBucket.Put([]byte(artifactID), b)
		if err != nil {
			return xerrors.Errorf("unable to store artifact information in cache (%s): %w", artifactID, err)
		}
		return nil
	})
	if err != nil {
		return xerrors.Errorf("DB update error: %w", err)
	}
	return nil
}

// MissingBlobs returns missing blob IDs such as layer IDs
func (fs FSCache) MissingBlobs(ctx context.Context, artifactID string, blobIDs []string) (bool, []string, error) {
	var missingArtifact bool
	var missingBlobIDs []string
	err := fs.db.View(func(tx *bolt.Tx) error {
		blobBucket := tx.Bucket([]byte(blobBucket))
		for _, blobID := range blobIDs {
			blobInfo, err := fs.getBlob(blobBucket, blobID)
			if err != nil {
				// error means cache missed blob info
				missingBlobIDs = append(missingBlobIDs, blobID)
				continue
			}
			if blobInfo.SchemaVersion != types.BlobJSONSchemaVersion {
				missingBlobIDs = append(missingBlobIDs, blobID)
			}
		}
		return nil
	})
	if err != nil {
		return false, nil, xerrors.Errorf("DB error: %w", err)
	}

	// get artifact info
	artifactInfo, err := fs.GetArtifact(ctx, artifactID)
	if err != nil {
		// error means cache missed artifact info
		return true, missingBlobIDs, nil
	}
	if artifactInfo.SchemaVersion != types.ArtifactJSONSchemaVersion {
		missingArtifact = true
	}
	return missingArtifact, missingBlobIDs, nil
}

// Close closes the database
func (fs FSCache) Close() error {
	if err := fs.db.Close(); err != nil {
		return xerrors.Errorf("unable to close DB: %w", err)
	}
	return nil
}

// Clear removes the database
func (fs FSCache) Clear(_ context.Context) error {
	if err := fs.Close(); err != nil {
		return err
	}
	if err := os.RemoveAll(fs.directory); err != nil {
		return xerrors.Errorf("failed to remove cache: %w", err)
	}
	return nil
}
