package v6

import (
	"fmt"
	"time"

	"gorm.io/gorm"

	"github.com/anchore/go-logger"
	"github.com/anchore/grype/internal/log"
	"github.com/anchore/grype/internal/schemaver"
)

type VulnerabilityDecoratorStoreWriter interface {
	AddKnownExploitedVulnerabilities(...*KnownExploitedVulnerabilityHandle) error
	AddEpss(...*EpssHandle) error
	AddCWE(...*CWEHandle) error
}

type VulnerabilityDecoratorStoreReader interface {
	GetKnownExploitedVulnerabilities(cve string) ([]KnownExploitedVulnerabilityHandle, error)
	GetEpss(cve string) ([]EpssHandle, error)
	GetCWEs(cve string) ([]CWEHandle, error)
}

type vulnerabilityDecoratorStore struct {
	db        *gorm.DB
	blobStore *blobStore
	epssDate  *time.Time
	vulnerabilityDecoratorCapabilities
}

type vulnerabilityDecoratorCapabilities struct {
	kevEnabled  bool
	epssEnabled bool
	cweEnabled  bool
}

func newVulnerabilityDecoratorStore(db *gorm.DB, bs *blobStore, dbVersion schemaver.SchemaVer) *vulnerabilityDecoratorStore {
	minSupportedKEVClientVersion := schemaver.New(6, 0, 1)
	minSupportedEPSSClientVersion := schemaver.New(6, 0, 2)
	minSupportedCWEClientVersion := schemaver.New(6, 1, 2)
	return &vulnerabilityDecoratorStore{
		db:        db,
		blobStore: bs,
		vulnerabilityDecoratorCapabilities: vulnerabilityDecoratorCapabilities{
			kevEnabled:  dbVersion.GreaterOrEqualTo(minSupportedKEVClientVersion),
			epssEnabled: dbVersion.GreaterOrEqualTo(minSupportedEPSSClientVersion),
			cweEnabled:  dbVersion.GreaterOrEqualTo(minSupportedCWEClientVersion),
		},
	}
}

func (s *vulnerabilityDecoratorStore) AddEpss(epss ...*EpssHandle) error {
	if !s.epssEnabled {
		// when populating a new DB any capability issues found should result in halting
		return ErrDBCapabilityNotSupported
	}

	for i := range epss {
		e := epss[i]

		if err := s.db.Create(e).Error; err != nil {
			return fmt.Errorf("unable to create EPSS: %w", err)
		}

		if err := s.setEPSSMetadata(e.Date); err != nil {
			return fmt.Errorf("unable to set EPSS metadata: %w", err)
		}
	}
	return nil
}

func (s *vulnerabilityDecoratorStore) AddCWE(cwe ...*CWEHandle) error {
	if !s.cweEnabled {
		// when populating a new DB any capability issues found should result in halting
		return ErrDBCapabilityNotSupported
	}

	for i := range cwe {
		c := cwe[i]

		if err := s.db.Create(c).Error; err != nil {
			return fmt.Errorf("unable to create CWE: %w", err)
		}
	}
	return nil
}

func (s *vulnerabilityDecoratorStore) setEPSSMetadata(date time.Time) error {
	if !s.epssEnabled {
		// when populating a new DB any capability issues found should result in halting
		return ErrDBCapabilityNotSupported
	}

	if s.epssDate != nil {
		if s.epssDate.Equal(date) {
			return nil
		}
		return fmt.Errorf("observed multiple EPSS dates: current=%q new=%q", s.epssDate.String(), date.String())
	}

	log.Trace("writing EPSS metadata")

	if err := s.db.Where("true").Delete(&EpssMetadata{}).Error; err != nil {
		return fmt.Errorf("failed to delete existing EPSS metadata record: %w", err)
	}

	instance := &EpssMetadata{
		Date: date,
	}

	if err := s.db.Create(instance).Error; err != nil {
		return fmt.Errorf("failed to create EPSS metadata record: %w", err)
	}

	s.epssDate = &date
	return nil
}

func (s *vulnerabilityDecoratorStore) getEPSSMetadata() (*EpssMetadata, error) {
	log.Trace("fetching EPSS metadata")

	var model EpssMetadata

	result := s.db.First(&model)
	return &model, result.Error
}

func (s *vulnerabilityDecoratorStore) GetEpss(cve string) ([]EpssHandle, error) {
	if !s.epssEnabled {
		// capability incompatibilities should gracefully degrade, returning no data or errors
		return nil, nil
	}

	fields := logger.Fields{
		"cve": cve,
	}
	start := time.Now()
	var count int
	defer func() {
		fields["duration"] = time.Since(start)
		fields["records"] = count
		log.WithFields(fields).Trace("fetched EPSS records")
	}()

	var models []EpssHandle
	var results []*EpssHandle

	if s.epssDate == nil {
		// fetch and cache the EPSS metadata
		metadata, err := s.getEPSSMetadata()
		if err != nil {
			return nil, fmt.Errorf("unable to fetch EPSS metadata: %w", err)
		}
		s.epssDate = &metadata.Date
	}

	if err := s.db.Where("cve = ? collate nocase", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {
		for _, r := range results {
			r.Date = *s.epssDate
			models = append(models, *r)
		}

		count += len(results)

		return nil
	}).Error; err != nil {
		return models, fmt.Errorf("unable to fetch EPSS records: %w", err)
	}

	return models, nil
}

func (s *vulnerabilityDecoratorStore) GetCWEs(cve string) ([]CWEHandle, error) {
	if !s.cweEnabled {
		// capability incompatibilities should gracefully degrade, returning no data or errors
		return nil, nil
	}

	fields := logger.Fields{
		"cve": cve,
	}
	start := time.Now()
	var count int
	defer func() {
		fields["duration"] = time.Since(start)
		fields["records"] = count
		log.WithFields(fields).Trace("fetched CWE records")
	}()

	var models []CWEHandle
	var results []*CWEHandle

	if err := s.db.Where("cve = ? collate nocase", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {
		for _, r := range results {
			models = append(models, *r)
		}

		count += len(results)

		return nil
	}).Error; err != nil {
		return models, fmt.Errorf("unable to fetch CWE records: %w", err)
	}

	return models, nil
}

func (s *vulnerabilityDecoratorStore) AddKnownExploitedVulnerabilities(kevs ...*KnownExploitedVulnerabilityHandle) error {
	if !s.kevEnabled {
		// when populating a new DB any capability issues found should result in halting
		return ErrDBCapabilityNotSupported
	}

	for i := range kevs {
		k := kevs[i]
		// this adds the blob value to the DB and sets the ID on the kev handle
		if err := s.blobStore.addBlobable(k); err != nil {
			return fmt.Errorf("unable to add KEV blob: %w", err)
		}

		if err := s.db.Create(k).Error; err != nil {
			return fmt.Errorf("unable to create known exploited vulnerability: %w", err)
		}
	}
	return nil
}

func (s *vulnerabilityDecoratorStore) GetKnownExploitedVulnerabilities(cve string) ([]KnownExploitedVulnerabilityHandle, error) {
	if !s.kevEnabled {
		// capability incompatibilities should gracefully degrade, returning no data or errors
		return nil, nil
	}

	fields := logger.Fields{
		"cve": cve,
	}
	start := time.Now()
	var count int
	defer func() {
		fields["duration"] = time.Since(start)
		fields["records"] = count
		log.WithFields(fields).Trace("fetched KEV records")
	}()

	var models []KnownExploitedVulnerabilityHandle
	var results []*KnownExploitedVulnerabilityHandle

	if err := s.db.Where("cve = ? collate nocase", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {
		var blobs []blobable
		for _, r := range results {
			blobs = append(blobs, r)
		}
		if err := s.blobStore.attachBlobValue(blobs...); err != nil {
			return fmt.Errorf("unable to attach KEV blobs: %w", err)
		}

		for _, r := range results {
			models = append(models, *r)
		}

		count += len(results)

		return nil
	}).Error; err != nil {
		return models, fmt.Errorf("unable to fetch KEV records: %w", err)
	}

	return models, nil
}
