package v6

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/scylladb/go-set/strset"
	"gorm.io/gorm"

	"github.com/anchore/grype/internal/log"
)

type VulnerabilityStoreWriter interface {
	AddVulnerabilities(vulns ...*VulnerabilityHandle) error
}

type VulnerabilityStoreReader interface {
	GetVulnerabilities(vuln *VulnerabilitySpecifier, config *GetVulnerabilityOptions) ([]VulnerabilityHandle, error)
}

type GetVulnerabilityOptions struct {
	Preload bool
}

type VulnerabilitySpecifier struct {
	// Name of the vulnerability (e.g. CVE-2020-1234)
	Name string

	// ID is the DB ID of the vulnerability
	ID ID

	// Status is the status of the vulnerability (e.g. "active", "rejected", etc.)
	Status VulnerabilityStatus

	// PublishedAfter is a filter to only return vulnerabilities published after the given time
	PublishedAfter *time.Time

	// ModifiedAfter is a filter to only return vulnerabilities modified after the given time
	ModifiedAfter *time.Time

	// IncludeAliases for the given name or ID in results
	IncludeAliases bool
}

func (v *VulnerabilitySpecifier) String() string {
	var parts []string
	if v.Name != "" {
		parts = append(parts, fmt.Sprintf("name=%s", v.Name))
	}

	if v.ID != 0 {
		parts = append(parts, fmt.Sprintf("id=%d", v.ID))
	}

	if v.Status != "" {
		parts = append(parts, fmt.Sprintf("status=%s", v.Status))
	}

	if v.PublishedAfter != nil {
		parts = append(parts, fmt.Sprintf("publishedAfter=%s", v.PublishedAfter.String()))
	}

	if v.ModifiedAfter != nil {
		parts = append(parts, fmt.Sprintf("modifiedAfter=%s", v.ModifiedAfter.String()))
	}

	return fmt.Sprintf("VulnerabilitySpecifier(%s)", strings.Join(parts, ", "))
}

func DefaultGetVulnerabilityOptions() *GetVulnerabilityOptions {
	return &GetVulnerabilityOptions{
		Preload: false,
	}
}

type vulnerabilityStore struct {
	db        *gorm.DB
	blobStore *blobStore
}

func newVulnerabilityStore(db *gorm.DB, bs *blobStore) *vulnerabilityStore {
	return &vulnerabilityStore{
		db:        db,
		blobStore: bs,
	}
}

func (s *vulnerabilityStore) AddVulnerabilities(vulnerabilities ...*VulnerabilityHandle) error {
	for _, v := range vulnerabilities {
		// this adds the blob value to the DB and sets the ID on the vulnerability handle
		if err := s.blobStore.addBlobable(v); err != nil {
			return fmt.Errorf("unable to add affected blob: %w", err)
		}

		if v.PublishedDate != nil && v.ModifiedDate == nil {
			// the data here should be consistent, and we are norming around initial publication counts as a modification date.
			// this allows for easily refining queries based on both publication date and modification date without needing
			// to worry about this edge case.
			v.ModifiedDate = v.PublishedDate
		}

		if v.BlobValue != nil {
			aliases := strset.New(v.BlobValue.Aliases...)
			aliases.Remove(v.Name)
			var aliasModels []VulnerabilityAlias
			for _, alias := range aliases.List() {
				aliasModels = append(aliasModels, VulnerabilityAlias{
					Name:  v.Name,
					Alias: alias,
				})
			}
			for _, aliasModel := range aliasModels {
				if err := s.db.FirstOrCreate(&aliasModel).Error; err != nil {
					return err
				}
			}
		}

		if err := s.addUniqueVulnerability(v); err != nil {
			return err
		}
	}
	return nil
}

func (s *vulnerabilityStore) addUniqueVulnerability(v *VulnerabilityHandle) error {
	// if this is a unique (name, status, published, modified, withdrawn, provider_id, blob_id) then it will be created
	// otherwise do not create the new entry (this is to prevent duplicates)
	query := s.db.Where("name = ? AND status = ? AND provider_id = ? AND blob_id = ?", v.Name, v.Status, v.ProviderID, v.BlobID)

	if v.PublishedDate != nil {
		query = query.Where("published_date = ?", *v.PublishedDate)
	} else {
		query = query.Where("published_date IS NULL")
	}

	if v.ModifiedDate != nil {
		query = query.Where("modified_date = ?", *v.ModifiedDate)
	} else {
		query = query.Where("modified_date IS NULL")
	}

	if v.WithdrawnDate != nil {
		query = query.Where("withdrawn_date = ?", *v.WithdrawnDate)
	} else {
		query = query.Where("withdrawn_date IS NULL")
	}

	if err := query.First(v).Error; err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}
		if err := s.db.Create(v).Error; err != nil {
			return err
		}
	}
	return nil
}

func (s *vulnerabilityStore) GetVulnerabilities(vuln *VulnerabilitySpecifier, config *GetVulnerabilityOptions) ([]VulnerabilityHandle, error) {
	if config == nil {
		config = DefaultGetVulnerabilityOptions()
	}
	log.WithFields("vuln", vuln.String(), "preload", config.Preload).Trace("fetching Vulnerability records")

	var models []VulnerabilityHandle

	query, err := handleVulnerabilityOptions(s.db, vuln)
	if err != nil {
		return nil, err
	}

	query = s.handlePreload(query, *config)

	result := query.Find(&models)

	if result.Error != nil {
		return nil, fmt.Errorf("unable to fetch vulnerability records: %w", result.Error)
	}

	if config.Preload {
		for i := range models {
			err := s.attachBlob(&models[i])
			if err != nil {
				return nil, fmt.Errorf("unable to attach blob %#v: %w", models[i], err)
			}
		}
	}

	return models, err
}

func (s *vulnerabilityStore) handlePreload(query *gorm.DB, config GetVulnerabilityOptions) *gorm.DB {
	if config.Preload {
		query = query.Preload("Provider")
	}
	return query
}

func (s *vulnerabilityStore) attachBlob(vh *VulnerabilityHandle) error {
	var blobValue *VulnerabilityBlob

	rawValue, err := s.blobStore.getBlobValue(vh.BlobID)
	if err != nil {
		return fmt.Errorf("unable to fetch vulnerability blob value: %w", err)
	}

	err = json.Unmarshal([]byte(rawValue), &blobValue)
	if err != nil {
		return fmt.Errorf("unable to unmarshal vulnerability blob value: %w", err)
	}

	vh.BlobValue = blobValue

	return nil
}

func handleVulnerabilityOptions(query *gorm.DB, config *VulnerabilitySpecifier) (*gorm.DB, error) {
	if config.Name != "" {
		if config.IncludeAliases {
			query = query.Joins("LEFT JOIN vulnerability_aliases ON vulnerability_aliases.name = vulnerability_handles.name")
			query = query.Where("vulnerability_handles.name = ? OR vulnerability_aliases.alias = ?", config.Name, config.Name)
		} else {
			query = query.Where("vulnerability_handles.name = ?", config.Name)
		}
	}

	if config.ID != 0 {
		query = query.Where("vulnerability_handles.id = ?", config.ID)
	}

	if config.PublishedAfter != nil {
		query = query.Where("vulnerability_handles.published_date > ?", *config.PublishedAfter)
	}

	if config.ModifiedAfter != nil {
		query = query.Where("vulnerability_handles.modified_date > ?", *config.ModifiedAfter)
	}

	if config.Status != "" {
		query = query.Where("vulnerability_handles.status = ?", config.Status)
	}

	return query, nil
}
