//go:build search
// +build search

package extensions

import (
	"net/http"
	"sync"
	"time"

	gqlHandler "github.com/99designs/gqlgen/graphql/handler"
	"github.com/gorilla/mux"

	"zotregistry.io/zot/pkg/api/config"
	"zotregistry.io/zot/pkg/api/constants"
	zcommon "zotregistry.io/zot/pkg/common"
	"zotregistry.io/zot/pkg/extensions/search"
	cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
	"zotregistry.io/zot/pkg/extensions/search/gql_generated"
	"zotregistry.io/zot/pkg/log"
	mTypes "zotregistry.io/zot/pkg/meta/types"
	"zotregistry.io/zot/pkg/scheduler"
	"zotregistry.io/zot/pkg/storage"
)

type (
	CveInfo cveinfo.CveInfo
	state   int
)

const (
	pending state = iota
	running
	done
)

func IsBuiltWithSearchExtension() bool {
	return true
}

func GetCVEInfo(config *config.Config, storeController storage.StoreController,
	metaDB mTypes.MetaDB, log log.Logger,
) CveInfo {
	if config.Extensions.Search == nil || !*config.Extensions.Search.Enable || config.Extensions.Search.CVE == nil {
		return nil
	}

	dbRepository := config.Extensions.Search.CVE.Trivy.DBRepository
	javaDBRepository := config.Extensions.Search.CVE.Trivy.JavaDBRepository

	return cveinfo.NewCVEInfo(storeController, metaDB, dbRepository, javaDBRepository, log)
}

func EnableSearchExtension(config *config.Config, storeController storage.StoreController,
	metaDB mTypes.MetaDB, taskScheduler *scheduler.Scheduler, cveInfo CveInfo, log log.Logger,
) {
	if config.Extensions.Search != nil && *config.Extensions.Search.Enable && config.Extensions.Search.CVE != nil {
		updateInterval := config.Extensions.Search.CVE.UpdateInterval

		downloadTrivyDB(updateInterval, taskScheduler, cveInfo, log)
	} else {
		log.Info().Msg("CVE config not provided, skipping CVE update")
	}
}

func downloadTrivyDB(interval time.Duration, sch *scheduler.Scheduler, cveInfo CveInfo, log log.Logger) {
	generator := NewTrivyTaskGenerator(interval, cveInfo, log)

	log.Info().Msg("Submitting CVE DB update scheduler")
	sch.SubmitGenerator(generator, interval, scheduler.HighPriority)
}

func NewTrivyTaskGenerator(interval time.Duration, cveInfo CveInfo, log log.Logger) *TrivyTaskGenerator {
	generator := &TrivyTaskGenerator{interval, cveInfo, log, pending, 0, time.Now(), &sync.Mutex{}}

	return generator
}

type TrivyTaskGenerator struct {
	interval     time.Duration
	cveInfo      CveInfo
	log          log.Logger
	status       state
	waitTime     time.Duration
	lastTaskTime time.Time
	lock         *sync.Mutex
}

func (gen *TrivyTaskGenerator) Next() (scheduler.Task, error) {
	var newTask scheduler.Task

	gen.lock.Lock()

	if gen.status == pending && time.Since(gen.lastTaskTime) >= gen.waitTime {
		newTask = newTrivyTask(gen.interval, gen.cveInfo, gen, gen.log)
		gen.status = running
	}
	gen.lock.Unlock()

	return newTask, nil
}

func (gen *TrivyTaskGenerator) IsDone() bool {
	gen.lock.Lock()
	status := gen.status
	gen.lock.Unlock()

	return status == done
}

func (gen *TrivyTaskGenerator) Reset() {
	gen.lock.Lock()
	gen.status = pending
	gen.waitTime = 0
	gen.lock.Unlock()
}

type trivyTask struct {
	interval  time.Duration
	cveInfo   cveinfo.CveInfo
	generator *TrivyTaskGenerator
	log       log.Logger
}

func newTrivyTask(interval time.Duration, cveInfo cveinfo.CveInfo,
	generator *TrivyTaskGenerator, log log.Logger,
) *trivyTask {
	return &trivyTask{interval, cveInfo, generator, log}
}

func (trivyT *trivyTask) DoWork() error {
	trivyT.log.Info().Msg("updating the CVE database")

	err := trivyT.cveInfo.UpdateDB()
	if err != nil {
		trivyT.generator.lock.Lock()
		trivyT.generator.status = pending

		if trivyT.generator.waitTime == 0 {
			trivyT.generator.waitTime = time.Second
		}

		trivyT.generator.waitTime *= 2
		trivyT.generator.lastTaskTime = time.Now()
		trivyT.generator.lock.Unlock()

		return err
	}

	trivyT.generator.lock.Lock()
	trivyT.generator.lastTaskTime = time.Now()
	trivyT.generator.status = done
	trivyT.generator.lock.Unlock()
	trivyT.log.Info().Str("DB update completed, next update scheduled after", trivyT.interval.String()).Msg("")

	return nil
}

func SetupSearchRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
	metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger,
) {
	log.Info().Msg("setting up search routes")

	if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
		resConfig := search.GetResolverConfig(log, storeController, metaDB, cveInfo)

		allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)

		extRouter := router.PathPrefix(constants.ExtSearch).Subrouter()
		extRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...))
		extRouter.Use(zcommon.AddExtensionSecurityHeaders())
		extRouter.Methods(allowedMethods...).
			Handler(gqlHandler.NewDefaultServer(gql_generated.NewExecutableSchema(resConfig)))
	}
}
