//go:build imagetrust
// +build imagetrust

package extensions

import (
	"errors"
	"io"
	"net/http"
	"time"

	"github.com/gorilla/mux"

	zerr "zotregistry.dev/zot/errors"
	"zotregistry.dev/zot/pkg/api/config"
	"zotregistry.dev/zot/pkg/api/constants"
	zcommon "zotregistry.dev/zot/pkg/common"
	"zotregistry.dev/zot/pkg/extensions/imagetrust"
	"zotregistry.dev/zot/pkg/log"
	mTypes "zotregistry.dev/zot/pkg/meta/types"
	"zotregistry.dev/zot/pkg/scheduler"
	sconstants "zotregistry.dev/zot/pkg/storage/constants"
)

func IsBuiltWithImageTrustExtension() bool {
	return true
}

func SetupImageTrustRoutes(conf *config.Config, router *mux.Router, metaDB mTypes.MetaDB, log log.Logger) {
	if !conf.IsImageTrustEnabled() || (!conf.IsCosignEnabled() && !conf.IsNotationEnabled()) {
		log.Info().Msg("skip enabling the image trust routes as the config prerequisites are not met")

		return
	}

	log.Info().Msg("setting up image trust routes")

	imgTrustStore, _ := metaDB.ImageTrustStore().(*imagetrust.ImageTrustStore)
	trust := ImageTrust{Conf: conf, ImageTrustStore: imgTrustStore, Log: log}
	allowedMethods := zcommon.AllowedMethods(http.MethodPost)

	if conf.IsNotationEnabled() {
		log.Info().Msg("setting up notation route")

		notationRouter := router.PathPrefix(constants.ExtNotation).Subrouter()
		notationRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
		notationRouter.Use(zcommon.AddExtensionSecurityHeaders())
		notationRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
		// The endpoints for uploading signatures should be available only to admins
		notationRouter.Use(zcommon.AuthzOnlyAdminsMiddleware(conf))
		notationRouter.Methods(allowedMethods...).HandlerFunc(trust.HandleNotationCertificateUpload)
	}

	if conf.IsCosignEnabled() {
		log.Info().Msg("setting up cosign route")

		cosignRouter := router.PathPrefix(constants.ExtCosign).Subrouter()
		cosignRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
		cosignRouter.Use(zcommon.AddExtensionSecurityHeaders())
		cosignRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
		// The endpoints for uploading signatures should be available only to admins
		cosignRouter.Use(zcommon.AuthzOnlyAdminsMiddleware(conf))
		cosignRouter.Methods(allowedMethods...).HandlerFunc(trust.HandleCosignPublicKeyUpload)
	}

	log.Info().Msg("finished setting up image trust routes")
}

type ImageTrust struct {
	Conf            *config.Config
	ImageTrustStore *imagetrust.ImageTrustStore
	Log             log.Logger
}

// Cosign handler godoc
// @Summary Upload cosign public keys for verifying signatures
// @Description Upload cosign public keys for verifying signatures
// @Router   /v2/_zot/ext/cosign [post]
// @Accept  octet-stream
// @Produce json
// @Param   requestBody     body     string   true   "Public key content"
// @Success 200 {string}   string    "ok"
// @Failure 400 {string}   string    "bad request".
// @Failure 500 {string}   string    "internal server error".
func (trust *ImageTrust) HandleCosignPublicKeyUpload(response http.ResponseWriter, request *http.Request) {
	body, err := io.ReadAll(request.Body)
	if err != nil {
		trust.Log.Error().Err(err).Str("component", "image-trust").Msg("failed to read cosign key body")
		response.WriteHeader(http.StatusInternalServerError)

		return
	}

	err = imagetrust.UploadPublicKey(trust.ImageTrustStore.CosignStorage, body)
	if err != nil {
		if errors.Is(err, zerr.ErrInvalidPublicKeyContent) {
			response.WriteHeader(http.StatusBadRequest)
		} else {
			trust.Log.Error().Err(err).Str("component", "image-trust").Msg("failed to save cosign key")
			response.WriteHeader(http.StatusInternalServerError)
		}

		return
	}

	response.WriteHeader(http.StatusOK)
}

// Notation handler godoc
// @Summary Upload notation certificates for verifying signatures
// @Description Upload notation certificates for verifying signatures
// @Router  /v2/_zot/ext/notation [post]
// @Accept  octet-stream
// @Produce json
// @Param   truststoreType  query    string   false  "truststore type"
// @Param   requestBody     body     string   true   "Certificate content"
// @Success 200 {string}   string    "ok"
// @Failure 400 {string}   string    "bad request".
// @Failure 500 {string}   string    "internal server error".
func (trust *ImageTrust) HandleNotationCertificateUpload(response http.ResponseWriter, request *http.Request) {
	var truststoreType string

	if zcommon.QueryHasParams(request.URL.Query(), []string{"truststoreType"}) {
		truststoreType = request.URL.Query().Get("truststoreType")
	} else {
		truststoreType = "ca" // default value of "truststoreType" query param
	}

	body, err := io.ReadAll(request.Body)
	if err != nil {
		trust.Log.Error().Err(err).Str("component", "image-trust").Msg("failed to read notation certificate body")
		response.WriteHeader(http.StatusInternalServerError)

		return
	}

	err = imagetrust.UploadCertificate(trust.ImageTrustStore.NotationStorage, body, truststoreType)
	if err != nil {
		if errors.Is(err, zerr.ErrInvalidTruststoreType) ||
			errors.Is(err, zerr.ErrInvalidCertificateContent) {
			response.WriteHeader(http.StatusBadRequest)
		} else {
			trust.Log.Error().Err(err).Str("component", "image-trust").Msg("failed to save notation certificate")
			response.WriteHeader(http.StatusInternalServerError)
		}

		return
	}

	response.WriteHeader(http.StatusOK)
}

func EnableImageTrustVerification(conf *config.Config, taskScheduler *scheduler.Scheduler,
	metaDB mTypes.MetaDB, log log.Logger,
) {
	if !conf.IsImageTrustEnabled() {
		return
	}

	generator := imagetrust.NewTaskGenerator(metaDB, log)

	numberOfHours := 2
	interval := time.Duration(numberOfHours) * time.Hour
	taskScheduler.SubmitGenerator(generator, interval, scheduler.MediumPriority)
}

func SetupImageTrustExtension(conf *config.Config, metaDB mTypes.MetaDB, log log.Logger) error {
	if !conf.IsImageTrustEnabled() {
		return nil
	}

	var imgTrustStore mTypes.ImageTrustStore

	var err error

	if conf.Storage.RemoteCache && conf.Storage.CacheDriver["name"] == sconstants.DynamoDBDriverName {
		// AWS secrets manager
		// In case of AWS let's assume if dynamodDB is used, the AWS secrets manager is also used
		// we use the CacheDriver settings as opposed to the storage settings because we want to
		// be able to use S3/minio and redis in the same configuration
		endpoint, _ := conf.Storage.CacheDriver["endpoint"].(string)
		region, _ := conf.Storage.CacheDriver["region"].(string)

		imgTrustStore, err = imagetrust.NewAWSImageTrustStore(region, endpoint)
		if err != nil {
			return err
		}
	} else {
		// Store secrets on the local disk
		imgTrustStore, err = imagetrust.NewLocalImageTrustStore(conf.Storage.RootDirectory)
		if err != nil {
			return err
		}
	}

	metaDB.SetImageTrustStore(imgTrustStore)

	return nil
}
