package middleware

import (
	"net/http"
	"path"
	"path/filepath"
	"strings"
	"time"

	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/go-chi/render"
	tusd "github.com/tus/tusd/pkg/handler"
	"google.golang.org/grpc/metadata"

	revactx "github.com/cs3org/reva/v2/pkg/ctx"
	"github.com/cs3org/reva/v2/pkg/storagespace"
	"github.com/cs3org/reva/v2/pkg/utils"

	pMessage "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/policies/v0"
	pService "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/policies/v0"
	"github.com/owncloud/ocis/v2/services/webdav/pkg/net"
)

type (
	// RequestDenied struct for OdataErrorMain
	RequestDenied struct {
		Error RequestDeniedError `json:"error"`
	}

	// RequestDeniedError struct for RequestDenied
	RequestDeniedError struct {
		Code    string `json:"code"`
		Message string `json:"message"`
		// The structure of this object is service-specific
		Innererror map[string]interface{} `json:"innererror,omitempty"`
	}
)

const DeniedMessage = "Operation denied due to security policies"

// Policies verifies if a request is granted or not.
func Policies(qs string, opts ...Option) func(next http.Handler) http.Handler {
	options := newOptions(opts...)
	logger := options.Logger
	gatewaySelector := options.RevaGatewaySelector
	policiesProviderClient := options.PoliciesProviderService

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if qs == "" {
				next.ServeHTTP(w, r)
				return
			}

			req := &pService.EvaluateRequest{
				Query: qs,
				Environment: &pMessage.Environment{
					Request: &pMessage.Request{
						Method: r.Method,
						Path:   r.URL.Path,
					},
					Stage: pMessage.Stage_STAGE_HTTP,
				},
			}

			resource := &pMessage.Resource{}

			// tus
			meta := tusd.ParseMetadataHeader(r.Header.Get(net.HeaderUploadMetadata))
			resource.Name = meta["filename"]

			// name is part of the request path
			if resource.Name == "" && filepath.Ext(r.URL.Path) != "" {
				resource.Name = filepath.Base(r.URL.Path)
			}

			// no resource info in path, stat the resource and try to obtain the file information.
			// this should only be used as last bastion, every request goes through the proxy and doing stats is expensive!
			// needed for:
			// - if a single resource is shared -> the url only contains the resourceID (spaceRef)
			if resource.Name == "" && filepath.Ext(r.URL.Path) == "" && r.Method == http.MethodPut && strings.HasPrefix(r.URL.Path, "/remote.php/dav/spaces") {
				client, err := gatewaySelector.Next()
				if err != nil {
					logger.Err(err).Msg("error selecting next gateway client")
					RenderError(w, r, req, http.StatusForbidden, DeniedMessage)
					return
				}

				resourceID, err := storagespace.ParseID(strings.TrimPrefix(r.URL.Path, "/remote.php/dav/spaces/"))
				if err != nil {
					logger.Debug().Err(err).Msg("error parsing the resourceId")
					RenderError(w, r, req, http.StatusForbidden, DeniedMessage)
					return
				}

				if resourceID.StorageId == "" && resourceID.SpaceId == utils.ShareStorageSpaceID {
					resourceID.StorageId = utils.ShareStorageProviderID
				}

				token := r.Header.Get(revactx.TokenHeader)
				ctx := metadata.AppendToOutgoingContext(r.Context(), revactx.TokenHeader, token)
				sRes, err := client.Stat(ctx, &provider.StatRequest{
					Ref: &provider.Reference{
						ResourceId: &resourceID,
					},
				})

				resource.Name = sRes.GetInfo().GetName()
			}

			req.Environment.Resource = resource

			if user, ok := revactx.ContextGetUser(r.Context()); ok {
				req.Environment.User = &pMessage.User{
					Id: &pMessage.User_ID{
						OpaqueId: user.GetId().GetOpaqueId(),
					},
					Username:    user.GetUsername(),
					Mail:        user.GetMail(),
					DisplayName: user.GetDisplayName(),
					Groups:      user.GetGroups(),
				}
			}

			rsp, err := policiesProviderClient.Evaluate(r.Context(), req)
			if err != nil {
				logger.Err(err).Msg("error evaluating request")
				w.WriteHeader(http.StatusInternalServerError)
				return
			}

			if !rsp.Result {
				RenderError(w, r, req, http.StatusForbidden, DeniedMessage)
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

// RenderError writes a Policies ErrorObject to the response writer
func RenderError(w http.ResponseWriter, r *http.Request, evaluateReq *pService.EvaluateRequest, status int, msg string) {
	filename := evaluateReq.Environment.GetResource().GetName()
	if filename == "" {
		filename = path.Base(evaluateReq.Environment.GetRequest().GetPath())
	}

	innererror := map[string]interface{}{
		"date": time.Now().UTC().Format(time.RFC3339),
	}

	innererror["request-id"] = middleware.GetReqID(r.Context())
	innererror["method"] = evaluateReq.Environment.GetRequest().GetMethod()
	innererror["filename"] = filename
	innererror["path"] = evaluateReq.Environment.GetRequest().GetPath()

	resp := &RequestDenied{
		Error: RequestDeniedError{
			Code:       "deniedByPolicy",
			Message:    msg,
			Innererror: innererror,
		},
	}
	render.Status(r, status)
	render.JSON(w, r, resp)
}
