//
// Copyright 2021 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package api

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net/http"
	"strings"

	"github.com/go-openapi/runtime/middleware"
	"github.com/go-openapi/swag/conv"

	"github.com/sigstore/rekor/pkg/generated/models"
	"github.com/sigstore/rekor/pkg/generated/restapi/operations/index"
	"github.com/sigstore/rekor/pkg/pki"
	"github.com/sigstore/rekor/pkg/util"
)

func SearchIndexHandler(params index.SearchIndexParams) middleware.Responder {
	httpReqCtx := params.HTTPRequest.Context()

	queryOperator := params.Query.Operator
	// default to "or" if no operator is specified
	if params.Query.Operator == "" {
		queryOperator = "or"
	}
	var result = NewCollection(queryOperator)

	var lookupKeys []string

	if params.Query.Hash != "" {
		// This must be a valid hash
		sha := strings.ToLower(util.PrefixSHA(params.Query.Hash))
		if queryOperator == "or" {
			lookupKeys = append(lookupKeys, sha)
		} else {
			resultUUIDs, err := indexStorageClient.LookupIndices(httpReqCtx, []string{sha})
			if err != nil {
				return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("index storage error: %w", err), indexStorageUnexpectedResult)
			}
			result.Add(resultUUIDs)
		}
	}
	if params.Query.PublicKey != nil {
		af, err := pki.NewArtifactFactory(pki.Format(conv.Value(params.Query.PublicKey.Format)))
		if err != nil {
			return handleRekorAPIError(params, http.StatusBadRequest, err, unsupportedPKIFormat)
		}
		keyReader := bytes.NewReader(params.Query.PublicKey.Content)

		key, err := af.NewPublicKey(keyReader)
		if err != nil {
			return handleRekorAPIError(params, http.StatusBadRequest, err, malformedPublicKey)
		}
		canonicalKey, err := key.CanonicalValue()
		if err != nil {
			return handleRekorAPIError(params, http.StatusInternalServerError, err, failedToGenerateCanonicalKey)
		}

		keyHash := sha256.Sum256(canonicalKey)
		keyHashStr := strings.ToLower(hex.EncodeToString(keyHash[:]))
		if queryOperator == "or" {
			lookupKeys = append(lookupKeys, keyHashStr)
		} else {
			resultUUIDs, err := indexStorageClient.LookupIndices(httpReqCtx, []string{keyHashStr})
			if err != nil {
				return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("index storage error: %w", err), indexStorageUnexpectedResult)
			}
			result.Add(resultUUIDs)
		}
	}
	if params.Query.Email != "" {
		emailStr := strings.ToLower(params.Query.Email.String())
		if queryOperator == "or" {
			lookupKeys = append(lookupKeys, emailStr)
		} else {
			resultUUIDs, err := indexStorageClient.LookupIndices(httpReqCtx, []string{emailStr})
			if err != nil {
				return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("index storage error: %w", err), indexStorageUnexpectedResult)
			}
			result.Add(resultUUIDs)
		}
	}
	if len(lookupKeys) > 0 {
		resultUUIDs, err := indexStorageClient.LookupIndices(httpReqCtx, lookupKeys)
		if err != nil {
			return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("index storage error: %w", err), indexStorageUnexpectedResult)
		}
		result.Add(resultUUIDs)
	}

	return index.NewSearchIndexOK().WithPayload(result.Values())
}

func SearchIndexNotImplementedHandler(_ index.SearchIndexParams) middleware.Responder {
	err := models.Error{
		Code:    http.StatusNotImplemented,
		Message: "Search Index API not enabled in this Rekor instance",
	}

	return index.NewSearchIndexDefault(http.StatusNotImplemented).WithPayload(&err)

}

func addToIndex(ctx context.Context, keys []string, value string) error {
	err := indexStorageClient.WriteIndex(ctx, keys, value)
	if err != nil {
		return fmt.Errorf("redis client: %w", err)
	}
	return nil
}

func storeAttestation(ctx context.Context, uuid string, attestation []byte) error {
	return attestationStorageClient.StoreAttestation(ctx, uuid, attestation)
}

// Uniq is a collection of unique elements.
type Uniq map[string]struct{}

func NewUniq() Uniq {
	return make(Uniq)
}

func (u Uniq) Add(elements ...string) {
	for _, e := range elements {
		u[e] = struct{}{}
	}
}

func (u Uniq) Values() []string {
	var result []string
	for k := range u {
		result = append(result, k)
	}
	return result
}

// Intersect returns the intersection of two collections.
func (u Uniq) Intersect(other Uniq) Uniq {
	result := make(Uniq)
	for k := range u {
		if _, ok := other[k]; ok {
			result[k] = struct{}{}
		}
	}
	return result
}

// Union returns the union of two collections.
func (u Uniq) Union(other Uniq) Uniq {
	result := make(Uniq)
	for k := range u {
		result[k] = struct{}{}
	}
	for k := range other {
		result[k] = struct{}{}
	}
	return result
}

// Collection is a collection of sets.
//
// its resulting values is a union or intersection of all the sets, depending on the operator.
type Collection struct {
	subsets  []Uniq
	operator string
}

// NewCollection creates a new collection.
func NewCollection(operator string) *Collection {
	return &Collection{
		subsets:  []Uniq{},
		operator: operator,
	}
}

// Add adds the elements into a new subset in the collection.
func (u *Collection) Add(elements []string) {
	subset := Uniq{}
	subset.Add(elements...)
	u.subsets = append(u.subsets, subset)
}

// Values flattens the subsets using the operator, and returns the collection as a slice of strings.
func (u *Collection) Values() []string {
	if len(u.subsets) == 0 {
		return []string{}
	}
	subset := u.subsets[0]
	for i := 1; i < len(u.subsets); i++ {
		if strings.EqualFold(u.operator, "and") {
			subset = subset.Intersect(u.subsets[i])
		} else {
			subset = subset.Union(u.subsets[i])
		}
	}
	return subset.Values()
}
